In this lesson, you'll learn the fundamentals of interacting with AI models in .NET. We'll start with the simplest interaction—text completions—and build up to full conversational applications.
Click the image to watch the video
In the previous lesson, you learned that calling an AI model is just like calling an API. Let's prove it.
By the end of this section, you'll build a chat application that:
- Remembers what you said earlier in the conversation
- Follows instructions you give it (like "be brief" or "respond in Spanish")
- Maintains full conversational context
And here's the best part: the core pattern fits in about 15 lines of code.
// A complete chat application with memory
List<ChatMessage> conversation = new()
{
new ChatMessage(ChatRole.System, "You are a helpful assistant.")
};
while (true)
{
Console.Write("You: ");
conversation.Add(new ChatMessage(ChatRole.User, Console.ReadLine()!));
var response = await chatClient.GetResponseAsync(conversation);
conversation.Add(new ChatMessage(ChatRole.Assistant, response.Text));
Console.WriteLine($"AI: {response.Text}");
}That's a working conversational AI. Let's break down how it works.
Before we build conversations, let's start with the simplest possible interaction: a single prompt and response.
A text completion is a one-shot interaction. You send a prompt, you get a response. Done.
Think of it like asking someone a question in passing. You're not starting a conversation, just getting a quick answer.
Best for:
- Summarization
- Sentiment analysis
- Classification
- One-time transformations
Here's how to get a text completion with Microsoft.Extensions.AI:
IChatClient client = new AzureOpenAIClient(
new Uri(config["endpoint"]),
new ApiKeyCredential(config["apikey"]))
.GetChatClient("gpt-5-mini")
.AsIChatClient();
var response = await client.GetResponseAsync(
"Summarize the benefits of cloud computing in one sentence.");
Console.WriteLine(response.Text);What's happening here:
| Line | What It Does |
|---|---|
AzureOpenAIClient |
Creates a connection to Azure OpenAI / Microsoft Foundry |
.GetChatClient("gpt-5-mini") |
Selects the deployment model |
.AsIChatClient() |
Wraps it in the standard IChatClient interface |
GetResponseAsync(prompt) |
Sends your prompt, waits for the response |
response.Text |
The AI's generated text |
Learn more: IChatClient interface provides a unified abstraction for any AI provider.
Try it yourself: BasicChat-01MEAI sample
To run the sample code, you'll need to:
-
Make sure you have set up your development environment as described in the Setup guide
-
Ensure you have configured your GitHub Token or other credentials
-
Open a terminal in your IDE
-
Navigate to the sample code directory:
cd samples/CoreSamples/BasicChat-01MEAI -
Run the application:
dotnet run app.cs
Let's do something useful. Analyze the sentiment of customer reviews:
StringBuilder prompt = new();
prompt.AppendLine("Analyze the sentiment of each review. Output: Review number, Sentiment (Positive/Negative/Neutral), Key reason.");
prompt.AppendLine();
prompt.AppendLine("Review 1: I bought this product and it's amazing. I love it!");
prompt.AppendLine("Review 2: This product is terrible. I hate it.");
prompt.AppendLine("Review 3: I'm not sure about this product. It's okay.");
var response = await client.GetResponseAsync(prompt.ToString());
Console.WriteLine(response.Text);Expected output:
Review 1: Positive - Strong enthusiasm ("amazing", "love it")
Review 2: Negative - Strong dissatisfaction ("terrible", "hate it")
Review 3: Neutral - Uncertainty and lukewarm response ("not sure", "okay")
Notice how we didn't write any sentiment analysis code. We described what we wanted, and the model did the rest.
Text completions are useful, but most AI applications need conversations. The model needs to remember what was said before.
By default, each call to an AI model is independent. The model doesn't remember previous calls.
// First call
await client.GetResponseAsync("My name is Bruno.");
// Response: "Nice to meet you, Bruno!"
// Second call
await client.GetResponseAsync("What's my name?");
// Response: "I don't know your name. You haven't told me."The second call has no idea about the first one.
To create a conversation, you maintain a list of messages and send the entire history with each request:
List<ChatMessage> conversation = new();
// Add user's first message
conversation.Add(new ChatMessage(ChatRole.User, "My name is Bruno."));
// Get response and add it to history
var response1 = await client.GetResponseAsync(conversation);
conversation.Add(new ChatMessage(ChatRole.Assistant, response1.Text));
// Now ask the follow-up question
conversation.Add(new ChatMessage(ChatRole.User, "What's my name?"));
var response2 = await client.GetResponseAsync(conversation);
// Response: "Your name is Bruno."Now the model "remembers" because you sent the entire conversation each time.
Here's what's happening:
Request 1: [User: "My name is Bruno."]
Response 1: "Nice to meet you, Bruno!"
Request 2: [User: "My name is Bruno.", Assistant: "Nice to meet you, Bruno!", User: "What's my name?"]
Response 2: "Your name is Bruno."
The history grows with each exchange. The model sees the full context every time.
The Microsoft.Extensions.AI library provides a convenient helper method to add response messages to your history:
List<ChatMessage> history = [];
while (true)
{
Console.Write("Q: ");
history.Add(new(ChatRole.User, Console.ReadLine()));
ChatResponse response = await client.GetResponseAsync(history);
Console.WriteLine(response);
history.AddMessages(response); // Adds all messages from the response
}The AddMessages extension method extracts messages from the ChatResponse and adds them to your history list.
Every message in a conversation has a role. Understanding these roles is key to building effective AI applications.
| Role | Who Creates It | Purpose |
|---|---|---|
| System | You (the developer) | Sets the AI's behavior, personality, and constraints |
| User | The end user | Questions, commands, or input |
| Assistant | The AI model | Responses generated by the model |
The system message is the most powerful tool for controlling AI behavior. It's the first message in the conversation and sets the rules.
List<ChatMessage> conversation = new()
{
new ChatMessage(ChatRole.System,
"You are a senior .NET developer. Answer questions about C# and .NET. " +
"Keep responses concise. Use code examples when helpful. " +
"If asked about other topics, politely redirect to .NET topics.")
};What this system message does:
- Sets expertise: "senior .NET developer"
- Defines scope: "C# and .NET"
- Sets style: "concise", "code examples"
- Sets boundaries: redirect non-.NET questions
Here are common patterns for system messages:
The Expert:
"You are an expert in [domain]. Your role is to [specific task]."The Persona:
"You are a friendly customer service agent named Alex. Be helpful but brief."The Constrained Assistant:
"Answer only questions about [topic]. For other topics, say 'I can only help with [topic].'"The Format Controller:
"Respond only in JSON format with these fields: { 'answer': string, 'confidence': number }"The system message runs once at the start, but influences every response. It's your main control mechanism.
Try it yourself: BasicChat-10ConversationHistory sample
Let's put it all together. Here's a complete, working chat application:
using Microsoft.Extensions.AI;
// Create the AI client (using Azure OpenAI)
IChatClient client = new AzureOpenAIClient(
new Uri(config["endpoint"]),
new ApiKeyCredential(config["apikey"]))
.GetChatClient("gpt-5-mini")
.AsIChatClient();
// Initialize conversation with a system message
List<ChatMessage> conversation = new()
{
new ChatMessage(ChatRole.System,
"You are a helpful assistant. Be concise but friendly.")
};
Console.WriteLine("Chat started. Type 'quit' to exit.\n");
while (true)
{
Console.Write("You: ");
var userInput = Console.ReadLine();
if (string.IsNullOrEmpty(userInput) || userInput.ToLower() == "quit")
break;
// Add user message to history
conversation.Add(new ChatMessage(ChatRole.User, userInput));
// Get AI response
var response = await client.GetResponseAsync(conversation);
// Add AI response to history
conversation.AddMessages(response);
Console.WriteLine($"AI: {response.Text}\n");
}Key points:
- The
conversationlist persists across the loop - Every user message is added before the API call
- Every AI response is added after using
AddMessages - The system message shapes all responses
The same code works with any IChatClient provider. Just change the client instantiation:
// Option 1: Azure OpenAI / Microsoft Foundry
IChatClient client = new AzureOpenAIClient(
new Uri(config["endpoint"]),
new ApiKeyCredential(config["apikey"]))
.GetChatClient("gpt-5-mini")
.AsIChatClient();
// Option 2: Ollama (local)
IChatClient client = new OllamaApiClient(
new Uri("http://localhost:11434"), "phi4-mini");Your conversation logic stays exactly the same.
| Concept | Key Takeaway |
|---|---|
| Text Completions | Single prompt, single response. Good for one-off tasks. |
| Chat Conversations | Maintain a message list to give the AI memory. |
| System Role | Controls AI behavior, personality, and constraints. Set once, affects all responses. |
| User/Assistant Roles | User provides input, Assistant provides responses. Both stored in history. |
Can you answer these questions?
- Why does the AI "forget" between calls if you don't maintain a conversation list?
- What's the purpose of the System role, and when is it set?
- How does
AddMessageshelp manage conversation history?
| Sample | Description |
|---|---|
| BasicChat-01MEAI | Text completion with Azure OpenAI |
| BasicChat-03Ollama | Chat with local Ollama |
| BasicChat-10ConversationHistory | Managing conversation history |
- IChatClient Interface Guide: Complete reference for the unified chat abstraction
- Build an AI Chat App with .NET: Step-by-step quickstart tutorial
- Chat message roles: Understanding System, User, and Assistant roles
Now that you understand the basics of text completions and conversations, let's explore more advanced capabilities: