A fluent .NET 8 library for composing prompts to send to LLM APIs. Build system instructions, render templates with named variables, manage conversation history, and check token budgets — all with a clean, chainable API.
dotnet add package Zaiets.PromptBuilderusing Zaiets.PromptBuilder;
var result = new PromptBuilder()
.WithSystemPrompt("You are a concise technical writer.")
.WithTemplate("Explain {{concept}} in one paragraph, targeted at a {{audience}}.")
.WithVariable("concept", "async/await in C#")
.WithVariable("audience", "junior developer")
.Build();
// Send result.Messages to your preferred LLM client
Console.WriteLine(result.FormattedPrompt);
Console.WriteLine($"Estimated tokens: {result.EstimatedTokenCount}");| Feature | Description |
|---|---|
| Fluent API | Chainable methods — no boilerplate |
| Template engine | {{variable}} placeholders with strict-mode validation |
| Chat history | Add, trim to token limit, keep last N turns |
| Token counting | cl100k_base-compatible approximation (±10 % for English) |
| DI support | services.AddPromptBuilder() for ASP.NET Core |
| Immutable messages | ChatMessage is a value-like sealed class |
The main entry point. All methods return this for chaining.
var builder = new PromptBuilder(); // default tokenizer
var builder = new PromptBuilder(myCustomTokenizer); // custom tokenizerbuilder.WithSystemPrompt("You are a helpful assistant.");Replaces any previously set system message.
builder
.WithTemplate("Summarise the following in {{language}}: {{text}}")
.WithVariable("language", "French")
.WithVariable("text", someArticle)
.WithStrictTemplates(); // throws TemplateRenderException if a variable is missingbuilder
.AddUserMessage("What is a monad?")
.AddAssistantMessage("A monad is a design pattern…") // few-shot example
.AddUserMessage("Can you give a C# example?");var history = new ChatHistory()
.AddSystemMessage("You are a code reviewer.")
.AddUserMessage("Here is my PR diff: …")
.AddAssistantMessage("Looks good overall, but consider …")
.TrimToTokenLimit(3000);
builder.WithChatHistory(history);PromptResult result = builder.Build();
result.Messages // IReadOnlyList<ChatMessage> — pass to your LLM client
result.FormattedPrompt // single string for logging / completion endpoints
result.EstimatedTokenCount // int — total estimated tokens
result.SystemPrompt // string? — extracted system message
result.TurnCount // non-system message count
result.FitsWithin(4096) // bool
result.TokensRemaining(4096) // int — remaining budgetvar history = new ChatHistory();
history
.AddSystemMessage("You are helpful.")
.AddUserMessage("Hello!")
.AddAssistantMessage("Hi there, how can I help?");
// Trim to a token budget (preserves system messages)
history.TrimToTokenLimit(2048);
// Keep only the last 5 turns
var recent = history.LastNTurns(5);
// Fit into a window without modifying the original
var fitted = history.FitWithin(1500);
// Convenience extension: add a user+assistant pair in one call
history.AddTurn("What is 2 + 2?", "4.");Use standalone when you need template rendering without the full builder.
var tpl = new PromptTemplate("Dear {{name}}, your order {{orderId}} has shipped.");
// Render with a dictionary
string text = tpl.Render(new Dictionary<string, string>
{
["name"] = "Alice",
["orderId"] = "ORD-9982"
});
// Check for missing variables before rendering
IReadOnlyList<string> missing = tpl.GetMissingVariables(myVars);
// Inspect declared placeholders
IReadOnlySet<string> declared = tpl.DeclaredVariables;
// → { "name", "orderid" }ITokenizer tokenizer = new SimpleTokenizer();
int tokens = tokenizer.CountTokens("Hello, world!"); // plain string
int tokens = tokenizer.CountTokens(someMessage); // ChatMessage
int tokens = tokenizer.CountTokens(history.Messages); // message sequenceImplement ITokenizer to plug in a BPE tokenizer (e.g. Microsoft.ML.Tokenizers).
// Program.cs
builder.Services.AddPromptBuilder();
// Inject PromptBuilder into any service
public class SummaryService(PromptBuilder promptBuilder)
{
public PromptResult BuildSummaryPrompt(string article) =>
promptBuilder
.WithSystemPrompt("You are a summarisation expert.")
.WithTemplate("Summarise this article in 3 bullet points:\n\n{{article}}")
.WithVariable("article", article)
.Build();
}Custom tokenizer:
builder.Services.AddPromptBuilder<MyBpeTokenizer>();builder
.WithSystemPrompt("You are a code assistant.")
.When(includeFewShot, b => b
.AddUserMessage("Reverse a string in C#.")
.AddAssistantMessage("return new string(input.Reverse().ToArray());"))
.AddUserMessage(userQuestion)
.Build();SimpleTokenizer uses a word-piece length heuristic calibrated against the OpenAI cl100k_base vocabulary:
| Text type | Typical error |
|---|---|
| English prose | ±5 % |
| Code (C#, Python, JS) | ±10 % |
| Mixed / non-Latin | ±15 % |
For production systems that need exact counts, implement ITokenizer using Microsoft.ML.Tokenizers or the tiktoken-dotnet library.
MIT © 2025 Vladyslav Zaiets