C# & .NET (C# 14, .NET 10 LTS) Module 20 – Command-line apps, hosting, DI, configuration and telemetry
Microsoft.Extensions.AI: chat clients in C#
Call chat models from C# through IChatClient, wrap them in middleware, validate structured output, compare embeddings and test it all with fakes.
What you will learn
- Call a chat model through IChatClient with messages, roles and options, and read its response
- Compose middleware around a chat client with ChatClientBuilder and DelegatingChatClient
- Validate structured output against an allowlist and ranges before the app acts on it
- Compare texts with IEmbeddingGenerator and cosine similarity
- Test code that depends on a chat model with a fake IChatClient, without a network or a key
Before you start
What you’ll learn · Before you start
What you will learn
- Call a chat model through IChatClient with messages, roles and options, and read its response
- Compose middleware around a chat client with ChatClientBuilder and DelegatingChatClient
- Validate structured output against an allowlist and ranges before the app acts on it
- Compare texts with IEmbeddingGenerator and cosine similarity
- Test code that depends on a chat model with a fake IChatClient, without a network or a key
Before you start
On this page
A support team wants every new ticket summarised and sorted before a person reads it, and a chat model can do both. Calling one is usually a web request; the hard parts are around it: which service, which settings, what to log, what to send, and whether the answer can be trusted. Microsoft.Extensions.AI gives .NET one set of types for all of it, so the code that uses a model does not depend on whose model it is.
You will call a chat client through its interface, wrap it in middleware, turn its answers into checked .NET values, compare texts with embeddings, and test every piece without a network, a key or a model.
IChatClient is one interface for chat models
The Microsoft.Extensions.AI.Abstractions package defines IChatClient: The Microsoft.Extensions.AI interface for chat models: GetResponseAsync sends a list of messages with options and returns a ChatResponse. Provider packages implement it, so app code and tests do not depend on one service., and packages for
chat services implement it; the Microsoft.Extensions.AI package adds middleware and helpers on top. A request is a list
of chat messages, each with a role: the system message holds the instructions and a user message holds the customer’s
words. The service in summarise.cs takes an IChatClient in its constructor, like any other dependency, and the
program registers a fake client that records what it was sent:
#:package Microsoft.Extensions.AI@10.10.0
#:package Microsoft.Extensions.Hosting@10.0.12
#:property PublishAot=false
// A ticket summariser that depends only on IChatClient. An app would register the IChatClient of a provider's package;
// this program registers FakeChatClient, which records what it is sent and answers with a fixed sentence, so the run
// needs no network, no key and no model, and prints the same lines every time.
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Logging.ClearProviders();
FakeChatClient fake = new("Charged twice for order A-1042; wants one payment refunded.");
builder.Services.AddChatClient(fake);
builder.Services.AddSingleton<TicketSummarizer>();
using IHost host = builder.Build();
TicketSummarizer summarizer = host.Services.GetRequiredService<TicketSummarizer>();
string summary = await summarizer.SummarizeAsync(
"Hello, I paid for order A-1042 on Monday and the money left my account twice. Please refund one payment.");
Console.WriteLine("the chat client was sent:");
foreach (ChatMessage message in fake.Received)
Console.WriteLine($" {message.Role}: {message.Text}");
Console.WriteLine($"with Temperature {fake.Options?.Temperature} and MaxOutputTokens {fake.Options?.MaxOutputTokens}");
Console.WriteLine($"summary: {summary}");
// The service: it builds the messages, sets the options and reads the text of the answer. It never names a model.
sealed class TicketSummarizer(IChatClient chat)
{
public async Task<string> SummarizeAsync(string ticket, CancellationToken cancellationToken = default)
{
ChatResponse response = await chat.GetResponseAsync(
[
new ChatMessage(ChatRole.System, "Summarise the support ticket in one sentence. Keep order numbers."),
new ChatMessage(ChatRole.User, ticket),
],
new ChatOptions { Temperature = 0, MaxOutputTokens = 60 },
cancellationToken);
return response.Text;
}
}
// A stand-in for a real client: it keeps the last request and returns its fixed answer.
sealed class FakeChatClient(string answer) : IChatClient
{
public List<ChatMessage> Received { get; } = [];
public ChatOptions? Options { get; private set; }
public Task<ChatResponse> GetResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options = null, CancellationToken cancellationToken = default)
{
Received.Clear();
Received.AddRange(messages);
Options = options;
return Task.FromResult(new ChatResponse(new ChatMessage(ChatRole.Assistant, answer)));
}
public IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? options = null, CancellationToken cancellationToken = default) =>
throw new NotSupportedException("this fake only answers whole responses");
public object? GetService(Type serviceType, object? serviceKey = null) => serviceType.IsInstanceOfType(this) ? this : null;
public void Dispose() { }
} from session import run
# network: true. summarise.cs names Microsoft.Extensions.AI 10.10.0 and Microsoft.Extensions.Hosting 10.0.12 in its
# #:package lines, and dotnet run restores them from nuget.org into the session's own home folder. The chat client is
# a fake inside the program, so nothing else is reached: no model, no key.
run("""
cd summarise
dotnet run summarise.cs
""", network=True) Output
$ cd summarise $ dotnet run summarise.cs the chat client was sent: system: Summarise the support ticket in one sentence. Keep order numbers. user: Hello, I paid for order A-1042 on Monday and the money left my account twice. Please refund one payment. with Temperature 0 and MaxOutputTokens 60 summary: Charged twice for order A-1042; wants one payment refunded.
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 summarise_session.py
The fake received exactly what the service built: the system message, the ticket as a user message, and the options,
a temperature of 0 and at most 60 output tokens. The service read the answer through response.Text and never named a
model, a service or a key, so the same class runs against a real client in production and against the fake in its
tests. Because the fake answers at once and the same way every time, a test that uses it is fast, repeatable and costs
nothing. Step through the request:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- Two messages. The service builds a system message with the instructions and a user message with the ticket. The roles tell a model which text is an instruction and which is the customer's.
- Options for this call. Temperature 0 and MaxOutputTokens 60 travel in ChatOptions, the settings most chat services share; each client maps them to its own service.
- One call through the interface. GetResponseAsync goes to the IChatClient that AddChatClient registered. The service never names a model or a service.
- A real client or a fake. In production the registered client comes from a provider's package. Here it is FakeChatClient, which keeps the messages and options it receives.
- The answer. The fake returns a ChatResponse with one assistant message, and response.Text is that message's text.
- What the run printed. The program printed both messages and both options as the client received them, then the summary. Every run prints the same lines, because nothing random or remote takes part.
The same interface carries the rest of this lesson. The builder ChatClientBuilder wraps a client in middleware
layers, such as logging, telemetry, caching or code of your own. The extension GetResponseAsync<T> asks for
structured output: A model answer requested in a fixed JSON shape, here derived from a .NET type, so the app can parse it into that type. A parsed answer is still untrusted input and is validated before use., JSON that fits a .NET type, and parses the answer into it. An
allowlist check accepts only the values the app knows and sends anything else to a person. A
prompt injection: Text in a model's input, such as a ticket, an email or a web page, that tries to change what the model was told to do. Defences treat such text as data, limit what the model can trigger and check every answer. is text in the input, such as a ticket, that tries to change the
instructions. The interface IEmbeddingGenerator turns texts into vectors that can be compared by meaning, usually
with cosine similarity: How closely two vectors point the same way: their dot product divided by the product of their lengths. It is 1 for the same direction and 0 when they share nothing..
Checkpoint
Check: IChatClient, messages and fake clients
Middleware wraps a client in layers Pro: opens with a Pro, Premium or Ultimate pass
In this part:
- Structured output is untrusted input
- Embeddings compare texts by meaning
- Practise: a ticket classifier tested with fakes
- Interview questions
- Your support tool asks a chat model to pick an action for each ticket and runs it. A customer's ticket text got their refund approved automatically. How did it happen, and how do you redesign the tool so that ticket text can never trigger an action on its own?
- The tests of your AI feature call the real chat service: they take minutes, fail now and then with different answers, need a key on every build machine and cost money on each run. How do you test the feature instead?
- Your app calls one model service's SDK in forty places. You must move to another service, and add caching, telemetry and redaction of personal data along the way, without a rewrite. How do you plan it?
- Key takeaways
2 diagrams · 8 runnable examples (C#, Python) · 3 interview answers · about 26 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Terms in this lesson
- cosine similarity
- How closely two vectors point the same way: their dot product divided by the product of their lengths. It is 1 for the same direction and 0 when they share nothing.
- embedding
- A vector of numbers computed for a piece of text, placed so that texts with similar meaning get vectors that point in similar directions. Embeddings are compared with a measure such as cosine similarity.
- IChatClient
- The Microsoft.Extensions.AI interface for chat models: GetResponseAsync sends a list of messages with options and returns a ChatResponse. Provider packages implement it, so app code and tests do not depend on one service.
- prompt injection
- Text in a model's input, such as a ticket, an email or a web page, that tries to change what the model was told to do. Defences treat such text as data, limit what the model can trigger and check every answer.
- structured output
- A model answer requested in a fixed JSON shape, here derived from a .NET type, so the app can parse it into that type. A parsed answer is still untrusted input and is validated before use.
References
- Microsoft.Extensions.AI libraries (Microsoft Learn)
- Use the IChatClient interface (Microsoft Learn)
- Use the IEmbeddingGenerator interface (Microsoft Learn)
- ChatClientStructuredOutputExtensions.GetResponseAsync Method (Microsoft Learn)
- LoggingChatClient Class (Microsoft Learn)
- Microsoft.Extensions.AI package (NuGet Gallery (.NET Foundation))
- Prompt Injection (OWASP Top 10 for LLM Applications) (OWASP GenAI Security Project)
- Sensitive Information Disclosure (OWASP Top 10 for LLM Applications) (OWASP GenAI Security Project)
- Improper Output Handling (OWASP Top 10 for LLM Applications) (OWASP GenAI Security Project)
- Vector and Embedding Weaknesses (OWASP Top 10 for LLM Applications) (OWASP GenAI Security Project)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress