Vai al contenuto principale

#c

Un agente AI è un sistema distribuito, non un chatbot: pattern di durable orchestration in .NET

Negli ultimi mesi molti team hanno scoperto a proprie spese che il problema più grande di un agente AI in produzione non è la qualità del prompt, ma l

Altro...

Negli ultimi mesi molti team hanno scoperto a proprie spese che il problema più grande di un agente AI in produzione non è la qualità del prompt, ma la sua affidabilità operativa. Un articolo recente su DZone, “Your AI Agent Is a Distributed System, Not a Chatbot”, mette a fuoco un punto che chi progetta sistemi backend conosce bene da anni: se un componente deve sopravvivere a crash, timeout, retry e attese umane di ore, non puoi trattarlo come una semplice chiamata sincrona a un modello. Devi trattarlo come un sistema distribuito, con tutto ciò che questo comporta: stato persistente, checkpoint, idempotenza e recovery.

In questo articolo riprendiamo quei concetti e li caliamo nella pratica con .NET, mostrando come Azure Durable Functions (o alternative come Temporal) permettano di costruire agenti AI multi-step che non perdono lo stato quando qualcosa va storto — cosa che, in produzione, prima o poi succede sempre.

Perché un chatbot stateless non bastaUn chatbot classico è un ciclo request/response: l’utente scrive, il modello risponde, fine. Un agente AI enterprise, invece, tipicamente deve:

orchestrare più chiamate a modelli e strumenti in sequenza o in parallelo;

interrogare sistemi esterni (CRM, ticketing, knowledge base, database);

attendere l’approvazione di un umano, che può richiedere minuti oppure ore;

gestire fallimenti parziali senza dover ripartire da zero;

garantire che un’azione con effetti collaterali (un rimborso, un invio email, una scrittura su un sistema esterno) non venga eseguita due volte per colpa di un retry.

Nessuno di questi requisiti è nuovo: sono gli stessi problemi che i sistemi distribuiti risolvono da decenni con pattern come saga, checkpointing e idempotency key. La differenza è che oggi il “servizio downstream” spesso è un LLM, e la latenza dominante non è più quella di rete, ma quella dell’attesa umana.

Durable orchestration: lo stato che sopravvive al crashIl pattern architetturale proposto è un runtime di orchestrazione durevole, che mantiene lo stato del workflow indipendentemente dal ciclo di vita del processo che lo esegue. In .NET, l’implementazione più diretta è Azure Durable Functions, che introduce tre concetti chiave:

Orchestrator function: decide “cosa succede dopo”, applica le retry policy e coordina le chiamate, ma non esegue lavoro con effetti collaterali direttamente;

Activity function: esegue il lavoro reale (chiamata al modello, query, side effect) ed è il livello dove si applica l’idempotenza;

Checkpointing automatico: dopo ogni await, il runtime salva lo stato dell’orchestrazione, così un crash del processo non fa perdere il progresso già fatto.

Un’alternativa nota, citata anche nell’articolo originale, è Temporal, che applica lo stesso principio con un modello di programmazione simile ma un runtime a sé stante, spesso preferito in contesti multi-linguaggio o Kubernetes-native.

Il pattern fan-out/fan-in per agenti multipliQuando un task richiede il contributo di più agenti specializzati (per esempio: un agente diagnostico, uno di ricerca sulla knowledge base, uno che consulta lo storico dei casi e uno che verifica le policy aziendali), il pattern corretto è il fan-out/fan-in: l’orchestratore lancia tutte le attività in parallelo e aggrega i risultati solo quando sono tutte terminate.

Ecco un esempio realistico in C# con il modello isolato di Azure Functions, ispirato agli esempi ufficiali Microsoft:

[Function("SupportInvestigationOrchestrator")]
public static async Task<InvestigationResult> Run(
[OrchestrationTrigger] TaskOrchestrationContext context)
{
var caseId = context.GetInput();

// Fan-out: 4 agenti specializzati lanciati in parallelo
var diagnosticTask = context.CallActivityAsync<AgentFinding>(
    "RunDiagnosticAgent", caseId);
var knowledgeTask = context.CallActivityAsync<AgentFinding>(
    "RunKnowledgeSearchAgent", caseId);
var historyTask = context.CallActivityAsync<AgentFinding>(
    "RunHistoricalCaseAgent", caseId);
var policyTask = context.CallActivityAsync<AgentFinding>(
    "RunPolicyAgent", caseId);

// Fan-in: si attende che tutti i task completino
AgentFinding[] findings = await Task.WhenAll(
    diagnosticTask, knowledgeTask, historyTask, policyTask);

// Sintesi della raccomandazione finale
var recommendation = await context.CallActivityAsync<Recommendation>(
    "SynthesizeRecommendation", findings);

return new InvestigationResult(caseId, findings, recommendation);

}Il vantaggio rispetto a un semplice Task.WhenAll in un servizio stateless è che, se il processo host crasha mentre due agenti su quattro hanno già risposto, l’orchestrazione riparte dal checkpoint e non richiama gli agenti già completati.

Human-in-the-loop senza tenere aperta la computeIl collo di bottiglia più comune non è il modello, ma l’attesa di un’approvazione umana. Un workflow che tenesse una funzione serverless “in ascolto” per ore sarebbe insostenibile in termini di costo e di timeout. Il pattern corretto è sospendere l’orchestrazione in attesa di un evento esterno:

[Function("SupportInvestigationOrchestrator")]
public static async Task<InvestigationResult> Run(
[OrchestrationTrigger] TaskOrchestrationContext context)
{
// ... fan-out/fan-in come sopra ...

await context.CallActivityAsync("NotifyHumanReviewer", recommendation);

// Il workflow si sospende senza consumare risorse compute
// fino a quando l'evento non arriva, anche dopo diverse ore
var decision = await context.WaitForExternalEvent<HumanReviewDecision>(
    "HumanReviewCompleted");

if (decision.Approved)
{
    await context.CallActivityAsync("ApplyResolution", recommendation);
}

return new InvestigationResult(caseId, findings, recommendation, decision);

}L’evento viene “consegnato” all’orchestrazione in pausa tramite una chiamata separata (per esempio da una Function HTTP-triggered collegata a un pulsante “Approva” nella UI), e il runtime si occupa di riprendere l’esecuzione esattamente dal punto in cui era stata sospesa.

Idempotenza: il vero rischio nascostoUn runtime durevole garantisce tipicamente semantica at-least-once: dopo un crash, un’attività può essere rieseguita. Questo va benissimo per operazioni pure (una query di lettura), ma è pericoloso per operazioni con side effect (un rimborso, un invio email, una scrittura irreversibile). Il momento critico è il cosiddetto crash window: l’intervallo tra il completamento effettivo di un’azione e il salvataggio del suo checkpoint.

La soluzione è associare a ogni operazione critica una idempotency key deterministica, così che una riesecuzione accidentale venga riconosciuta e ignorata a livello applicativo:

[Function("ApplyGoodwillRefund")]
public static async Task ApplyGoodwillRefund(
[ActivityTrigger] RefundRequest request)
{
string idempotencyKey = $"{request.CaseId}:goodwill-refund";

if (await _paymentService.WasAlreadyProcessedAsync(idempotencyKey))
{
    return; // operazione già eseguita, nessun effetto collaterale duplicato
}

await _paymentService.ProcessRefundAsync(request, idempotencyKey);

}Questo pattern, ben noto a chi lavora con gateway di pagamento, va applicato sistematicamente a ogni activity che tocchi sistemi esterni non idempotenti per natura.

Successo parziale, non tutto-o-nienteSe uno dei quattro agenti fallisce (per esempio, il servizio di knowledge search va in timeout), il sistema non dovrebbe far fallire l’intera indagine. Meglio trattare i risultati come esiti strutturati, distinguendo tra “successo”, “fallito” e “degradato”, e permettere che la sintesi finale proceda comunque, segnalando esplicitamente quali fonti mancano:

public record AgentFinding(
string AgentName,
AgentStatus Status, // Success, Failed, Degraded
string? Result,
string? FailureReason);Questo approccio “graceful degradation” è preferibile a un fallimento totale che obbliga a rieseguire da capo un’indagine costosa in termini di tempo e token consumati.

