Microsoft Reactor Series · 75–90 min · Intermediate .NET
Bruno Capuano — Principal Cloud Advocate, Microsoft github.com/elbruno · @elbruno
Pablo Nunes Lopes — Cloud Advocate, Microsoft linkedin.com/in/pablonuneslopes
elbruno/openclawnet
Today: chat with a model. Next time: give it tools. Then jobs, MCP, multi-agent.
A working Aspire distributed app with:
IAgentProvider
dot.net/download
dotnet workload install aspire
ollama pull llama3.2
Hardware: 16 GB RAM minimum for local LLMs. 32 GB recommended.
┌──────────────────────────────────────────────┐ │ Blazor Web (chat UI) │ ├──────────────────────────────────────────────┤ │ HTTP NDJSON + Minimal APIs (Gateway) │ ├──────────────────────────────────────────────┤ │ RuntimeAgentProvider (router) │ ├────────┬────────┬────────┬────────┬──────────┤ │ Ollama │ Azure │Foundry │Foundry │ GitHub │ │ │ OpenAI │ │ Local │ Copilot │ ├────────┴────────┴────────┴────────┴──────────┤ │ Storage (EF Core, SQLite) │ └──────────────────────────────────────────────┘
public interface IAgentProvider { string Name { get; } IChatClient CreateChatClient(AgentProfile profile); Task<bool> IsAvailableAsync(CancellationToken ct = default); }
IChatClient
Models.Abstractions
Models.Ollama
Models.AzureOpenAI
Models.GitHubCopilot
Storage
Gateway
Web
AppHost
services.Configure<OllamaOptions>(o => { o.Endpoint = "http://localhost:11434"; o.Model = "llama3.2"; }); services.AddSingleton<IAgentProvider, OllamaAgentProvider>(); var provider = sp.GetRequiredService<IAgentProvider>(); var client = provider.CreateChatClient(profile); await foreach (var update in client.GetStreamingResponseAsync(messages)) Console.Write(update.Text);
The provider picks the right credential based on AzureOpenAIOptions.AuthMode.
AzureOpenAIOptions.AuthMode
services.Configure<GitHubCopilotOptions>(o => { o.Model = "gpt-5-mini"; // or claude-sonnet-4.5, gpt-5, ... }); services.AddSingleton<IAgentProvider, GitHubCopilotAgentProvider>();
Auth: gh auth login (uses host config) or GitHubCopilot:GitHubToken user-secret. Requires an active GitHub Copilot subscription (free tier exists).
gh auth login
GitHubCopilot:GitHubToken
We migrated from ChatHub (SignalR) to POST /api/chat/stream returning application/x-ndjson.
ChatHub
application/x-ndjson
Why?
HttpClient
group.MapPost("/api/chat/stream", async ( ChatStreamRequest req, IAgentRuntime runtime, HttpContext ctx) => { ctx.Response.ContentType = "application/x-ndjson"; await foreach (var ev in runtime.ExecuteStreamAsync(ctx)) { var line = JsonSerializer.Serialize(ev) + "\n"; await ctx.Response.WriteAsync(line); await ctx.Response.Body.FlushAsync(); } });
using var resp = await Http.PostAsJsonAsync( "/api/chat/stream", request, HttpCompletionOption.ResponseHeadersRead); using var stream = await resp.Content.ReadAsStreamAsync(); using var reader = new StreamReader(stream); while (!reader.EndOfStream) { var line = await reader.ReadLineAsync(); var ev = JsonSerializer.Deserialize<StreamEvent>(line!); AppendToken(ev.Delta); // re-renders Blazor cell StateHasChanged(); }
ChatSession
ChatMessageEntity
AgentProfile
ScheduledJob
JobRun
JobRunEvent
We use EnsureCreatedAsync + a hand-written SchemaMigrator:
EnsureCreatedAsync
SchemaMigrator
await db.Database.EnsureCreatedAsync(); await SchemaMigrator.UpgradeAsync(db);
Reasons:
aspire start
$env:NUGET_PACKAGES = "$env:USERPROFILE\.nuget\packages2" aspire start src\OpenClawNet.AppHost
Then:
code/demo1
elbruno/openclawnet · MIT licensed · contributions welcome