Chi sviluppa applicazioni AI in .NET lo sa bene: ogni volta che si integra un LLM in un progetto Blazor, si finisce a riscrivere lo stesso boilerplate per connettere OpenAI, Azure OpenAI, Anthropic o Google. SimpleChat è un template open-source che risolve esattamente questo problema — un’applicazione Blazor Server + .NET 10 Aspire che gestisce quattro provider AI in modo intercambiabile, esponendo un unico IChatClient all’intera applicazione.
Il progetto è disponibile su GitHub: github.com/ADefWebserver/SimpleChat
Perché esiste SimpleChat
Il problema che SimpleChat risolve è concreto: ogni provider AI ha il proprio contratto REST, le proprie intestazioni e le proprie particolarità. OpenAI usa una API key e un base URL. Azure OpenAI richiede un endpoint, un deployment name e una versione API. Anthropic ha il proprio formato di richiesta e rifiuta il parametro temperature sui modelli Claude 4 con ragionamento. Google AI (Gemini) ha ancora un’altra struttura REST sotto generativelanguage.googleapis.com.
SimpleChat risolve tutto questo costruendo su Microsoft.Extensions.AI e centralizzando la logica di creazione del client in una sola classe: ChatClientFactory. Tutto il codice upstream non deve mai sapere quale provider è attivo.
Architettura del progetto
La soluzione Aspire è composta da tre progetti:
- SimpleChat — l’app web Blazor Server
- SimpleChat.AppHost — l’orchestratore Aspire
- SimpleChat.ServiceDefaults — OpenTelemetry condiviso, health check e resilienza
I componenti principali dell’applicazione sono:
AIConfigurationService— legge la sezioneAIdella configurazione e scrive le modifiche dell’utente su discoChatClientFactory— l’unico posto dove l’app conosce la differenza tra i provider; restituisce unIChatClientChatService— orchestratore stateless che fa streaming delle risposte dall’IChatClientattivoAIModelService— chiama l’endpoint/modelsdi ogni provider, con una lista di fallback
Il modello di configurazione
Tutta la configurazione AI vive in una sezione AI di appsettings.json:
{
"AI": {
"ActiveProvider": "OpenAI",
"Providers": {
"OpenAI": {
"Enabled": true,
"ApiKey": "",
"Endpoint": "https://api.openai.com/v1",
"DefaultModel": "gpt-4o-mini",
"Models": ["gpt-4o-mini", "gpt-4o", "gpt-5-mini", "o4-mini"]
},
"AzureOpenAI": {
"Enabled": false,
"ApiKey": "",
"Endpoint": "",
"DeploymentName": "",
"ApiVersion": "2024-10-21"
},
"Anthropic": {
"Enabled": false,
"ApiKey": "",
"Endpoint": "https://api.anthropic.com",
"DefaultModel": "claude-sonnet-4-20250514"
},
"GoogleAI": {
"Enabled": false,
"ApiKey": "",
"Endpoint": "https://generativelanguage.googleapis.com",
"DefaultModel": "gemini-2.5-flash"
}
},
"Defaults": {
"Temperature": 0.7,
"MaxOutputTokens": 1024,
"SystemPrompt": "You are a helpful assistant."
}
}
}
Questa struttura è il contratto. Per scambiare il layer di storage (database, Key Vault, browser storage, file share), basta modificare solo AIConfigurationService — nient’altro nell’app cambia.
La ChatClientFactory: un solo posto per tutta la logica provider
La factory è il cuore del template. Dopo che questo metodo ritorna, tutto il codice downstream vede solo un IChatClient:
public (IChatClient Client, string Model) Create(string providerKey)
{
var key = NormalizeProviderKey(providerKey);
var settings = _config.GetProvider(key);
if (!settings.Enabled)
throw new InvalidOperationException($"Provider '{providerKey}' is disabled.");
IChatClient inner;
string model;
switch (key)
{
case "OpenAI":
model = settings.DefaultModel ?? "gpt-4o-mini";
var openAI = new OpenAIClient(
new ApiKeyCredential(settings.ApiKey!),
new OpenAIClientOptions { Endpoint = ... });
inner = openAI.GetChatClient(model).AsIChatClient();
break;
case "AzureOpenAI":
model = settings.DeploymentName!;
var azure = new AzureOpenAIClient(
new Uri(settings.Endpoint!),
new AzureKeyCredential(settings.ApiKey!));
inner = azure.GetChatClient(model).AsIChatClient();
break;
case "Anthropic":
model = settings.DefaultModel ?? "claude-sonnet-4-20250514";
inner = new AnthropicChatClient(settings.ApiKey!, model, _httpClientFactory.CreateClient(...));
break;
case "GoogleAI":
model = settings.DefaultModel ?? "gemini-2.5-flash";
inner = new GoogleAIChatClient(settings.ApiKey!, model, _httpClientFactory.CreateClient(...));
break;
default:
throw new InvalidOperationException($"Unknown provider '{providerKey}'.");
}
return (inner, model);
}
Per Anthropic e Google AI, SimpleChat include client personalizzati (AnthropicChatClient e GoogleAIChatClient) che adattano le rispettive REST API all’interfaccia IChatClient di Microsoft.Extensions.AI — necessari perché questi provider non hanno ancora un pacchetto MEAI ufficiale.
Streaming e configurazione runtime
ChatService.StreamAsync restituisce un IAsyncEnumerable<string> di chunk di token. L’interfaccia utente fa lo streaming degli aggiornamenti nella bolla dell’assistente man mano che arrivano, e supporta un pulsante Cancel che interrompe la chiamata in corso.
Una delle funzionalità più interessanti è il cambio di provider e modello a runtime senza riavviare l’app. Il dropdown viene popolato dall’endpoint /models live di ogni provider. Il trucco tecnico è in Program.cs:
builder.Configuration.AddJsonFile(userOverlay, optional: true, reloadOnChange: true);
// ...
builder.Services.AddOptions<AIOptions>()
.Bind(builder.Configuration.GetSection(AIOptions.SectionName));
reloadOnChange: true fa sì che salvare il file faccia scattare IOptionsMonitor<AIOptions>.OnChange, al quale il ChatPanel è sottoscritto — così il dropdown del provider si aggiorna senza ricaricare la pagina.
Gestione sicura delle chiavi API
In Development, le modifiche alla pagina Settings vengono scritte su appsettings.Development.json. In Production vengono scritte su un file separato appsettings.User.json che viene sovrapposto ad appsettings.json all’avvio — così le chiavi reali non devono mai essere committate nel repository.
private string WritablePath => _env.IsDevelopment()
? Path.Combine(_env.ContentRootPath, "appsettings.Development.json")
: Path.Combine(_env.ContentRootPath, "appsettings.User.json");
Come usarlo come template per i propri agenti AI
Il vero valore di SimpleChat non è l’applicazione in sé, ma il pattern riutilizzabile che porta con sé. Quando si inizia una nuova app, si può puntare un agente AI di coding verso questo repository con l’istruzione: “implementa la configurazione di SimpleChat, ma salva le impostazioni AI in un database (o in un file JSON, o in Azure Key Vault, o dove mi dici).” Tutta la logica di incollaggio specifica per provider è già fatta.
Il contratto è semplice: l’agente deve implementare solo AIConfigurationService per leggere e scrivere lo stesso grafo di oggetti AIOptions. Nient’altro nell’app deve cambiare.
Conclusione
SimpleChat è un punto di partenza concreto per chiunque voglia costruire applicazioni AI con Blazor e .NET 10 Aspire senza dover reinventare ogni volta la ruota dell’integrazione multi-provider. L’architettura è intenzionalmente piccola e comprensibile: un frontend Blazor Server, un servizio di configurazione, una factory che produce un IChatClient, e un orchestratore di streaming leggero sopra. Il codice è aperto e modificabile per adattarsi a qualsiasi layer di storage.
Per chi lavora regolarmente con LLM in progetti .NET, avere questo template a disposizione può fare risparmiare ore di lavoro ripetitivo ad ogni nuovo progetto.
Fonte originale: SimpleChat: A Provider-Agnostic AI Chat Starter for Blazor di Michael Washington — via Morning Dew #4664