Osservabilità come requisito di prodottoIn un sistema dove un’indagine può durare ore e coinvolgere quattro o più agenti, la tracciabilità non è un dettaglio tecnico: è parte dell’esperienza utente e, in molti contesti regolamentati, un requisito di audit. Vale la pena tracciare sistematicamente:

workflow instance ID, correlation ID e case ID;

lo stage corrente e la durata di ogni singolo agente;

il numero di retry e il motivo di ogni fallimento;

la latenza di revisione umana (spesso il fattore dominante);

la tracciabilità delle evidenze usate per la raccomandazione finale, per scopi di audit.

ConclusioneIl messaggio di fondo è semplice ma spesso trascurato: il workflow conta più del prompt. Un agente AI ben progettato non è quello con il prompt più raffinato, ma quello costruito su un runtime capace di sopravvivere a crash, gestire attese umane di ore senza sprecare risorse, ed evitare effetti collaterali duplicati. Per chi lavora nello stack .NET, Azure Durable Functions offre oggi gli strumenti necessari per applicare questi pattern senza dover reinventare un motore di orchestrazione da zero; per contesti poliglotta o già containerizzati, Temporal resta un’alternativa solida con la stessa filosofia.

Fonte originale: “Your AI Agent Is a Distributed System, Not a Chatbot” su DZone.

#ai #ia #microsoft #c #net #azure #dev

0 0 1

Crittografia post-quantistica: perché il threat modeling non può più aspettare (con esempi in .NET 10)

C’è una minaccia alla crittografia moderna che non richiede un computer quantistico funzionante oggi per essere reale oggi: si chiama “harvest now, de

Altro...

C’è una minaccia alla crittografia moderna che non richiede un computer quantistico funzionante oggi per essere reale oggi: si chiama “harvest now, decrypt later” (raccogli ora, decifra dopo). Un attaccante con risorse sufficienti — uno stato-nazione, tipicamente — può intercettare e archiviare traffico cifrato con RSA o ECC adesso, per poi decifrarlo tra qualche anno, quando computer quantistici sufficientemente potenti saranno disponibili. Per dati con un ciclo di vita lungo — segreti industriali, cartelle cliniche, comunicazioni diplomatiche, chiavi di firma di lungo periodo — la finestra di esposizione è già aperta, anche se il computer quantistico “rompi-RSA” non esiste ancora.

È in questo contesto che Microsoft ha recentemente pubblicato una guida al threat modeling applicato specificamente alla migrazione verso la crittografia post-quantistica (PQC), invitando organizzazioni e team di sviluppo a non aspettare la disponibilità degli algoritmi per cominciare a muoversi.

Perché serve il threat modeling, e non solo un elenco di algoritmiIl problema centrale che Microsoft evidenzia non è tecnico in senso stretto, è organizzativo: la maggior parte delle aziende non ha un inventario completo di dove e come viene usata la crittografia nei propri sistemi. Certificati TLS, librerie di firma incorporate in applicazioni legacy, hardware embedded con chiavi hard-coded, protocolli proprietari che usano RSA “perché si è sempre fatto così” — sono tutte dipendenze crittografiche nascoste che un aggiornamento generico non intercetta.

Il threat modeling applicato alla PQC serve esattamente a questo: mappare asset, flussi di dati, confini di fiducia (trust boundary) e controlli di sicurezza esistenti per far emergere queste dipendenze prima che diventino un problema urgente sotto pressione regolatoria o, peggio, sotto attacco. Concretamente, significa rispondere a domande come: quali sistemi cifrano dati che devono restare confidenziali per più di 5-10 anni? Quali certificati di firma del codice hanno una validità che si estende oltre il 2030? Quali protocolli interni non supportano l’agilità crittografica, cioè non permettono di sostituire un algoritmo senza riscrivere l’applicazione?

Gli algoritmi: da NIST a nomi che iniziano a essere familiariDopo anni di standardizzazione, il NIST ha pubblicato tre standard che è ormai il momento di conoscere, perché stanno entrando nei prodotti che usiamo quotidianamente:

