May 24, 20264 min read/2026/05/24/getting-started-github-copilot-sdk-part-10-blazor-chat-app/

Getting Started with the GitHub Copilot SDK — Part 10: A Full-Stack Blazor Chat App

This is Part 10, and it's the payoff. Up to now we've been poking at pieces in
isolation. Now we wire them all together into a real app.

A browser chat UI → the Copilot SDK → AI tools that query a live database.

The companion code is on GitHub at
egarim/GettingStartedWithGithubCopilotSDK.
This demo is a full-stack Blazor Server chat app backed by Northwind data.

The shape of it

Three layers, top to bottom:

  • Blazor UI — a chat page that talks to an IChatClient.
  • The adapter — a CopilotChatClient that is an IChatClient, wrapping the SDK.
  • The data + tools — six AI functions that run real EF Core queries.

The clever bit: the UI never touches the SDK directly. It only knows
Microsoft.Extensions.AI. Everything Copilot-specific hides behind one interface.

One client, the whole app

CopilotChatService is a singleton. It owns a single CopilotClient, starts it
lazily on first ask, and guards that start with a lock so concurrent requests don't
race:

private async Task EnsureStartedAsync()
{
    if (_started) return;
    await _startLock.WaitAsync();
    try
    {
        if (_started) return;
        await _client.StartAsync();
        _started = true;
    }
    finally { _startLock.Release(); }
}

Each ask spins up a fresh session, subscribes to the event stream, and buffers the
AssistantMessageDeltaEvent deltas until a SessionIdleEvent says we're done:

var subscription = session.On(evt =>
{
    switch (evt)
    {
        case AssistantMessageDeltaEvent delta:
            buffer.Append(delta.Data.DeltaContent);
            break;
        case SessionErrorEvent error:
            lastError = error.Data?.Message;
            idleTcs.TrySetResult(false);
            break;
        case SessionIdleEvent:
            idleTcs.TrySetResult(true);
            break;
    }
});

If you read Part 1 through Part 4, this is the same lifecycle and event loop —
just packaged for a web app.

The IChatClient adapter

This is the seam that keeps the UI clean. CopilotChatClient implements
IChatClient, pulls the last user message, and forwards it to the service:

public async Task<ChatResponse> GetResponseAsync(
    IEnumerable<ChatMessage> chatMessages,
    ChatOptions? options = null,
    CancellationToken cancellationToken = default)
{
    var prompt = chatMessages.LastOrDefault(m => m.Role == ChatRole.User)?.Text ?? "";
    var response = await _service.AskAsync(prompt, cancellationToken);
    return new ChatResponse(new ChatMessage(ChatRole.Assistant, response));
}

Now the Blazor page can inject IChatClient and stay blissfully unaware of GitHub Copilot.

Six tools over EF Core

The model doesn't hallucinate your orders — it calls functions.
NorthwindToolsProvider builds six AIFunctions with AIFunctionFactory.Create:

AIFunctionFactory.Create(QueryOrders, "query_orders"),
AIFunctionFactory.Create(InvoiceAging, "invoice_aging"),
AIFunctionFactory.Create(LowStockProducts, "low_stock_products"),
AIFunctionFactory.Create(EmployeeOrderStats, "employee_order_stats"),
AIFunctionFactory.Create(EmployeeTerritories, "employee_territories"),
AIFunctionFactory.Create(CreateOrder, "create_order"),

Each one is a plain method, decorated with [Description] so the model knows when to
reach for it, and it opens its own DbContext from a factory — thread-safe by
construction:

[Description("Search orders by customer name and/or status.")]
private string QueryOrders(
    [Description("Customer company name (partial match).")] string customerName = "",
    [Description("Order status filter.")] string status = "")
{
    using var db = _dbFactory.CreateDbContext();
    // Include + filter + project to formatted text
}

Note create_order actually writes — it validates the customer, product and
shipper, then SaveChanges(). The AI can mutate your data, so the descriptions and
validation matter.

Tying it together in DI

AddCopilotSdk registers the singletons and wires the chat client, handing it the tools
and the system prompt:

services.AddChatClient(sp =>
{
    var service = sp.GetRequiredService<CopilotChatService>();
    service.Tools = sp.GetRequiredService<NorthwindToolsProvider>().Tools;
    service.SystemMessage = CopilotChatDefaults.SystemPrompt;
    return new CopilotChatClient(service);
});

That system prompt is doing heavy lifting — it spells out the whole Northwind schema
and seed data so the model knows what tables, statuses, and customers exist.

The browser side

Chat.razor injects IChatClient, keeps a list of in-memory sessions, and sends:

@inject IChatClient ChatClient

var response = await ChatClient.GetResponseAsync(chatMessages);
var content  = response.Messages.LastOrDefault()?.Text ?? "No response.";
var html     = CopilotChatDefaults.ConvertMarkdownToHtml(content);

The model replies in markdown, so we run it through Markdig (pipe tables, emphasis,
autolinks) and then HtmlSanitizer before dropping it into a MarkupString. Tables
and bold survive; injected scripts don't.

The UI itself is composed of small components — ChatHeader, ChatSuggestions,
ChatMessageList, ChatInput, plus a SessionSidebar — and prompt-suggestion cards
to get users started.

Takeaways

  • Hide the SDK behind IChatClient. The UI depends on an interface, not on Copilot.
  • One singleton client, lazily started, lock-guarded. A session per request.
  • Tools are just [Description]-decorated methods + a DbContext factory for safe DB
    access. They can read and write.
  • The system prompt is your schema contract — describe the data so the model knows
    what it's working with.
  • Sanitize markdown before rendering anything the model produced.

That's a complete, runnable AI app — UI, SDK, tools, and a database — in one project.

What's next? In Part 11 — Bring Your Own Key (BYOK), we swap the auth model and point
the SDK at OpenRouter so you can run it on your own key:
/2026/05/28/getting-started-github-copilot-sdk-part-11-byok-openrouter/.

Want to run it? You'll need the .NET 10 SDK and GitHub Copilot access. Then
dotnet run --project Course/demos/10.BlazorChatDemo and open
https://localhost:5101 — the SQLite database seeds itself on first launch.