← back to knowledge-hub

Build an AI Chat App with .NET and OpenAI

Most “call an LLM from your app” tutorials hand you a vendor SDK and call it a day. It works — until the day you want to swap models or add a second provider, and suddenly you’re rewriting plumbing.

This Microsoft quickstart takes a different route: a small .NET console chat app built on Microsoft.Extensions.AI, the abstraction layer that sits above OpenAI, Azure OpenAI, Ollama and the rest. You write against IChatClient once; the model behind it becomes a registration detail.

Here’s the whole thing, OpenAI flavour.

What you need

That’s it. No Azure subscription for this path.

Scaffold the app

Create a console project and drop into it:

1
2
dotnet new console -o ChatAppAI
cd ChatAppAI

Add the packages. The star is Microsoft.Extensions.AI.OpenAI — the bridge between the abstractions and the OpenAI SDK. It’s prerelease, so the flag is required:

1
2
3
4
dotnet add package OpenAI
dotnet add package Microsoft.Extensions.AI.OpenAI --prerelease
dotnet add package Microsoft.Extensions.Configuration
dotnet add package Microsoft.Extensions.Configuration.UserSecrets

Keep the key out of your code

Never hardcode an API key. .NET’s user-secrets store keeps it off disk-in-your-repo and out of source control:

1
2
3
dotnet user-secrets init
dotnet user-secrets set OpenAIKey <your-OpenAI-key>
dotnet user-secrets set ModelName <your-OpenAI-model-name>

ModelName is whatever model you’re entitled to — gpt-4o-mini, gpt-4o, and so on.

Wire up the client

In Program.cs, read the secrets and build an IChatClient. Note the shape: you create an OpenAIClient, ask it for a chat client, then call .AsIChatClient() to hand yourself back the abstraction. Everything after this line is provider-agnostic.

1
2
3
4
5
6
7
var config = new ConfigurationBuilder().AddUserSecrets<Program>().Build();
string model = config["ModelName"];
string key = config["OpenAIKey"];

// Create the IChatClient
IChatClient chatClient =
    new OpenAIClient(key).GetChatClient(model).AsIChatClient();

Give the model a personality

The app plays a hiking guide. A system message sets the role and the rules up front — what to ask, what to return, how to close. This goes in first as the seed of the conversation history:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// Start the conversation with context for the AI model
List<ChatMessage> chatHistory =
    [
        new ChatMessage(ChatRole.System, """
            You are a friendly hiking enthusiast who helps people discover fun hikes in their area.
            You introduce yourself when first saying hello.
            When helping people out, you always ask them for this information
            to inform the hiking recommendation you provide:

            1. The location where they would like to hike
            2. What hiking intensity they are looking for

            You will then provide three suggestions for nearby hikes that vary in length
            after you get that information. You will also share an interesting fact about
            the local nature on the hikes when making a recommendation. At the end of your
            response, ask if there is anything else you can help with.
        """)
    ];

The conversation loop

Here’s the part that makes it feel like a chat rather than a one-shot prompt: a List<ChatMessage> that grows. Every user turn and every assistant reply gets appended, and the whole list is sent on each request. That accumulating history is the app’s memory — the model has no state of its own between calls.

Responses stream token-by-token via GetStreamingResponseAsync, so text appears as it’s generated instead of after a pause:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// Loop to get user input and stream AI response
while (true)
{
    // Get user prompt and add to chat history
    Console.WriteLine("Your prompt:");
    string? userPrompt = Console.ReadLine();
    chatHistory.Add(new ChatMessage(ChatRole.User, userPrompt));

    // Stream the AI response and add to chat history
    Console.WriteLine("AI Response:");
    string response = "";
    await foreach (ChatResponseUpdate item in
        chatClient.GetStreamingResponseAsync(chatHistory))
    {
        Console.Write(item.Text);
        response += item.Text;
    }
    chatHistory.Add(new ChatMessage(ChatRole.Assistant, response));
    Console.WriteLine();
}

Run it

1
dotnet run

Type a prompt, watch the reply stream in, ask a follow-up. Because history is preserved, the guide remembers your location and intensity across turns — the conversation actually builds.

Why the abstraction matters

Nothing here is exotic. The payoff is the seam: swap to Azure OpenAI and only the client-construction line changes — from new OpenAIClient(key)... to new AzureOpenAIClient(endpoint, credential).... The system prompt, the history list, the streaming loop all stay identical.

That’s the whole pitch of Microsoft.Extensions.AI: your app talks to IChatClient, and the model market underneath can shift as often as it likes. The next provider is a one-line change, not a rewrite.

If you want the layer of the picture this sits in, see my note on the core .NET AI building blocks.

Based on Microsoft’s Build an AI chat app with .NET quickstart.

graph cloud