ML-KEM (Module-Lattice Key Encapsulation Mechanism, FIPS 203) — sostituisce lo scambio di chiavi basato su RSA o curve ellittiche (ECDH). È l’algoritmo che entra in gioco nell’handshake TLS per stabilire un segreto condiviso.

ML-DSA (Module-Lattice Digital Signature Algorithm, FIPS 204) — l’algoritmo di firma digitale post-quantistico, pensato come sostituto di RSA e ECDSA per firmare certificati, codice e messaggi.

SLH-DSA (Stateless Hash-Based Digital Signature Algorithm, FIPS 205) — un algoritmo di firma alternativo, basato su funzioni hash anziché su reticoli, pensato come backup conservativo nel caso in cui in futuro emergano debolezze crittanalitiche nei problemi su reticolo.

Sul fronte delle raccomandazioni pratiche, Microsoft insiste su un punto spesso sottovalutato: la migrazione a TLS 1.3 è il prerequisito, non un dettaglio. TLS 1.2 non supporta i meccanismi ibridi necessari per introdurre ML-KEM accanto agli algoritmi classici, e restare su TLS 1.2 significa restare bloccati fuori dalla transizione PQC anche quando gli algoritmi saranno pronti. In parallelo, si consiglia di alzare l’asticella anche sulla crittografia simmetrica e sulle funzioni hash, adottando AES-256 e SHA-384 come standard, per mantenere margini di sicurezza adeguati anche in scenari quantistici (dove alcuni attacchi, come Grover, dimezzano di fatto la sicurezza effettiva degli algoritmi simmetrici).

La timeline che Microsoft si è data per la transizione dei propri prodotti e servizi critici è il 2029 — una data che vale la pena tenere a mente come riferimento di settore, anche per pianificare le proprie migrazioni interne con lo stesso ordine di grandezza.

Cosa c’è già oggi: le API disponibiliLa parte più interessante per chi scrive codice è che la PQC non è più solo teoria da paper accademico: le API sono già disponibili e generalmente disponibili (GA) sulle piattaforme Microsoft.

Windows: CNGSu Windows, il supporto arriva a livello di CNG (Cryptography API: Next Generation), con identificatori di algoritmo dedicati per ML-KEM e ML-DSA utilizzabili tramite le funzioni BCryptGenerateKeyPair e le relative API di key exchange e firma. Il supporto nativo è arrivato a partire da Windows 11 con gli aggiornamenti di novembre 2025: chi gestisce flotte Windows dovrebbe verificare di essere allineato con le patch più recenti prima di pianificare qualunque test.

.NET 10: MLKem e MLDsa in System.Security.CryptographyPer chi sviluppa in C#, la notizia più concreta è l’arrivo delle classi MLKem e MLDsa direttamente in System.Security.Cryptography con .NET 10. Ecco un esempio minimo di key encapsulation:

using System.Security.Cryptography;

if (!MLKem.IsSupported)
{
Console.WriteLine("ML-KEM non è supportato su questa piattaforma");
return;
}

MLKemAlgorithm alg = MLKemAlgorithm.MLKem768;

using (MLKem privateKey = MLKem.GenerateKey(alg))
using (MLKem publicKey = MLKem.ImportEncapsulationKey(
alg, privateKey.ExportEncapsulationKey()))
{
publicKey.Encapsulate(out byte[] ciphertext, out byte[] sharedSecret1);
byte[] sharedSecret2 = privateKey.Decapsulate(ciphertext);

bool match = sharedSecret1.AsSpan().SequenceEqual(sharedSecret2);
Console.WriteLine($"I segreti condivisi coincidono: {match}");

}E un esempio di firma con ML-DSA:

MLDsaAlgorithm alg = MLDsaAlgorithm.MLDsa65;

using (MLDsa key = MLDsa.GenerateKey(alg))
{
byte[] data = "Messaggio da firmare"u8.ToArray();
byte[] signature = new byte[alg.SignatureSizeInBytes];

key.SignData(data, signature);
bool verified = key.VerifyData(data, signature);

Console.WriteLine($"Firma verificata: {verified}");

}Entrambe le famiglie di algoritmi sono disponibili in più varianti — MLKem512, MLKem768, MLKem1024 per il key encapsulation, e MLDsa44, MLDsa65, MLDsa87 per la firma — con un compromesso crescente tra sicurezza e dimensione delle chiavi. Ad esempio, per MLDsa65 la chiave pubblica occupa 1952 byte, quella privata 4032 byte e la firma 3309 byte: numeri sensibilmente più grandi rispetto a ECDSA, un dettaglio da tenere presente quando si progettano protocolli o formati di messaggio con vincoli di banda o storage.

