AI Templates
A Liquid-based template engine for managing, rendering, and composing AI system prompts from multiple sources.
Quick Start
builder.Services.AddCoreAITemplating();
You rarely need to call this directly — AddCoreAIServices() chains it automatically.
Problem & Solution
Hard-coding system prompts in C# makes them difficult to maintain, localize, and customize. The template system:
- Stores prompts as markdown files with front-matter metadata
- Renders them with Liquid syntax for dynamic content
- Discovers templates from multiple sources (embedded resources, file system, code)
- Supports merging multiple templates into a single prompt
Services Registered by AddCoreAITemplating()
AddCoreAITemplating() builds on the lower-level AddTemplating() registration and also adds the built-in AI template source metadata for SystemPrompt and Profile templates.
| Service | Implementation | Lifetime | Purpose |
|---|---|---|---|
ITemplateParser | DefaultMarkdownTemplateParser | Singleton | Parses markdown front-matter templates |
ITemplateEngine | FluidTemplateEngine | Singleton | Renders Liquid templates |
ITemplateService | DefaultTemplateService | Scoped | Unified template discovery and rendering |
OptionsTemplateProvider | — | Singleton | Templates registered via code |
FileSystemTemplateProvider | — | Singleton | Templates discovered from disk |
Key Interfaces
ITemplateService
The main service for working with templates.
public interface ITemplateService
{
Task<IReadOnlyList<Template>> ListAsync();
Task<Template> GetAsync(string id);
Task<string> RenderAsync(string id, IDictionary<string, object> arguments = null);
Task<string> MergeAsync(
IEnumerable<string> ids,
IDictionary<string, object> arguments = null,
string separator = "\n\n");
}
ITemplateEngine
Renders Liquid templates. You can replace this with a custom engine.
public interface ITemplateEngine
{
Task<string> RenderAsync(string template, IDictionary<string, object> arguments);
bool TryValidate(string template, out IReadOnlyList<string> errors);
}
ITemplateProvider
Implement to supply templates from a custom source (database, API, etc.).
public interface ITemplateProvider
{
Task<IReadOnlyList<Template>> GetTemplatesAsync();
}
Template File Format
Templates are markdown files with YAML front matter, stored in Templates/Prompts/:
---
Title: Helpful Assistant
Description: A general-purpose helpful assistant prompt
Category: General
IsListable: true
---
You are a helpful assistant. Today's date is {{ "now" | date: "%Y-%m-%d" }}.
{% if user_name %}
You are assisting {{ user_name }}.
{% endif %}
Front Matter Fields
| Field | Type | Description |
|---|---|---|
Title | string | Display name |
Description | string | Human-readable description |
Category | string | Grouping category |
IsListable | bool | Whether the template appears in listing APIs |
Registering Templates
From Embedded Resources
Store .md files as embedded resources under Templates/Prompts/ in your assembly:
builder.Services.AddTemplatesFromAssembly(typeof(MyClass).Assembly, source: "MyApp");
From Code
builder.Services.AddTemplating(options =>
{
options.AddTemplate("my-template", "You are {{ role }}.", metadata =>
{
metadata.Title = "Role Template";
});
});
From File System
builder.Services.AddTemplating(options =>
{
options.AddDiscoveryPath("/app/templates");
});
Configuration
TemplateOptions
services.Configure<TemplateOptions>(options =>
{
options.AddDiscoveryPath("/path/to/templates");
options.AddTemplate(new Template { /* ... */ });
});
Template Examples
Example 1: Customer Support Assistant
---
Title: Customer Support Assistant
Description: Prompt for a customer-facing support chatbot
Category: Support
IsListable: true
---
You are a customer support assistant for {{ company_name | default: "our company" }}.
## Your Responsibilities
- Answer product questions accurately and concisely.
- Help customers troubleshoot common issues.
- Escalate to a human agent when you cannot resolve an issue.
## Guidelines
- Always be polite and professional.
- Never share internal pricing or unreleased product details.
- Today's date is {{ "now" | date: "%B %d, %Y" }}.
{% if support_hours %}
Our support hours are {{ support_hours }}. If the customer is contacting us outside
these hours, let them know when support will be available.
{% endif %}
{% if knowledge_base_context %}
## Relevant Knowledge Base Articles
{{ knowledge_base_context }}
{% endif %}
Example 2: Content Writer with Tone Control
---
Title: Content Writer
Description: Generates content with configurable tone and style
Category: Content
IsListable: true
---
You are a professional content writer.
**Tone**: {{ tone | default: "professional" }}
**Target audience**: {{ audience | default: "general" }}
**Max length**: {{ max_words | default: "500" }} words
{% case tone %}
{% when "casual" %}
Write in a friendly, conversational style. Use contractions and simple language.
{% when "formal" %}
Write in a formal, authoritative style. Avoid contractions and colloquialisms.
{% when "technical" %}
Write with precision. Use domain-specific terminology and cite sources where possible.
{% endcase %}
Example 3: RAG-Augmented Assistant
---
Title: RAG Assistant
Description: Assistant that uses retrieved documents for grounded answers
Category: RAG
IsListable: false
---
You are a knowledgeable assistant. Answer questions using ONLY the provided context.
## Rules
- If the context does not contain enough information, say so honestly.
- Always cite which document or section your answer comes from.
- Do not make up information that is not in the provided context.
{% if retrieved_documents %}
## Retrieved Context
{% for doc in retrieved_documents %}
### Document: {{ doc.title }}
{{ doc.content }}
{% endfor %}
{% endif %}
Liquid Reference
The template engine uses the Fluid Liquid implementation. Here are the most commonly used filters and tags in templates:
Filters
| Filter | Example | Output |
|---|---|---|
date | {{ "now" | date: "%Y-%m-%d" }} | 2025-01-15 |
default | {{ name | default: "User" }} | User (if name is nil) |
upcase | {{ "hello" | upcase }} | HELLO |
downcase | {{ "HELLO" | downcase }} | hello |
truncate | {{ text | truncate: 100 }} | First 100 characters |
strip_html | {{ html_content | strip_html }} | Plain text |
escape | {{ user_input | escape }} | HTML-escaped string |
size | {{ items | size }} | Number of items |
join | {{ tags | join: ", " }} | Comma-separated string |
Tags
{% comment %} Conditional content {% endcomment %}
{% if variable %}...{% elsif other %}...{% else %}...{% endif %}
{% comment %} Loops {% endcomment %}
{% for item in collection %}{{ item.name }}{% endfor %}
{% comment %} Switch/case {% endcomment %}
{% case variable %}{% when "value1" %}...{% when "value2" %}...{% endcase %}
{% comment %} Variable assignment {% endcomment %}
{% assign greeting = "Hello, " | append: user_name %}
Template Composition
For complex prompts, compose templates using the MergeAsync method instead of creating one monolithic template:
// Render multiple templates and merge into a single prompt
var systemPrompt = await templateService.MergeAsync(
["base-personality", "safety-rules", "rag-instructions"],
arguments: new Dictionary<string, object>
{
["user_name"] = currentUser.DisplayName,
["company"] = tenant.Name,
},
separator: "\n\n---\n\n"
);
Best practices for template composition:
- Base templates — Define shared personality, tone, and general rules.
- Feature templates — Add instructions for specific capabilities (RAG, tool use, safety).
- Context templates — Inject dynamic, session-specific content (user info, retrieved docs).
Keep each template focused on a single concern. Compose them at runtime rather than duplicating instructions across templates. This makes it easy to update one aspect (e.g., safety rules) without touching every template.
Testing Templates
Validate Liquid Syntax
Use ITemplateEngine.TryValidate() to check for syntax errors before saving a template:
var engine = serviceProvider.GetRequiredService<ITemplateEngine>();
var template = "Hello {{ user_name }}, today is {{ 'now' | date: '%A' }}.";
if (engine.TryValidate(template, out var errors))
{
Console.WriteLine("Template is valid.");
}
else
{
foreach (var error in errors)
{
Console.WriteLine($"Error: {error}");
}
}
Render with Test Arguments
Use ITemplateService.RenderAsync() to preview the final output:
var result = await templateService.RenderAsync("customer-support", new Dictionary<string, object>
{
["company_name"] = "Contoso",
["support_hours"] = "9 AM – 5 PM EST",
["knowledge_base_context"] = "Article: How to reset your password...",
});
// Inspect the rendered output
Console.WriteLine(result);
Unit Testing Templates
public sealed class TemplateRenderingTests
{
[Fact]
public async Task CustomerSupportTemplate_ShouldIncludeCompanyName()
{
// Arrange
var engine = new FluidTemplateEngine();
var template = "You are a support agent for {{ company_name }}.";
var arguments = new Dictionary<string, object>
{
["company_name"] = "TestCorp",
};
// Act
var result = await engine.RenderAsync(template, arguments);
// Assert
Assert.Contains("TestCorp", result);
}
[Fact]
public void InvalidTemplate_ShouldReturnErrors()
{
var engine = new FluidTemplateEngine();
var invalid = "Hello {{ user_name | nonexistent_filter }}";
var isValid = engine.TryValidate(invalid, out var errors);
Assert.False(isValid);
Assert.NotEmpty(errors);
}
}
Custom Template Provider
Implement ITemplateProvider to load templates from a custom source (e.g., a database or remote API):
public sealed class DatabaseAITemplateProvider(
ISession session,
ITemplateParser parser) : ITemplateProvider
{
public async Task<IReadOnlyList<Template>> GetTemplatesAsync()
{
var records = await session
.Query<PromptTemplateRecord, PromptTemplateIndex>()
.ListAsync();
var templates = new List<Template>();
foreach (var record in records)
{
// Parse the markdown content (with YAML front-matter) into an Template
if (parser.TryParse(record.Content, out var template))
{
template.Id = record.TemplateId;
template.Source = "Database";
templates.Add(template);
}
}
return templates;
}
}
Register the provider:
builder.Services.AddSingleton<ITemplateProvider, DatabaseAITemplateProvider>();
All registered ITemplateProvider instances are queried by ITemplateService. Templates from multiple providers are merged into a single collection. If two providers return templates with the same Id, the last-registered provider wins.