You don't need a vector database, an agent framework or a weekend to ship your first AI feature in .NET. You need one interface, IChatClient, and about fifteen minutes.
In this post we'll build a small but real feature: a support ticket triage endpoint. You POST a customer email, and you get back a category, a priority and a one-line summary as a typed C# object. Then we'll stream a reply draft and make the model swappable with one line.
📄 Want the full version of every snippet here? Grab the free .NET AI Starter Cheat Sheet. Enter your email and get it free: 8 pages of copy-paste C# covering the whole .NET AI stack, tool calling, structured output, RAG, MCP and a production checklist.
🔥 Tech Skill Builder is $5/month for a very limited time only (normally $10). Grab it now before it goes back to $10. Complete, tested .NET + AI projects with full source and step-by-step PDF guides: Join Tech Skill Builder
The plan (and the clock)
| Minutes | Step |
|---|---|
| 0 to 2 | Create the project and add two packages |
| 2 to 5 | Register IChatClient once, in DI |
| 5 to 9 | Typed triage with structured output |
| 9 to 12 | Stream a reply draft |
| 12 to 15 | Guardrails, and swapping the model |
The code below shows the shape of each step. Some parts are trimmed and marked as such.
Minute 0 to 2: project and packages
dotnet new webapi -n TicketTriage --use-minimal-apis
cd TicketTriage
dotnet add package Microsoft.Extensions.AI
dotnet add package Microsoft.Extensions.AI.OpenAI
dotnet user-secrets init
dotnet user-secrets set "OpenAI:Key" "<your-key>"
Microsoft.Extensions.AI gives you the abstractions (IChatClient, ChatMessage, ChatOptions) and the middleware. Microsoft.Extensions.AI.OpenAI is the adapter that turns the OpenAI SDK client into an IChatClient. Your app code will only ever see the interface.
Keep the key in user secrets (or environment variables in production). Never in appsettings.json, and never in Git.
Minute 2 to 5: one registration, used everywhere
The mistake I see most often in first AI features is new OpenAIClient(...) inside a controller. Register the client once and inject the interface instead:
using Microsoft.Extensions.AI;
using OpenAI;
builder.Services
.AddChatClient(sp => new OpenAIClient(builder.Configuration["OpenAI:Key"])
.GetChatClient("gpt-4o-mini")
.AsIChatClient())
.UseLogging();
// more middleware (telemetry, caching, tool calls): see the cheat sheet
That builder is a pipeline. Every call goes through the middleware you add, in order, before it reaches the model. Logging is enough for today.
Minute 5 to 9: typed triage, not string parsing
Here's the part that makes it feel like a real feature. Don't ask the model for "JSON please" and then fight JsonSerializer. Ask for a C# type:
public enum Priority { Low, Normal, High, Urgent }
public record Triage(string Category, Priority Priority, string Summary);
public record TicketRequest(string Body);
app.MapPost("/tickets/triage", async (IChatClient ai, TicketRequest req) =>
{
ChatResponse<Triage> res = await ai.GetResponseAsync<Triage>(
[
new(ChatRole.System, "You triage support emails for an online shop. ..."),
new(ChatRole.User, req.Body)
]);
// ... validate res.Result before trusting it (step 5)
return Results.Ok(res.Result);
});
GetResponseAsync<T> generates a JSON schema from your record, sends it with the request and deserializes the answer into Triage. Using an enum for Priority means the model can't invent a "Kinda urgent" level.
The system prompt matters more than the code here. Mine lists the allowed categories and says what counts as urgent. I've cut it short above because the wording is where most of the tuning goes.
Minute 9 to 12: stream the reply draft
A triage result is small, so waiting for it is fine. A reply draft is longer, and a spinner for six seconds feels broken. Stream it:
app.MapPost("/tickets/draft", (IChatClient ai, TicketRequest req) =>
StreamDraft(ai, req));
static async IAsyncEnumerable<string> StreamDraft(IChatClient ai, TicketRequest req)
{
await foreach (var update in ai.GetStreamingResponseAsync(/* system + user messages */))
yield return update.Text;
}
Returning IAsyncEnumerable<string> from a minimal API endpoint sends the chunks as they arrive. The front end shows the text as the model writes it. Cancellation, retries and Server-Sent Events are the next layer up, and you can skip them for a first version.
Minute 12 to 15: guardrails and the one-line swap
Three things I'd add before anyone else uses this:
-
Cap the cost. Pass
new ChatOptions { MaxOutputTokens = 300, Temperature = 0.1f }for triage. Low temperature for classification, a little higher for drafts. -
Don't trust the result blindly. If
Summaryis empty or the category isn't in your list, fall back to "Needs a human". The model will be wrong sometimes, and your code should expect that. - Cap the input. Truncate the email body before sending it. A 40-page pasted log shouldn't cost you a dollar.
And the payoff for coding against the interface: switching to a local model for development is one registration change.
// dev: a local model through Ollama (OllamaSharp implements IChatClient)
builder.Services.AddChatClient(new OllamaApiClient(new Uri("http://localhost:11434"), "llama3.2"));
Your endpoints, prompts and records don't change at all.
What we skipped on purpose
To keep it to fifteen minutes I left out tool calling (letting the model look up the customer's order), RAG (answering from your own help-center docs), telemetry and evals. Those are the steps that turn a demo into something you'd put in front of customers, and each one has a few traps.
Get the full version, free
Every snippet in this post, completed, plus tool calling, RAG in six steps, an MCP server in C# and a production checklist, is in the free .NET AI Starter Cheat Sheet. Enter your email and get it free. 📄
When you're ready for complete projects instead of snippets, Tech Skill Builder gives you working, tested .NET + AI solutions with full source and step-by-step PDF guides. Tech Skill Builder is $5/month for a very limited time only (normally $10). Grab it now before it goes back to $10. Join Tech Skill Builder
What was the first AI feature you shipped in .NET, or the one you're planning? Tell me in the comments.
Top comments (0)