Un paio di note pratiche per chi vuole sperimentare da subito: su Linux serve OpenSSL 3.5 o superiore, mentre chi è ancora su .NET Standard 2.0 può accedere alle stesse API tramite il pacchetto NuGet Microsoft.Bcl.Cryptography. Per il supporto lato TLS 1.3, certificati basati su ML-DSA e SLH-DSA funzionano già in scenari di autenticazione quando sia il sistema operativo sia la controparte della connessione supportano i nuovi algoritmi — un altro motivo per cui la coordinazione tra client e server nella transizione non è opzionale.

Da dove cominciare, in praticaPer un team che affronta questo tema per la prima volta, un percorso ragionevole è: primo, effettuare un inventario reale di certificati, librerie crittografiche e protocolli in uso, distinguendo per criticità e durata di vita dei dati protetti; secondo, verificare che l’infrastruttura TLS sia già su 1.3 ovunque possibile, perché è il prerequisito tecnico per tutto il resto; terzo, iniziare a sperimentare le nuove API — su .NET 10 il costo di ingresso è basso, si tratta di poche righe di codice — in ambienti non di produzione, per capire l’impatto su dimensioni di chiavi, firme e tempi di calcolo prima che diventi un requisito con scadenza stretta.

La crittografia post-quantistica non è ancora un’emergenza operativa per la maggior parte delle organizzazioni, ma il messaggio di Microsoft è chiaro: il momento di mappare le proprie dipendenze crittografiche e cominciare a testare gli algoritmi è adesso, non quando arriverà l’obbligo normativo o il primo incidente.

Fonte originale: Microsoft Urges Threat Modeling to Prepare for Post-Quantum Cryptography Migration, Petri IT Knowledgebase

#sicurezza #microsoft #windows #c #net

0 0 1

Unity abbandona Mono per CoreCLR: cosa cambia con .NET 10 e C# 14

Da Mono a CoreCLR: la fine di un’era per UnitySe sviluppate in C# ma non toccate Unity, la notizia potrebbe sembrarvi di nicchia. Non lo è. Con la rel

Altro...

Da Mono a CoreCLR: la fine di un’era per UnitySe sviluppate in C# ma non toccate Unity, la notizia potrebbe sembrarvi di nicchia. Non lo è. Con la release di Unity 6.8, prevista entro la fine del 2026, il motore di gioco più diffuso al mondo abbandona definitivamente il proprio fork custom di Mono in favore di CoreCLR, il runtime “vero” del .NET moderno, con supporto a .NET 10 e C# 14. Per anni Unity è rimasto ostaggio di un’architettura di scripting congelata a metà del decennio scorso, mentre il resto dell’ecosistema .NET correva avanti tra top-level statements, pattern matching avanzato, source generator e miglioramenti di performance del JIT. Il cambio di runtime non è solo un aggiornamento di versione: è un caso di studio interessante per chiunque lavori con .NET, anche fuori dal game dev, perché racconta come si affronta la migrazione di un’applicazione legacy da AppDomain a un modello di isolamento moderno.

Domain Reload: il problema che ogni sviluppatore Unity conosce a memoriaChi lavora su progetti Unity di dimensioni consistenti conosce fin troppo bene il Domain Reload: si salva uno script, si torna nell’editor, e parte una barra di progresso che su progetti grandi può durare decine di secondi. Il motivo è architetturale: ogni ricompilazione degli script comporta lo scaricamento completo dell’AppDomain e il suo ricaricamento da zero in memoria, un’operazione “a tappeto” pensata per un modello di isolamento vecchio di vent’anni.

Con CoreCLR, Unity abbandona il concetto di AppDomain in favore degli AssemblyLoadContext (ALC), il meccanismo di isolamento e caricamento assembly introdotto nel .NET moderno fin dai tempi di .NET Core. Invece di ricaricare tutto lo stato applicativo, il runtime può scaricare e ricaricare selettivamente solo gli assembly effettivamente modificati.

AssemblyLoadContext: un concetto utile ben oltre UnityPer chi non l’avesse mai usato fuori da un contesto plugin-system, un AssemblyLoadContext è essenzialmente un contenitore isolato in cui caricare assembly .NET, che può essere scaricato indipendentemente dal resto dell’applicazione. È lo stesso meccanismo che sta dietro a scenari come plugin dinamici, hot reload di parti di un’applicazione server, o sandboxing di codice di terze parti. Un esempio minimale, applicabile a qualunque applicazione .NET moderna:

using System.Reflection;
using System.Runtime.Loader;

var alc = new AssemblyLoadContext("PluginContext", isCollectible: true);

Assembly plugin = alc.LoadFromAssemblyPath(@"C:\plugins\MyPlugin.dll");
// ... usa il plugin tramite reflection o interfacce condivise ...

alc.Unload(); // richiede la garbage collection per liberare davvero la memoria
GC.Collect();
GC.WaitForPendingFinalizers();
Il dettaglio da tenere presente, sia in Unity sia in un’applicazione .NET qualsiasi, è che uno scaricamento “leaked” è possibile: se un riferimento a un tipo caricato nell’ALC sopravvive fuori dal suo scope (una closure, un evento sottoscritto, un campo statico), l’assembly non può essere effettivamente liberato dal garbage collector. Non a caso, nel forum ufficiale Unity gli sviluppatori hanno confermato che il motore segnalerà esplicitamente all’utente quando un AssemblyLoadContext non riesce a essere scaricato correttamente, un problema di leak molto simile a quello che chi scrive plugin system in .NET conosce già.

Cosa cambia in pratica, oltre al Domain ReloadLa migrazione a CoreCLR porta con sé una serie di conseguenze pratiche per chi sviluppa con Unity:

ECS più integrato: gli Instance ID passano da 32 a 64 bit, permettendo a GameObject “classici” ed entità ECS (Data-Oriented Technology Stack) di condividere lo stesso spazio di identificatori. Un nuovo metodo GetEntityId() collega direttamente gli oggetti di scena al mondo ECS, riducendo la frizione tra i due paradigmi.

Serializzazione nativa dei dizionari: Dictionary<TKey, TValue> sarà finalmente serializzabile nativamente dall’Inspector, eliminando i workaround basati su liste parallele di chiavi e valori o su ISerializationCallbackReceiver. In parallelo, Unity rimuove il datato e insicuro BinaryFormatter.

IL2CPP resta, ma diventa opzionale: per le piattaforme non coperte da NativeAOT (che CoreCLR usa per la compilazione ahead-of-time), IL2CPP continuerà a essere il backend di scripting necessario. Su piattaforme compatibili, però, sarà possibile scegliere CoreCLR come backend alternativo già a partire da una preview tecnica prevista intorno a Unity 6.7.

MiMalloc come allocatore di memoria: l’integrazione dell’allocatore ad alte prestazioni di Microsoft riduce i problemi di lock contention nei carichi multithread, con benefici diretti per chi usa il C# Job System in scenari data-oriented.

Perché la cosa interessa anche chi non fa game devIl percorso di Unity da Mono a CoreCLR è, in piccolo, lo stesso tipo di migrazione che molte software house con applicazioni .NET Framework legacy dovranno affrontare prima o poi: passare da un modello di isolamento basato su AppDomain (o peggio, da un fork custom del runtime) a un’architettura moderna basata su ALC, con tutti i vantaggi in termini di performance, ma anche le insidie legate alla gestione del ciclo di vita degli assembly caricati dinamicamente. Se lavorate su plugin system, hosting di codice di terze parti o architetture a moduli scaricabili a runtime, vale la pena studiare da vicino come Unity gestisce concretamente i leak di ALC: è un problema che, presto o tardi, si incontra in qualunque applicazione .NET che provi a fare hot-reload o isolamento dinamico.

ConclusioneCon Unity 6.8, il motore smette di essere un’isola separata dall’ecosistema .NET e si allinea finalmente al runtime moderno, portando in dote C# 14, prestazioni migliori e un developer loop molto più rapido. Per chi sviluppa giochi è una notizia enorme; per chi sviluppa in .NET in generale è un promemoria utile su come affrontare, con gli strumenti giusti, la migrazione da architetture di isolamento legacy a AssemblyLoadContext.

Fonte: DZone, con dettagli tecnici dal thread ufficiale Path to CoreCLR, 2026: Upgrade Guide su Unity Discussions.

#c #programmazione #net #dev

0 0 1

C# 15 introduce le union: basta OneOf e Results per modellare i tipi (.NET 11 Preview 6)

Da anni gli sviluppatori C# aspettano un modo nativo per dire “questo valore è esattamente uno tra questi tipi, e nient’altro”, con il compilatore in

Altro...

Da anni gli sviluppatori C# aspettano un modo nativo per dire “questo valore è esattamente uno tra questi tipi, e nient’altro”, con il compilatore in grado di garantirlo. In assenza di un supporto di linguaggio, la community si è arrangiata con pattern come Results<T> di ASP.NET Core Minimal API o con librerie come OneOf. Funzionano, ma con un limite strutturale: se aggiungi un quarto caso possibile a una funzione che prima ne restituiva tre, nessuno ti avvisa che uno switch a valle non gestisce il nuovo tipo. Manca l’esaustività, che è poi il punto centrale delle discriminated union.

Con .NET 11 Preview 6 (rilasciata il 14 luglio 2026) e C# 15, questa lacuna si chiude: arriva la parola chiave union, insieme a un intero pacchetto di funzionalità correlate (conversioni implicite, pattern matching esaustivo, gestione della nullabilità) che rendono i union type un cittadino di prima classe del linguaggio.

La sintassi: dichiarare un union typeLa forma più semplice è la union declaration, una sintassi compatta che genera automaticamente uno struct:

public union Pet(Cat, Dog, Bird);Questa singola riga dice al compilatore che un Pet è esattamente un Cat, un Dog o un Bird, e nient’altro. Sotto il cofano, il compilatore genera uno struct che implementa l’interfaccia System.Runtime.CompilerServices.IUnion e conserva il valore effettivo in una proprietà Value di tipo object?.

I union generici sono immediatamente utili per modellare risultati di operazioni che possono fallire, un caso d’uso ricorrentissimo in codice di produzione:

public union Result<TSuccess, TError>(TSuccess, TError);

Result<User, Error> Register(string username)
{
if (string.IsNullOrEmpty(username))
return new Error("Username cannot be empty");

return new User(username);

}Da notare l’ultima riga: nessun wrapping esplicito, nessuna chiamata a un factory method. Il valore User o Error viene convertito implicitamente in Result<User, Error> grazie alle union conversion generate automaticamente per ogni case type.

Pattern matching esaustivo, finalmenteIl vero salto di qualità è nel matching. Se dimentichi un caso in uno switch su un union type, il compilatore te lo dice:

string Describe(Pet pet) => pet switch
{
Dog d => d.Name,
Cat c => c.Name,
// CS8509: switch expression does not handle all
// possible values of its input type (Bird)
};Aggiungendo il caso mancante, l’avviso scompare, e non serve più il classico _ => throw new InvalidOperationException("unreachable") per zittire il compilatore:

string Describe(Pet pet) => pet switch
{
Dog d => d.Name,
Cat c => c.Name,
Bird b => b.Name,
};Il punto interessante è che questa esaustività è dinamica: se in futuro aggiungi un nuovo case type al union (per esempio un quarto animale), ogni switch esistente che non lo gestisce si illumina di un warning. Per chi ha inseguito bug di produzione causati da un case dimenticato in un evento di dominio, è un cambiamento non da poco.

I pattern si applicano direttamente al union, senza bisogno di accedere manualmente a .Value: il compilatore effettua l’unwrapping in automatico, anche negli if pattern classici:

if (pet is Dog d)
{
Console.WriteLine(d.Name);
}Gestione del nullIl valore di default di un union type ha Value == null, ed è possibile intercettarlo esplicitamente con il pattern null:

string Describe(Pet pet) => pet switch
{
null => "nessun animale",
Dog d => d.Name,
Cat c => c.Name,
Bird b => b.Name,
};La necessità o meno del ramo null dipende dal fatto che il parametro o il campo union-typed sia nullable: l’analisi di nullabilità del compilatore guida correttamente questi casi, in coerenza con le annotazioni enable già in uso nella maggior parte delle codebase moderne.

Un caso pratico: modellare eventi di dominioI union type diventano davvero interessanti quando li si combina con i record per il domain modeling, per esempio in un sistema di elaborazione ordini con eventi distinti:

public record OrderPlaced(Guid OrderId, decimal Total);
public record OrderShipped(Guid OrderId, string TrackingNumber);
public record OrderCancelled(Guid OrderId, string Reason);
public union OrderEvent(OrderPlaced, OrderShipped, OrderCancelled);

string Summarize(OrderEvent evt) => evt switch
{
OrderPlaced p => $"Ordine {p.OrderId} piazzato per {p.Total:C}",
OrderShipped s => $"Ordine {s.OrderId} spedito, tracking: {s.TrackingNumber}",
OrderCancelled c => $"Ordine {c.OrderId} annullato: {c.Reason}",
};Quando tra tre mesi qualcuno aggiungerà OrderRefunded al union, ogni handler che non lo gestisce lancerà un warning in fase di compilazione, invece di fallire silenziosamente in produzione. È il compilatore che fa da revisore di esaustività al posto tuo.

I union possono anche esporre metodi e proprietà calcolate (ma non campi d’istanza), utile per incapsulare logica di conversione:

public union Length(Meters, Feet)
{
public double TotalMeters => this switch
{
Meters m => m.Value,
Feet f => f.Value * 0.3048,
_ => throw new InvalidOperationException(),
};
}Union personalizzati con l’attributo [Union]La parola chiave union genera sempre uno struct. Se per motivi di identità, ereditarietà o layout di memoria serve un tipo reference, è possibile costruire un union personalizzato applicando direttamente l’attributo System.Runtime.CompilerServices.UnionAttribute e implementando l’interfaccia IUnion:

[System.Runtime.CompilerServices.Union]
public class Shape : System.Runtime.CompilerServices.IUnion
{
private readonly object? _value;
public Shape(Circle value) { _value = value; }
public Shape(Rectangle value) { _value = value; }
public object? Value => _value;
}Nella grande maggioranza dei casi, però, la sintassi union compatta copre già ogni esigenza pratica: l’approccio con attributo è pensato per scenari con requisiti specifici che lo struct generato non può soddisfare.

Come provarlo oggiI union type sono attualmente in preview. Per sperimentarli serve l’SDK .NET 11 Preview e questa configurazione nel file di progetto:

ConclusioniF# ha le discriminated union fin dalle origini, e la richiesta di un equivalente in C# (csharplang issue #113) è aperta da anni. Con l’arrivo dei union type, non ogni problema di modellazione richiede più una gerarchia di classi: per rappresentare “uno tra un insieme chiuso di tipi”, ora esiste un costrutto di linguaggio dedicato, con conversioni implicite, pattern matching esaustivo e un’ottima integrazione con i record esistenti. Per chi lavora ogni giorno con API che possono restituire risultati eterogenei, o con event sourcing e domain modeling, è una delle novità più concrete della prossima versione di C#.

Fonte: .NET Blog, C# language reference proposal: Unions e Maarten Balliauw.

#c #guide #tutorial #net

0 0 0

Imparare a programmare giocando

Volete imparare a programmare giocando? Oppure siete già in grado ma volete mettere alla prova le vostre conoscenze

Altro...

Imparare a programmare giocando

Volete imparare a programmare giocando? Oppure siete già in grado ma volete mettere alla prova le vostre conoscenze? Abbiamo quello che fa per voi!

https://blog.lealternative.net/2020/10/07/imparare-a-programmare-giocando/

Imparare a programmare giocando Imparare a programmare giocando

#c #coding #esempipratici #giocando #giocare #javascript #programmare #programmazione

0 0 1