<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Topics tagged with net]]></title><description><![CDATA[A list of topics that have been tagged with net]]></description><link>https://forum.androidiani.net/tags/net</link><generator>RSS for Node</generator><lastBuildDate>Fri, 01 May 2026 00:36:51 GMT</lastBuildDate><atom:link href="https://forum.androidiani.net/tags/net.rss" rel="self" type="application/rss+xml"/><pubDate>Thu, 30 Apr 2026 08:43:30 GMT</pubDate><ttl>60</ttl><item><title><![CDATA[Il pulsante di emergenza: revoca immediata dei token in .NET 10 con Duende IdentityServer]]></title><description><![CDATA[Immagina questo scenario da incubo: il telefono di un cliente bancario viene rubato, l’app mobile è già autenticata, e il ladro ha pieno accesso al suo conto. Il supporto riceve la chiamata disperata. Ogni secondo conta. Quanto tempo ci vuole per revocare quella sessione attiva e mettere al sicuro i fondi?Se stai usando JWT self-contained standard, la risposta onesta potrebbe essere “fino a un’ora”, a seconda della durata di validità del token. Non è accettabile. Vediamo come i Reference Token ti forniscono un vero pulsante di emergenza per queste situazioni, e come configurarli con Duende IdentityServer in .NET 10.Il problema dei JWT self-containedI JWT self-contained sono il cavallo di battaglia dell’autorizzazione moderna. Trasportano tutte le claim di cui un’API ha bisogno direttamente nel token. Nessuna query al database, nessuna chiamata al provider di identità. L’API valida la firma, controlla la scadenza, e il gioco è fatto. È elegante e performante.Ma questa natura self-contained è un’arma a doppio taglio. Una volta emesso un JWT, il provider di identità non ha più nulla da dire su di esso. Il token è valido fino a quando la claim exp non dice il contrario, tipicamente 5-60 minuti. Se un dispositivo viene rubato, un account compromesso, o una minaccia rilevata, non puoi revocare quel token. Sei costretto ad aspettare che scada.Per molte applicazioni questo compromesso è accettabile. Per ambienti ad alta sicurezza come banking, sanità o sistemi governativi, è un gap che non puoi permetterti.Reference Token: premere il pulsanteI Reference Token ribaltano il modello. Invece di incorporare tutte le claim direttamente nel token, IdentityServer memorizza il contenuto del token lato server nel suo persisted grant store e consegna al client un identificatore opaco (un “handle”). Quando un’API riceve questo handle, chiama l’endpoint di introspection di IdentityServer per validare il token e recuperare le claim.Questo cambia tutto. Poiché i dati del token risiedono sul server, puoi cancellarli in qualsiasi momento. La revoca è immediata. La prossima volta che l’API chiama l’endpoint di introspection, riceve "active": false, e l’accesso viene negato. Niente attese di scadenza, niente token obsoleti in circolazione.Il compromesso? Ogni chiamata API richiede un round-trip verso l’endpoint di introspection. Per API pubbliche su scala internet, è una preoccupazione. Per servizi interni e ambienti ad alta sicurezza, è un prezzo ragionevole per la capacità di staccare la spina istantaneamente.Configurare i Reference Token in IdentityServerPassare a Reference Token per un client richiede una singola riga di configurazione. Quando definisci il client in Duende IdentityServer, imposta la proprietà AccessTokenType:new Client
{
    ClientId = "banking_app",
    ClientSecrets = { new Secret("secret".Sha256()) },
    AllowedGrantTypes = GrantTypes.Code,

    // Questa è la riga chiave
    AccessTokenType = AccessTokenType.Reference,

    AllowOfflineAccess = true,
    RedirectUris = { "https://banking.example.com/signin-oidc" },
    AllowedScopes = { "openid", "profile", "accounts.read", "transfers.write" }
};I token emessi per questo client saranno ora handle opachi invece di JWT self-contained.Configurare l’API per l’introspectionLa tua API deve sapere come validare questi token opachi. Invece del (o in aggiunta al) classico JWT validation, configuri l’introspection OAuth 2.0. Prima, definisci un API Resource con un secret:new ApiResource("banking_api")
{
    Scopes = { "accounts.read", "transfers.write" },
    ApiSecrets = { new Secret("api_secret".Sha256()) }
};Poi nel Program.cs della tua API, registra l’handler di introspection:builder.Services.AddAuthentication("token")
    .AddOAuth2Introspection("token", options =&gt;
    {
        options.Authority = "https://identity.banking.example.com";
        options.ClientId = "banking_api";
        options.ClientSecret = "api_secret";
    });Se devi supportare sia JWT che Reference Token (magari durante una migrazione), puoi registrare entrambi gli handler e usare il forwarding per instradare i token a quello corretto:builder.Services.AddAuthentication("token")
    .AddJwtBearer("token", options =&gt;
    {
        options.Authority = "https://identity.banking.example.com";
        options.Audience = "banking_api";
        options.TokenValidationParameters.ValidTypes = ["at+jwt"];
        options.ForwardDefaultSelector = Selector.ForwardReferenceToken("introspection");
    })
    .AddOAuth2Introspection("introspection", options =&gt;
    {
        options.Authority = "https://identity.banking.example.com";
        options.ClientId = "banking_api";
        options.ClientSecret = "api_secret";
    });Revocare un tokenQuando quella chiamata disperata arriva, il tuo sistema di supporto (o una pipeline automatica di rilevamento minacce) può revocare il token immediatamente usando l’endpoint di revocation di IdentityServer, che implementa la RFC 7009:using Duende.IdentityModel.Client;

var client = new HttpClient();
var result = await client.RevokeTokenAsync(new TokenRevocationRequest
{
    Address = "https://identity.banking.example.com/connect/revocation",
    ClientId = "banking_app",
    ClientSecret = "secret",
    Token = stolenAccessToken
});

if (result.IsError)
{
    logger.LogError("Token revocation failed: {Error}", result.Error);
}Una volta revocato, il token viene rimosso dal persisted grant store. La prossima richiesta di introspection da qualsiasi API confermerà che il token non è più attivo. L’accesso è tagliato.Non dimenticare: dovresti anche revocare il refresh token dell’utente per impedire al client di ottenere silenziosamente un nuovo access token:await client.RevokeTokenAsync(new TokenRevocationRequest
{
    Address = "https://identity.banking.example.com/connect/revocation",
    ClientId = "banking_app",
    ClientSecret = "secret",
    Token = refreshToken
});Nota: sia l’introspection che la revocation emettono eventi di audit che puoi usare per implementare log di audit nei settori regolamentati.Quando usare i Reference TokenI Reference Token non sono un sostituto universale dei JWT. Brillano in scenari specifici:La revoca immediata è un requisito imprescindibile (banking, sanità, sistemi compliance-driven)Comunicazione service-to-service interna dove il round-trip di introspection è trascurabileOperazioni ad alto rischio dove il beneficio di sicurezza supera il costo in performancePer API pubbliche su larga scala dove la latenza di revoca è accettabile, i JWT self-contained con breve durata rimangono una scelta solida. Puoi anche mixare i due approcci: Reference Token per client sensibili e JWT per quelli a minor rischio, tutto all’interno dello stesso deployment IdentityServer.ConclusioneOgni architettura di sicurezza implica compromessi. I JWT self-contained scambiano la revocabilità per la performance. I Reference Token scambiano la performance per il controllo. Per gli ambienti dove “aspetta che scada” non è una risposta accettabile, i Reference Token con Duende IdentityServer ti forniscono un vero pulsante di emergenza.L’implementazione è semplice: una proprietà sul client, un handler di introspection sull’API, e una chiamata di revocation quando devi staccare la spina. Quando accadono incidenti di sicurezza — e accadranno — sarai felice di averlo configurato.Fonte originale: The Emergency Stop Button – Implementing Immediate Token Revocation in .NET 10 — Khalid Abuhakmeh, Duende Software (28 aprile 2026)]]></description><link>https://forum.androidiani.net/topic/650373c9-e80f-4878-8fd6-cc69f4d213eb/il-pulsante-di-emergenza-revoca-immediata-dei-token-in-.net-10-con-duende-identityserver</link><guid isPermaLink="true">https://forum.androidiani.net/topic/650373c9-e80f-4878-8fd6-cc69f4d213eb/il-pulsante-di-emergenza-revoca-immediata-dei-token-in-.net-10-con-duende-identityserver</guid><dc:creator><![CDATA[blog@spcnet.it]]></dc:creator><pubDate>Thu, 30 Apr 2026 08:43:30 GMT</pubDate></item><item><title><![CDATA[Come strutturare un’applicazione ASP.NET Core in crescita: dal monolite a strati ai vertical slice]]></title><description><![CDATA[Quando un’applicazione ASP.NET Core è piccola, quasi qualsiasi struttura di cartelle funziona. Controller in una cartella, servizi in un’altra, repository da qualche altra parte. Per un po’ va bene. Poi l’applicazione cresce, le funzionalità si moltiplicano e le regole di business si diffondono per tutta la codebase. Ogni modifica tocca cinque o sei file in posti diversi. I nuovi sviluppatori hanno bisogno di una guida turistica solo per capire dove si trova qualcosa.Quel momento è il punto in cui la struttura smette di essere una scelta cosmetica e diventa un problema di manutenibilità. In questo articolo esaminiamo le opzioni più comuni — Feature Folders, architettura a strati, Clean Architecture, Vertical Slices e Modular Monolith — con un’ottica pratica su quando e perché usarle.L’obiettivo reale non è l'”architettura perfetta”Prima di confrontare i pattern, è utile chiarire l’obiettivo. Non si tratta di rendere il progetto architetturalmente impressionante. Si tratta di rendere più facile:capire dove appartiene il codicemodificare una funzionalità senza rompere funzionalità non correlateinserire nuovi sviluppatori più velocementetestare il comportamento importante con meno attritofar evolvere la struttura man mano che il sistema cresceLa risposta giusta è solitamente quella che crea meno confusione per i prossimi 12-24 mesi di sviluppo, non quella che vince i dibattiti architetturali.La testabilità è una questione di architetturaUno dei controlli pratici più importanti è questo: possiamo verificare il comportamento di business importante con unit test veloci, senza avviare l’intera applicazione o un database reale? Se la risposta è no, l’attrito architetturale si manifesta come feedback lento, modifiche fragili e paura di fare refactoring.Una buona struttura migliora la testabilità rendendo esplicite le dipendenze e mantenendo le regole di business lontane dai dettagli del framework e dell’infrastruttura — cose come accesso al database, gestione HTTP, file system e chiamate a servizi esterni. Una regola pratica:Unit test per le decisioni di business e gli invarianti del dominioIntegration test per database, messaging e wiring HTTPEnd-to-end test per i percorsi utente criticiFeature FoldersI Feature Folders organizzano il codice per capacità di business invece che per tipo tecnico. Invece della struttura classica orizzontale:Controllers/ Services/ Repositories/ Models/ si passa a una struttura verticale per funzionalità:Features/   Orders/     Create/       CreateOrderEndpoint.cs       CreateOrderRequest.cs       CreateOrderHandler.cs     GetById/       GetOrderByIdEndpoint.cs       GetOrderByIdHandler.cs   Products/     List/       ListProductsEndpoint.cs       ListProductsHandler.cs Il principio guida è semplice: se devi modificare la funzionalità “Orders”, la maggior parte del codice che ti serve dovrebbe trovarsi da qualche parte sotto la cartella Orders. Questo riduce drasticamente il tempo di ricerca e la probabilità di modifiche accidentali a funzionalità non correlate.Adatto quando: l’applicazione sta crescendo oltre il CRUD basilare, il team vuole una chiara ownership per funzionalità, gli sviluppatori sono stanchi di saltare tra strati orizzontali per fare una piccola modifica.Attenzione: se applicati in modo disordinato, i Feature Folders possono diventare inconsistenti e trasformarsi in “un’altra convenzione di cartelle”.Architettura a strati (Layered Architecture)L’architettura a strati è la classica separazione in UI, logica di business e accesso ai dati:Web/ Application/ Domain/ Infrastructure/ Esiste da decenni proprio perché è facile da spiegare, facile da insegnare e fornisce una separazione delle responsabilità immediata. Per i team che vengono da tutorial e applicazioni di esempio, è spesso il punto di partenza più familiare.Un dettaglio pratico per .NET moderno: non è sempre necessario un layer repository separato, soprattutto se Entity Framework Core fornisce già l’astrazione necessaria per l’accesso ai dati semplice. Creare repository per “rispettare la struttura” piuttosto che per risolvere un problema reale è una delle trappole comuni.Adatto quando: il team è relativamente piccolo, l’applicazione non è ancora molto complessa, gli sviluppatori traggono vantaggio da una struttura familiare, la codebase è principalmente transazionale e CRUD-oriented.Attenzione: una modifica a una funzionalità richiede spesso modifiche su più strati. La logica di business può frammentarsi. Gli sviluppatori iniziano a creare astrazioni perché la struttura le richiede, non perché il problema ne ha bisogno.Clean ArchitectureClean Architecture pone forte enfasi sui confini tra logica di dominio e dettagli dell’infrastruttura. Il principio centrale è valido: le regole di business non dovrebbero essere strettamente accoppiate a database, web framework, message broker o SDK esterni.In pratica, però, alcuni team spingono Clean Architecture così lontano che ogni caso d’uso viene sepolto sotto strati di interfacce, wrapper, handler, repository e adattatori che il sistema non ha realmente bisogno. Il takeaway più importante non è il template completo, ma il principio: tieni le regole di business lontane dall’infrastruttura tecnica.// Esempio: un handler di dominio che NON dipende da EF Core direttamente public class CreateOrderHandler {     private readonly IOrderRepository _repository;  // astrazione, non EF diretto     private readonly IEventPublisher _events;      public CreateOrderHandler(IOrderRepository repository, IEventPublisher events)     {         _repository = repository;         _events = events;     }      public async Task&lt;OrderId&gt; Handle(CreateOrderCommand command, CancellationToken ct)     {         var order = Order.Create(command.CustomerId, command.Items);         await _repository.SaveAsync(order, ct);         await _events.PublishAsync(new OrderCreated(order.Id), ct);         return order.Id;     } } Adatto quando: il dominio ha una complessità significativa, l’applicazione ha una lunga vita prevista, più infrastrutture devono rimanere scambiabili o isolate, il team ha la disciplina per usare i confini intenzionalmente.Attenzione: è facile over-engineerare. Troppa cerimonia rallenta il lavoro su funzionalità semplici. I team inesperti spesso copiano diagrammi invece di risolvere il vero problema di manutenibilità.Vertical Slice ArchitectureL’architettura a vertical slice organizza il codice attorno a singoli casi d’uso o richieste. Invece di pensare per layer tecnici, ogni “slice” è un percorso verticale completo dalla richiesta alla risposta:Features/   PlaceOrder/     PlaceOrderCommand.cs     PlaceOrderHandler.cs     PlaceOrderValidator.cs     PlaceOrderEndpoint.cs   CancelOrder/     CancelOrderCommand.cs     CancelOrderHandler.cs     CancelOrderEndpoint.cs Ogni slice è autonoma e contiene tutto il necessario per gestire quella specifica operazione. Questo riduce l’accoppiamento tra funzionalità diverse: modificare “PlaceOrder” non richiede di toccare il codice di “CancelOrder”.MediatR è comunemente usato con questo pattern in .NET, ma non è obbligatorio — il pattern funziona anche con endpoint minimali diretti.Adatto quando: le funzionalità sono relativamente indipendenti tra loro, il team preferisce massimizzare la coesione per caso d’uso, si vuole limitare al minimo l’accoppiamento laterale.Attenzione: la duplicazione del codice tra slice simili può crescere se non si definisce chiaramente cosa è condiviso e cosa non lo è.Modular MonolithIl modular monolith è uno step successivo rispetto ai pattern precedenti: invece di organizzare il codice per funzionalità singole, si definiscono moduli di business più ampi con confini chiari tra loro, pur rimanendo un’unica applicazione deployabile.Modules/   Ordering/     Api/     Application/     Domain/     Infrastructure/   Catalog/     Api/     Application/     Domain/     Infrastructure/   Payments/     ... Ogni modulo espone un’interfaccia pubblica e nasconde i propri dettagli interni. La comunicazione tra moduli avviene attraverso quella interfaccia — mai direttamente tra le implementazioni interne. Questo crea i presupposti per un eventuale passaggio a microservizi, se e quando il sistema lo richiederà, senza dover fare un refactoring massiccio.Adatto quando: il sistema è abbastanza grande da giustificare confini chiari tra aree di business, non si vuole la complessità operativa dei microservizi, si vuole prepararsi a una futura decomposizione senza impegnarsi subito.Quale scegliere?Non esiste una risposta universale, ma questo schema può orientare la scelta:App nuova, team piccolo, CRUD dominante → Layered o Feature FoldersApp in crescita, molte funzionalità indipendenti → Feature Folders o Vertical SlicesDominio complesso, lunga vita prevista, team disciplinato → Clean ArchitectureSistema grande, confini di business chiari, no microservizi ancora → Modular MonolithInizia dalla struttura più semplice che risolve il tuo problema attuale. Evolvi quando la complessità del sistema lo giustifica, non prima. Il momento migliore per passare a un’architettura più sofisticata è quando il dolore del non averla è reale e misurabile — non anticipatorio.Fonte: ASP.NET: How to Structure a Growing Application So It Stays Maintainable — Chris Pietschmann, pietschsoft.com.]]></description><link>https://forum.androidiani.net/topic/26decad8-f135-4f5c-9328-7bbdf366198c/come-strutturare-un-applicazione-asp.net-core-in-crescita-dal-monolite-a-strati-ai-vertical-slice</link><guid isPermaLink="true">https://forum.androidiani.net/topic/26decad8-f135-4f5c-9328-7bbdf366198c/come-strutturare-un-applicazione-asp.net-core-in-crescita-dal-monolite-a-strati-ai-vertical-slice</guid><dc:creator><![CDATA[blog@spcnet.it]]></dc:creator><pubDate>Wed, 29 Apr 2026 15:53:47 GMT</pubDate></item><item><title><![CDATA[CodeAct e Hyperlight: agenti AI piu veloci con meno chiamate al modello nel .NET Agent Framework]]></title><description><![CDATA[Chi lavora con agenti AI su basi .NET sa bene che il vero collo di bottiglia non è spesso la qualità del modello, ma il numero di round trip tra il modello stesso e i tool. Un agente che deve recuperare dati, filtrarli, fare calcoli e assemblare un risultato finisce tipicamente per eseguire cinque o sei chiamate separate al modello, ognuna con la propria latenza e il proprio costo in token. Microsoft ha presentato una soluzione concreta a questo problema: CodeAct, ora disponibile nel pacchetto alpha agent-framework-hyperlight.Il problema: troppi turni, troppa latenzaNel flusso tradizionale, un agente ragiona come segue: chiede al modello quale tool usare, esegue quel tool, rimanda il risultato al modello, il quale decide il prossimo tool, e così via. Questo schema modello → tool → modello → tool moltiplica la latenza e il consumo di token con ogni step aggiuntivo. Su task composti da tre, quattro o cinque operazioni concatenate (tipico nelle pipeline di data wrangling, elaborazione report, lookup incrociati), il costo diventa significativo.CodeAct risolve il problema in modo elegante: invece di chiedere al modello di scegliere un tool alla volta, gli viene offerto un singolo tool speciale chiamato execute_code. Il modello esprime l’intero piano come un breve programma Python, che viene eseguito una volta sola in un ambiente sandbox. Il risultato? Latenza ridotta del ~50% e consumo di token calato di oltre il 60% su workload rappresentativi, secondo i dati pubblicati da Microsoft.Hyperlight: sandbox micro-VM per sicurezza senza compromessiLa parte che rende CodeAct praticabile in produzione è Hyperlight: una tecnologia Microsoft che avvia una micro-VM isolata per ogni esecuzione di codice generato dal modello. Il codice Python prodotto dall’LLM gira dentro questa sandbox, senza accesso al filesystem host, alla rete o a qualsiasi risorsa non esplicitamente autorizzata. I tool reali invece continuano a girare nel runtime dell’applicazione, con tutti i permessi necessari.Il bridge tra sandbox e tool avviene tramite la funzione call_tool(...): quando il codice nella sandbox chiama call_tool("nome_tool", ...), Hyperlight instrada la chiamata verso il tool nel processo principale, ne ritorna il risultato nella sandbox, e il programma continua. Il codice generato dall’AI rimane isolato; solo i tool verificati e distribuiti dallo sviluppatore hanno accesso reale alle risorse.Come si integra CodeAct nel proprio agenteIl setup è sorprendentemente compatto. Dopo aver installato i pacchetti agent-framework e agent-framework-hyperlight:from agent_framework import Agent, tool from agent_framework_hyperlight import HyperlightCodeActProvider  @tool def get_weather(city: str) -&gt; dict:     # Restituisce il meteo corrente per una citta     return {"city": city, "temperature_c": 21.5, "conditions": "partly cloudy"}  codeact = HyperlightCodeActProvider(     tools=[get_weather],     approval_mode="never_require", )  agent = Agent(     client=client,     name="CodeActAgent",     instructions="Sei un assistente utile.",     context_providers=[codeact], )  result = await agent.run(     "Ottieni il meteo di Seattle e Amsterdam e confrontali." ) HyperlightCodeActProvider si occupa di due cose in automatico: registra il tool execute_code ad ogni run dell’agente, e inietta nel system prompt le istruzioni sulla sandbox e sui tool disponibili via call_tool(...).Gestione delle approvazioni: chi controlla cosaAgent Framework distingue due modalità di approvazione per i tool:never_require: il framework invoca il tool automaticamente.always_require: ogni chiamata viene sospesa in attesa di un’approvazione human-in-the-loop.Con CodeAct, la logica cambia leggermente. I tool registrati su HyperlightCodeActProvider non vengono esposti direttamente al modello come tool di primo livello: il modello vede solo execute_code e raggiunge gli altri tool scrivendo call_tool("nome", ...) nel programma Python. L’approvazione, se richiesta, si applica all’intero blocco di codice, non alle singole chiamate interne.La regola pratica è chiara: i tool puri e sicuri (lookup dati, calcoli, chiamate read-only) vanno passati al provider, così il modello li può comporre in un unico turno. I tool con side effect (invio email, scrittura su sistemi in produzione, transazioni economiche) vanno tenuti sull’agente direttamente con approval_mode="always_require", così il modello li deve invocare esplicitamente uno per uno.Quando conviene usare CodeActCodeAct non è la soluzione giusta per ogni agente. I benefici massimi si ottengono con task che coinvolgono molte operazioni concatenate e chainabili: data wrangling, generazione report, lookup multipli, calcoli intermedi. Se il task dell’agente si risolve quasi sempre con una o due chiamate a tool, il guadagno è marginale.È anche importante considerare che il codice Python generato dal modello deve essere revisionabile: uno dei vantaggi collaterali di CodeAct è che l’intero piano dell’agente è concentrato in un singolo blocco di codice leggibile e auditabile, invece di essere distribuito su una catena di messaggi di tool-call.ConclusioneCodeAct con Hyperlight rappresenta un’evoluzione pragmatica nell’architettura degli agenti AI su .NET: meno turni, meno token, stessa qualità. Il pattern è disponibile oggi nel pacchetto alpha agent-framework-hyperlight, pronto per essere sperimentato su workload interni prima di adottarlo in produzione. Chi sta già usando Agent Framework e si trova a costruire pipeline di tool-calling complesse troverà probabilmente il guadagno di latenza immediato e concreto.Fonte: CodeAct in Agent Framework: Faster Agents with Fewer Model Turns – Microsoft Dev Blogs, 23 aprile 2026]]></description><link>https://forum.androidiani.net/topic/15e2400c-327b-4c3d-b59f-dc42f91dbc68/codeact-e-hyperlight-agenti-ai-piu-veloci-con-meno-chiamate-al-modello-nel-.net-agent-framework</link><guid isPermaLink="true">https://forum.androidiani.net/topic/15e2400c-327b-4c3d-b59f-dc42f91dbc68/codeact-e-hyperlight-agenti-ai-piu-veloci-con-meno-chiamate-al-modello-nel-.net-agent-framework</guid><dc:creator><![CDATA[blog@spcnet.it]]></dc:creator><pubDate>Mon, 27 Apr 2026 13:55:45 GMT</pubDate></item><item><title><![CDATA[State Pattern in C#: guida decisionale con esempi pratici]]></title><description><![CDATA[Gli oggetti cambiano comportamento in base al loro stato interno continuamente. Un documento passa da bozza a revisione a pubblicato. Un personaggio di un gioco alterna tra inattivo, in corsa e in attacco. Un pagamento transita da pending ad autorizzato a catturato. La domanda quando usare lo State Pattern in C# emerge nel momento in cui la logica condizionale inizia a ramificarsi sullo stesso campo stato in metodo dopo metodo — e ogni nuovo stato obbliga a toccare più file.In questo articolo troverete una guida decisionale pratica per il pattern State in C#: quando merita davvero il suo posto e quando soluzioni più semplici — enum o flag booleani — bastano e avanzano.Cos’è lo State Pattern e cosa fa davveroLo State Pattern consente a un oggetto di cambiare il proprio comportamento quando il suo stato interno cambia. Dall’esterno sembra che l’oggetto abbia cambiato classe. Invece di sparpagliare istruzioni if e switch in ogni metodo che dipende dallo stato corrente, si incapsula il comportamento di ciascuno stato in una propria classe. L’oggetto delega al state object attivo in quel momento.La struttura coinvolge tre ruoli:Context — mantiene un riferimento allo state object corrente e vi delega il comportamentoState interface — dichiara i metodi che ogni stato deve implementareConcrete state classes — implementano l’interfaccia con il comportamento specifico del loro statoQuando avviene una transizione, il context sostituisce il riferimento al proprio state object. Questo approccio elimina i blocchi condizionali che crescono ogni volta che si aggiunge uno stato nuovo.Segnali che indicano che avete bisogno dello State PatternNon ogni oggetto con un campo status ha bisogno dello State Pattern. Tuttavia certi code smell sono segnali forti che il pattern ripulirà il design.La logica condizionale si ramifica sullo stesso stato ovunqueQuesto è il trigger principale. Quando vedete lo stesso switch o if-else che controlla _status in tre, quattro o dieci metodi diversi, il vostro oggetto sta gestendo le transizioni di stato nel modo più difficile. Ogni nuovo stato significa toccare ciascuno di quei metodi.Avete regole di transizione complesseSe le transizioni valide dipendono dallo stato attuale — e alcune azioni devono lanciare eccezioni o essere semplicemente ignorate a seconda di dove ci si trova — il pattern State rende queste regole esplicite invece di affogarle in condizionali.Ogni stato ha un comportamento distintoQuando lo stato influenza come si comporta un metodo, non soltanto se eseguirlo, il pattern vale l’investimento. Ciascuna classe stato diventa un luogo coeso che racchiude tutto il comportamento per quella condizione.Scenario 1: gestione degli ordini (comportamento condizionale complesso)Ecco un esempio di elaborazione ordini con lo State Pattern:public interface IOrderState {     void Submit(OrderContext context);     void Cancel(OrderContext context);     void Ship(OrderContext context);     void Deliver(OrderContext context); }  public sealed class OrderContext {     public IOrderState CurrentState { get; private set; }     public string OrderId { get; }      public OrderContext(string orderId)     {         OrderId = orderId;         CurrentState = new PendingState();     }      public void TransitionTo(IOrderState state)     {         Console.WriteLine(             $"Order {OrderId}: " +             $"{CurrentState.GetType().Name} -&gt; " +             $"{state.GetType().Name}");         CurrentState = state;     }      public void Submit() =&gt; CurrentState.Submit(this);     public void Cancel() =&gt; CurrentState.Cancel(this);     public void Ship()   =&gt; CurrentState.Ship(this);     public void Deliver() =&gt; CurrentState.Deliver(this); }  public sealed class PendingState : IOrderState {     public void Submit(OrderContext context)     {         Console.WriteLine("Ordine inviato per elaborazione.");         context.TransitionTo(new ProcessingState());     }      public void Cancel(OrderContext context)     {         Console.WriteLine("Ordine annullato prima dell'invio.");         context.TransitionTo(new CancelledState());     }      public void Ship(OrderContext context) =&gt;         throw new InvalidOperationException("Impossibile spedire un ordine in attesa.");      public void Deliver(OrderContext context) =&gt;         throw new InvalidOperationException("Impossibile consegnare un ordine in attesa."); }Notate come PendingState sappia esattamente cosa fare (o non fare) per ogni azione. Non c’è nessuno switch nello stato: il polimorfismo gestisce tutto.Scenario 2: workflow e gestione dei processiUn caso d’uso classico è la gestione di una domanda con audit trail integrato:public interface IApplicationState {     string StatusName { get; }     void Review(ApplicationContext context);     void Approve(ApplicationContext context);     void Reject(ApplicationContext context);     void RequestInfo(ApplicationContext context); }  public sealed class ApplicationContext {     public IApplicationState CurrentState { get; private set; }     public string ApplicantName { get; }     public List&lt;string&gt; AuditLog { get; } = new();      public ApplicationContext(string applicantName)     {         ApplicantName = applicantName;         CurrentState = new SubmittedState();         Log("Domanda inviata");     }      public void TransitionTo(IApplicationState state)     {         Log($"Transizione a {state.StatusName}");         CurrentState = state;     }      public void Log(string message) =&gt;         AuditLog.Add($"[{DateTime.UtcNow:u}] {message}"); }L’audit trail viene aggiornato automaticamente a ogni transizione, senza duplicazione di codice nei metodi chiamanti.Scenario 3: gestione dello stato di un personaggio in un giocoI giochi sono un esempio naturale: un personaggio che alterna tra IdleState, RunningState, AttackingState e DyingState beneficia enormemente di questo pattern, poiché ciascuno stato ha logica di input e di update completamente diversa.Quando NON usare lo State PatternIl pattern non è sempre la risposta giusta. Evitate di applicarlo nei seguenti casi:Stati booleani semplici: se l’oggetto ha solo attivo e inattivo, un campo booleano è più chiaro e diretto.Pochi stati senza transizioni significative: se avete due o tre stati con poco comportamento differenziato, gli enum bastano.La logica di transizione è esterna all’oggetto: se le decisioni di cambio stato appartengono a un orchestratore esterno, il pattern State aggiunge complessità senza benefici.State Pattern vs alternative: quando scegliere cosaUn enum con un switch è la scelta giusta quando gli stati sono pochi, stabili e il comportamento differisce solo su una o due dimensioni. Appena gli stati crescono, il comportamento diverge significativamente per metodo, o le transizioni diventano complesse, è il momento di passare al pattern State.Il pattern State non è la stessa cosa del pattern Strategy: Strategy cambia l’algoritmo usato per una singola operazione, mentre State cambia il comportamento complessivo dell’oggetto al variare della condizione interna. Possono tuttavia lavorare insieme: una transizione di stato può emettere eventi che degli Observer gestiscono.Integrazione con Dependency InjectionUna domanda comune è se il pattern State si integri bene con la DI di ASP.NET Core. La risposta è sì, con qualche accorgimento: le classi stato concrete possono essere registrate nel contenitore DI, ma è consigliabile usare factory o ActivatorUtilities.CreateInstance per creare le istanze in modo da evitare cicli nel contenitore.ConclusioneLo State Pattern in C# risolve un problema preciso: oggetti il cui comportamento cambia radicalmente al variare dello stato interno, con transizioni complesse e comportamento specifico per stato. Prima di applicarlo, fate questa verifica rapida: contate quante volte controllate lo stesso campo di stato in metodi diversi. Se la risposta supera tre o quattro, probabilmente il pattern vi risparmierà mesi di manutenzione futura.La regola d’oro rimane: preferite sempre la soluzione più semplice che risolve il problema. Un enum con uno switch è più leggibile di una gerarchia di classi per casi banali. Ma quando la complessità cresce, lo State Pattern offre un’architettura che scala senza sforzo.Fonte originale: When to Use State Pattern in C#: Decision Guide with Examples — Dev Leader]]></description><link>https://forum.androidiani.net/topic/8b11c7c5-9756-4b50-acf6-984e1c99b5ae/state-pattern-in-c-guida-decisionale-con-esempi-pratici</link><guid isPermaLink="true">https://forum.androidiani.net/topic/8b11c7c5-9756-4b50-acf6-984e1c99b5ae/state-pattern-in-c-guida-decisionale-con-esempi-pratici</guid><dc:creator><![CDATA[blog@spcnet.it]]></dc:creator><pubDate>Sun, 26 Apr 2026 18:13:43 GMT</pubDate></item><item><title><![CDATA[Classificazione documenti in C# senza AI: approccio deterministico, spiegabile e pronto per la produzione]]></title><description><![CDATA[Classificare automaticamente i documenti aziendali è uno di quei problemi che, a prima vista, sembra un caso d’uso ideale per i modelli AI. Ma in ambienti di produzione, la stabilità, la tracciabilità e la prevedibilità del comportamento spesso contano più della flessibilità. In questo articolo vediamo come implementare un classificatore di documenti rule-based, ponderato e completamente spiegabile in C# .NET, senza toccare un singolo modello di machine learning.Perché non partire dall’AI?I modelli AI per la classificazione di testo sono potenti, ma introducono una serie di criticità in contesti enterprise:Non-determinismo: lo stesso documento può ricevere classificazioni diverse a seconda della versione del modello, del wording del prompt o di aggiornamenti interni del provider.Opacità: spiegare a un responsabile compliance perché il modello ha classificato un contratto come “fattura” è praticamente impossibile.Dipendenza da pipeline di dati: aggiornare il classificatore richiede raccolta di dati, rietichettatura, riaddestramento e deployment.Un approccio deterministico e rule-based risolve tutti e tre i problemi: stesso input, stesso output, sempre. Ogni decisione è tracciabile e modificabile senza toccare il codice, solo aggiornando un file di configurazione JSON.Architettura del classificatoreIl sistema segue una pipeline chiara:Caricamento dei profili di classificazione da file JSONApertura del documento .docx con TX Text ControlEstrazione del testo da corpo, intestazioni e piè di paginaRilevamento delle regioni strutturali (titolo, heading, corpo, header, footer)Matching delle regole per categoria con strategie configurabiliCalcolo degli score ponderati per categoriaRestituzione della categoria vincente con confidence score e spiegazione dettagliataL’elemento chiave è che tutta la logica di classificazione vive nel file JSON, non nel codice. Questo significa che un domain expert (non un developer) può modificare e migliorare il classificatore semplicemente editando la configurazione.Il file di configurazioneOgni categoria è descritta da un insieme di regole. Ogni regola specifica:term: il termine o la frase da cercareweight: il peso del contributo di questa regola allo scorematchMode: la strategia di matching (Phrase, WholeWord, Contains)strength: la forza del segnale (Strong, Weak)Esempio per la categoria “Resume”:{   "name": "Resume",   "rules": [     {       "term": "work experience",       "weight": 3.0,       "matchMode": "Phrase",       "strength": "Strong"     },     {       "term": "email",       "weight": 1.0,       "matchMode": "WholeWord",       "strength": "Weak"     }   ] }La distinzione tra segnali forti e deboli è cruciale. “Work experience” in un documento è un indicatore molto specifico di un CV, mentre “email” può apparire praticamente ovunque e deve pesare di meno. Questa granularità evita i falsi positivi che affliggono i classifier naïve basati su semplice keyword counting.Estrazione strutturata con TX Text ControlIl classificatore non tratta il documento come un blocco di testo piatto. Usando TX Text Control .NET Server, estrae il contenuto per regioni strutturali:using var textControl = new ServerTextControl(); textControl.Create(); textControl.Load(docxPath, StreamType.WordprocessingML);Vengono estratti separatamente:textControl.Paragraphs → testo del corpotextControl.Sections → HeadersAndFooters → intestazioni e piè di pagina di ogni sezioneQuesta distinzione è fondamentale: nelle fatture, i termini identificativi come “FATTURA N.” appaiono tipicamente all’inizio del documento o nel titolo. Nei report, il tipo di documento è spesso incorporato nell’header. Ignorare queste regioni significherebbe perdere segnali classificatori di primo livello.Structure Awareness: non tutto il testo vale ugualeIl miglioramento più significativo rispetto al semplice keyword matching è la consapevolezza della struttura. Il classificatore assegna pesi diversi agli stessi termini a seconda della regione in cui appaiono:Un termine nel titolo del documento ha peso massimo: è quasi certamente indicativo del tipo di documentoUn termine in un heading H1/H2 ha peso altoLo stesso termine nel corpo del documento ha peso standardNel footer (tipicamente template boilerplate) il peso è ridottoQuesto approccio riflette come un essere umano leggerebbe effettivamente il documento: prima si guarda il titolo, poi le intestazioni principali, infine il corpo.Scoring, confidence e spiegabilitàAl termine dell’analisi, il sistema restituisce non solo la categoria vincente, ma anche:Il confidence score (rapporto tra lo score della categoria vincente e la somma degli score di tutte le categorie)Una spiegazione dettagliata: quali regole hanno fatto match, in quale regione, con quale pesoQuesta tracciabilità è essenziale per scenari di audit e compliance. Se un documento viene classificato erroneamente, il problema è sempre identificabile e correggibile: si modifica la regola nel JSON e si riclassifica. Nessun riaddestramento, nessuna nuova pipeline di dati.Quando usare questo approccio (e quando no)L’approccio deterministico è ottimale quando:Il set di categorie è definito e stabile (fatture, contratti, report, CV, ecc.)La compliance e l’auditability sono requisiti primariIl volume di documenti è alto e la velocità di classificazione è criticaNon si dispone di dataset etichettati sufficienti per addestrare un modelloDove l’AI rimane superiore è nei casi con categorie ambigue, linguaggio naturale molto variabile, o quando il set di categorie evolve rapidamente e non si vuole aggiornare manualmente le regole. Un’architettura ibrida – classificazione rule-based come primo filtro, AI solo per i casi borderline – è spesso la soluzione migliore in produzione.ConclusioneLa classificazione documentale senza AI non è un’alternativa di ripiego: è una scelta ingegneristica deliberata per sistemi che richiedono stabilità, spiegabilità e controllo. Il pattern rule-based, ponderato e configuration-driven descritto in questo articolo è già in uso in ambienti di produzione enterprise e offre vantaggi concreti in termini di manutenibilità e trasparenza.Se il vostro stack include la gestione di documenti .docx in C#, questo approccio vale la pena di essere valutato prima di introdurre la complessità di un modello ML.Fonte: Document Classification Without AI: Deterministic, Explainable, and Built for Production in C# .NET – Bjoern Meyer, TX Text Control Blog]]></description><link>https://forum.androidiani.net/topic/0aa5c73a-f7b6-4d82-9d50-53d0272001cd/classificazione-documenti-in-c-senza-ai-approccio-deterministico-spiegabile-e-pronto-per-la-produzione</link><guid isPermaLink="true">https://forum.androidiani.net/topic/0aa5c73a-f7b6-4d82-9d50-53d0272001cd/classificazione-documenti-in-c-senza-ai-approccio-deterministico-spiegabile-e-pronto-per-la-produzione</guid><dc:creator><![CDATA[blog@spcnet.it]]></dc:creator><pubDate>Sun, 26 Apr 2026 10:08:16 GMT</pubDate></item><item><title><![CDATA[.NET 10 su Ubuntu 26.04 “Resolute Raccoon”: installazione, container e Native AOT]]></title><description><![CDATA[Ubuntu 26.04 LTS, nome in codice Resolute Raccoon, è disponibile e porta con sé una delle novità più attese per gli sviluppatori .NET su Linux: .NET 10 è il runtime ufficiale incluso nel repository standard. In questo articolo esploriamo come installare .NET 10, come aggiornare le immagini container esistenti e come sfruttare Native AOT per ottenere binari ultra-compatti con avvio in pochi millisecondi.Perché .NET e Ubuntu insiemeLa collaborazione tra Microsoft e Canonical non è nuova: ogni nuovo Ubuntu LTS porta con sé l’ultimo .NET LTS come toolchain ufficialmente supportata. Ubuntu 26.04 non fa eccezione: .NET 10 è direttamente installabile via APT senza configurare PPA aggiuntive. Per chi lavora in ambienti enterprise o vuole un’infrastruttura stabile e aggiornabile tramite il gestore pacchetti di sistema, questo è un vantaggio non trascurabile.È comunque possibile installare anche .NET 8 e .NET 9 tramite PPA dedicata, per chi ha applicazioni su versioni precedenti.Installazione rapidaL’installazione di .NET 10 su Ubuntu 26.04 è immediata:sudo apt update sudo apt install dotnet-sdk-10.0Nessun repository aggiuntivo, nessuna chiave GPG da configurare manualmente. Il package manager si occupa di tutto. Per verificare la versione installata:dotnet --version # 10.0.105Eseguire C# direttamente da stdinUna delle funzionalità meno note ma molto utile per script e automazione è la possibilità di passare codice C# direttamente a dotnet run via stdin, usando i file-based apps:dotnet run - &lt;&lt; 'EOF' using System.Runtime.InteropServices; Console.WriteLine($"Hello {RuntimeInformation.OSDescription} from .NET {RuntimeInformation.FrameworkDescription}"); EOF # Hello Ubuntu Resolute Raccoon from .NET .NET 10.0.5Questo pattern è particolarmente utile negli script di sistema e nei workflow CI/CD dove si vuole eseguire logica .NET senza creare un progetto completo.Novità rilevanti di Ubuntu 26.04 per .NETUbuntu 26.04 introduce tre cambiamenti che impattano direttamente gli stack .NET in produzione:Linux 7.0: il team .NET avvierà test su questo kernel non appena disponibili VM nel laboratorio. Le prime build sono già in CI.Post-Quantum Cryptography: Ubuntu 26.04 spinge su questo fronte e .NET 10 include già il supporto agli algoritmi post-quantum, quindi la compatibilità è garantita.Rimozione di cgroup v1: nessun problema per .NET, che supporta cgroup v2 da diversi anni. Tuttavia, chi usa container con immagini molto datate o configurazioni cgroup v1 dovrà verificare la compatibilità.Container: aggiornare da noble a resoluteLe immagini ufficiali per .NET 10 sono già disponibili con il tag resolute. Aggiornare un Dockerfile esistente è questione di un semplice sed:sed -i "s/noble/resolute/g" Dockerfile.chiseledEsempio di build e avvio con limiti di risorse:docker build --pull -t aspnetapp -f Dockerfile.chiseled . docker run --rm -it -p 8000:8080 -m 50mb --cpus .5 aspnetappLe varianti Chiseled (immagini minimali senza shell e strumenti non necessari) sono disponibili anche per resolute, con le stesse caratteristiche di sicurezza della versione noble.Nota importante: i container ereditano il kernel dell’host. Un container resolute su un host Ubuntu 24.04 userà il kernel 6.x dell’host, non Linux 7.0. Tenere presente questa distinzione in fase di planning.Native AOT: binari compatti e avvio in 3msNative AOT (NAOT) è una delle funzionalità più potenti di .NET 10 per scenari server e CLI. Su Ubuntu 26.04, il pacchetto dedicato è dotnet-sdk-aot-10.0:apt install -y dotnet-sdk-aot-10.0 clangPubblicando una semplice applicazione console come NAOT si ottiene un binario da circa 1.4 MB, pronto all’esecuzione senza runtime installato:dotnet publish app.cs du -h artifacts/app/* # 1.4M  artifacts/app/app # 3.0M  artifacts/app/app.dbgLe performance di avvio sono notevoli:time ./artifacts/app/app # real 0m0.003s3 millisecondi. Per confronto, un’applicazione .NET classica JIT può richiedere 100-500ms di warm-up in scenari tipici. Native AOT è la scelta ideale per CLI tools, Lambda functions, microservizi ad avvio freddo e sidecar container.Per applicazioni web, lo stesso approccio funziona con &lt;PublishAot&gt;true&lt;/PublishAot&gt; nel .csproj:dotnet publish # Produce: releasesapi (13MB) + releasesapi.dbg (32MB)Considerazioni pratiche per il team di sviluppoPer chi gestisce pipeline CI/CD con Ubuntu, questo rilascio semplifica notevolmente la gestione delle dipendenze: non è più necessario configurare feed Microsoft o repository aggiuntivi per .NET 10. L’intero stack è aggiornabile tramite apt upgrade come qualsiasi altro pacchetto di sistema.Per i team che usano container come base di sviluppo standardizzata, aggiornare il tag da -noble a -resolute nei Dockerfile è sufficiente per passare alla nuova LTS. È comunque raccomandato verificare la compatibilità con la propria configurazione cgroup se si usano orchestratori come Kubernetes con configurazioni custom.ConclusioneUbuntu 26.04 LTS consolida ulteriormente la posizione di Linux come piattaforma di prima classe per .NET. L’integrazione diretta nel repository APT, il supporto alle immagini Chiseled, la compatibilità post-quantum e le performance eccezionali di Native AOT fanno di questo rilascio un upgrade significativo per chiunque sviluppi o distribuisca applicazioni .NET su Linux.Fonte: What’s new for .NET in Ubuntu 26.04 – Richard Lander, Microsoft .NET Blog]]></description><link>https://forum.androidiani.net/topic/f98e3a50-3d23-4e5b-b49e-189e51e14dc9/.net-10-su-ubuntu-26.04-resolute-raccoon-installazione-container-e-native-aot</link><guid isPermaLink="true">https://forum.androidiani.net/topic/f98e3a50-3d23-4e5b-b49e-189e51e14dc9/.net-10-su-ubuntu-26.04-resolute-raccoon-installazione-container-e-native-aot</guid><dc:creator><![CDATA[blog@spcnet.it]]></dc:creator><pubDate>Sat, 25 Apr 2026 19:12:54 GMT</pubDate></item><item><title><![CDATA[Addio byte[]: allocazioni a costo zero in .NET Framework con  ReadOnlySpan]]></title><description><![CDATA[Uno dei pattern di ottimizzazione più semplici e meno conosciuti nel mondo .NET è la sostituzione dei campi static readonly byte[] con proprietà static ReadOnlySpan&lt;byte&gt;. Andrew Lock, noto per le sue analisi approfondite su ASP.NET Core e il runtime, ha pubblicato un articolo che conferma un dettaglio fondamentale: questa tecnica funziona anche su .NET Framework, basta il pacchetto NuGet System.Memory. Zero allocazioni, zero costo di startup, nessuna pressione sul garbage collector.Il problema: allocazioni “gratuite” che non lo sonoConsideriamo un pattern che troviamo in quasi tutte le librerie che manipolano dati binari: signature di file, magic number, header fissi, tabelle di lookup. Tipicamente si scrive:public static class MyStaticData {     private static readonly byte[] ByteField = new byte[] { 1, 2, 3, 4 }; }Sembra innocuo: un singolo array, allocato una volta sola al caricamento del tipo. Ma in un processo con migliaia di tipi simili — pensiamo a un parser di formati immagine, a una libreria di crittografia, a un framework web — queste allocazioni si sommano. Ogni array è un oggetto gestito: richiede header, richiede tracciamento GC, occupa spazio sulla Gen 2 (perché sopravvive per sempre) e aumenta i tempi di startup.La soluzione: ReadOnlySpan&lt;byte&gt; come proprietàLa trasformazione è quasi meccanica:public static class MyStaticData {     private static ReadOnlySpan&lt;byte&gt; ReadOnlySpanProp =&gt; new byte[] { 1, 2, 3, 4 }; }Sintatticamente sembra che stiamo allocando un array ogni volta che accediamo alla proprietà. In realtà è esattamente il contrario: il compilatore C# riconosce questo pattern e incorpora i byte direttamente nei metadati dell’assembly, costruendo lo span con un puntatore a quei dati. Non viene mai eseguito newarr.L’IL generato mostra chiaramente la magia:IL_0000: ldsflda      int32 '&lt;PrivateImplementationDetails&gt;'::'...' IL_0005: ldc.i4.4 IL_0006: newobj       instance void valuetype [System.Memory]System.ReadOnlySpan`1&lt;unsigned int8&gt;::.ctor(void*, int32) IL_000b: retI dati vivono in una sezione di sola lettura dell’assembly; lo span viene costruito on-the-fly con pointer + length. È essenzialmente gratuito.Letterali UTF-8: lo stesso trucco, più ergonomicoA partire da C# 11 (.NET 7), la stessa ottimizzazione si ottiene con i letterali UTF-8:private static ReadOnlySpan&lt;byte&gt; Utf8Hello =&gt; "Hello world"u8;Il suffisso u8 istruisce il compilatore a codificare la stringa direttamente in UTF-8 nell’assembly. Molto utile per header HTTP, prefissi di protocollo, marker di formato binari — tutti casi in cui storicamente si manteneva una byte[] statica generata da Encoding.UTF8.GetBytes.I vincoli da rispettareL’ottimizzazione non si applica in modo uniforme. Vale solo per i tipi a byte singolo:byte[]sbyte[]bool[]Per gli altri tipi primitivi (int, long, double…) entra in gioco l’endianness: su .NET 7 e successivi c’è RuntimeHelpers.CreateSpan&lt;T&gt;() che la gestisce in modo trasparente, ma su .NET Framework il compilatore emette codice che cache l’array in un campo statico alla prima chiamata. Ancora efficiente, ma non zero-alloc.Il secondo vincolo è che tutti i valori devono essere costanti a compile-time:// Anti-pattern: alloca a ogni accesso private static readonly byte One = 1; private static ReadOnlySpan&lt;byte&gt; Bad =&gt; new byte[] { One, 2, 3, 4 };Qui One è un campo, non una costante, quindi il compilatore deve costruire l’array a runtime. La differenza tra const byte e static readonly byte diventa improvvisamente importante.Il terzo vincolo è usare ReadOnlySpan&lt;T&gt;, mai Span&lt;T&gt;:// Sbagliato: alloca un array mutabile a ogni accesso private static Span&lt;byte&gt; MutSpan =&gt; new byte[] { 1, 2, 3, 4 };Uno Span&lt;byte&gt; potrebbe essere scritto, e modificare dati immutabili condivisi sarebbe catastrofico. Il compilatore quindi non applica l’ottimizzazione.Il supporto su .NET FrameworkQuesta è la parte più interessante: il trucco funziona su .NET Framework 4.6.2+ semplicemente referenziando il pacchetto System.Memory:&lt;ItemGroup&gt;   &lt;PackageReference Include="System.Memory" Version="4.6.3" /&gt; &lt;/ItemGroup&gt;La ragione è che l’ottimizzazione è una feature del compilatore, non del runtime: serve solo che ReadOnlySpan&lt;T&gt; esista come tipo, e il pacchetto System.Memory lo fornisce. Chi mantiene librerie multi-target può quindi applicare questa ottimizzazione senza creare codice condizionale #if NET6_0_OR_GREATER.Collection expressions: la rete di sicurezzaSu C# 12 e successivi le collection expressions offrono protezione a compile-time:// Compila e non alloca private static ReadOnlySpan&lt;byte&gt; Safe =&gt; [1, 2, 3, 4];  // Errore CS9203 — il compilatore rifiuta private static Span&lt;byte&gt; Dangerous =&gt; [1, 2, 3, 4];L’errore CS9203 è un salvavita: impedisce di assegnare una collection expression a un tipo Span&lt;T&gt; in contesti static, perché il risultato sarebbe condivisibile e mutabile. Su .NET Framework o su versioni di C# precedenti questa protezione non esiste, quindi serve attenzione in fase di code review.Quando applicarla nel codice realeLe candidate ideali sono costanti binarie che vivono in campi static readonly byte[]: magic number (PNG, ZIP, PDF), prefissi protocollari, tabelle di sostituzione, chiavi di test fisse, certificati embedded. Il refactoring è meccanico e non cambia l’API pubblica della classe se la visibility è private.Attenzione invece ai metodi che accettano byte[]: non possiamo passare uno ReadOnlySpan&lt;byte&gt; a un’API che richiede un array. In questi casi la scelta è tra riscrivere il consumer per accettare ReadOnlySpan&lt;byte&gt; (preferibile) o mantenere l’array tradizionale. Molte API del BCL sono già state aggiornate negli ultimi anni: Stream.Write, HashAlgorithm.ComputeHash, Encoding.GetString accettano tutti ReadOnlySpan&lt;byte&gt; in overload moderni.ConclusioneCambiare static readonly byte[] in static ReadOnlySpan&lt;byte&gt; =&gt; è uno di quei refactoring che riducono allocazioni e startup con una modifica locale a costo zero. Funziona anche su .NET Framework, quindi vale la pena considerarla durante la manutenzione di codice legacy — un punto che spesso sfugge perché l’ecosistema associa Span&lt;T&gt; esclusivamente a .NET moderno.Fonte: Removing byte[] allocations in .NET Framework using ReadOnlySpan&lt;T&gt; di Andrew Lock.]]></description><link>https://forum.androidiani.net/topic/9882bbb4-d010-4a5e-a02d-eca46c97ec7b/addio-byte-allocazioni-a-costo-zero-in-.net-framework-con-readonlyspan</link><guid isPermaLink="true">https://forum.androidiani.net/topic/9882bbb4-d010-4a5e-a02d-eca46c97ec7b/addio-byte-allocazioni-a-costo-zero-in-.net-framework-con-readonlyspan</guid><dc:creator><![CDATA[blog@spcnet.it]]></dc:creator><pubDate>Thu, 23 Apr 2026 15:01:47 GMT</pubDate></item><item><title><![CDATA[Creare addon nativi per Node.js con .NET Native AOT: addio a Python e node-gyp]]></title><description><![CDATA[Da sempre, creare addon nativi per Node.js significava entrare nel mondo di C++ e node-gyp, con la necessità di installare Python, Visual Studio Build Tools e una serie di dipendenze che trasformavano il setup dell’ambiente in un’impresa. Il team di C# Dev Kit di Microsoft ha trovato una soluzione elegante: usare .NET Native AOT per produrre librerie condivise compatibili con l’interfaccia N-API di Node.js, scritte interamente in C#.In questo articolo vediamo come funziona questa tecnica, analizzando la struttura del progetto, il meccanismo di interop e i punti critici da tenere d’occhio in produzione.Perché Node.js supporta addon scritti in qualsiasi linguaggioUn addon nativo per Node.js è semplicemente una libreria condivisa (.dll su Windows, .so su Linux, .dylib su macOS) che esporta un punto di ingresso preciso: la funzione napi_register_module_v1. Node.js carica la libreria, chiama questa funzione e da quel momento il modulo è disponibile per JavaScript.L’interfaccia che rende tutto questo possibile è N-API (Node-API), una API C stabile e ABI-compatibile tra le versioni di Node.js. Questo significa che qualsiasi linguaggio in grado di produrre una shared library ed esportare una funzione C può diventare un addon Node.js — incluso C# compilato con Native AOT.Configurazione del progetto .NETIl file di progetto è sorprendentemente minimale:&lt;Project Sdk="Microsoft.NET.Sdk"&gt;
  &lt;PropertyGroup&gt;
    &lt;TargetFramework&gt;net10.0&lt;/TargetFramework&gt;
    &lt;PublishAot&gt;true&lt;/PublishAot&gt;
    &lt;AllowUnsafeBlocks&gt;true&lt;/AllowUnsafeBlocks&gt;
  &lt;/PropertyGroup&gt;
&lt;/Project&gt;Due impostazioni chiave:PublishAot: abilita la compilazione Ahead-of-Time, producendo una shared library nativa invece di un assembly IL.AllowUnsafeBlocks: necessario per l’interop con N-API tramite function pointer e tipi non gestiti.Il punto di ingresso del moduloL’entry point usa l’attributo [UnmanagedCallersOnly], che istruisce il compilatore a generare una funzione C-callable con la firma esatta attesa da Node.js:[UnmanagedCallersOnly(
    EntryPoint = "napi_register_module_v1",
    CallConvs = [typeof(CallConvCdecl)])]
public static nint Init(nint env, nint exports)
{
    // Registrazione delle funzioni esposte
    return exports;
}Il tipo nint (native-sized integer) rappresenta gli handle opachi che N-API usa per riferirsi agli oggetti JavaScript. Non si tratta di puntatori diretti a memoria, ma di token gestiti dall’engine V8 tramite N-API.Risoluzione delle funzioni N-API a runtimeLe funzioni N-API (come napi_create_string_utf8 o napi_get_cb_info) sono esportate direttamente da node.exe, non da una DLL separata. Per fare in modo che P/Invoke le risolva correttamente, si registra un custom resolver:private static void Initialize()
{
    NativeLibrary.SetDllImportResolver(
        System.Reflection.Assembly.GetExecutingAssembly(),
        ResolveDllImport);
}

private static nint ResolveDllImport(string libraryName, Assembly assembly, DllImportSearchPath? searchPath)
{
    if (libraryName == "node")
        return NativeLibrary.GetMainProgramHandle();
    return IntPtr.Zero;
}Questo permette di dichiarare le importazioni P/Invoke con [LibraryImport("node")] e averle risolte contro il processo host a runtime.Marshalling delle stringhe UTF-8Uno dei punti più delicati è la conversione tra stringhe JavaScript (UTF-16 internamente in V8, UTF-8 via N-API) e stringhe .NET. La strategia ottimale prevede:Uso dello stack per stringhe piccole (≤512 byte) tramite stackallocUso di ArrayPool&lt;byte&gt; per stringhe più grandi, evitando allocazioni sull’heapprivate static string GetStringArg(nint env, nint info, int argIndex)
{
    // Recupera l'handle dell'argomento
    nint value = GetArgument(env, info, argIndex);
    
    // Prima chiamata: ottieni la dimensione necessaria
    nuint byteCount;
    napi_get_value_string_utf8(env, value, null, 0, out byteCount);
    
    // Allocazione efficiente in base alla dimensione
    if (byteCount &lt;= 512)
    {
        Span&lt;byte&gt; buffer = stackalloc byte[(int)byteCount + 1];
        napi_get_value_string_utf8(env, value, buffer, (nuint)buffer.Length, out _);
        return Encoding.UTF8.GetString(buffer[..^1]);
    }
    else
    {
        byte[] buffer = ArrayPool&lt;byte&gt;.Shared.Rent((int)byteCount + 1);
        try
        {
            napi_get_value_string_utf8(env, value, buffer, (nuint)buffer.Length, out _);
            return Encoding.UTF8.GetString(buffer, 0, (int)byteCount);
        }
        finally
        {
            ArrayPool&lt;byte&gt;.Shared.Return(buffer);
        }
    }
}Implementazione di una funzione reale: lettura dal RegistryL’esempio concreto mostrato dal team di Microsoft è un lettore del Windows Registry, che sostituisce il precedente addon C++:private static nint ReadStringValue(nint env, nint info)
{
    try
    {
        var keyPath = GetStringArg(env, info, 0);
        var valueName = GetStringArg(env, info, 1);
        
        using var key = Registry.CurrentUser.OpenSubKey(keyPath, writable: false);
        
        return key?.GetValue(valueName) is string value
            ? CreateString(env, value)
            : GetUndefined(env);
    }
    catch (Exception ex)
    {
        // CRITICO: le eccezioni non gestite in [UnmanagedCallersOnly] crashano il processo
        ThrowError(env, $"Registry read failed: {ex.Message}");
        return 0;
    }
}Attenzione: in un metodo [UnmanagedCallersOnly], le eccezioni non gestite provocano il crash dell’intero processo Node.js. Il pattern try/catch con ThrowError trasforma l’eccezione .NET in un errore JavaScript, mantenendo stabile il runtime.Integrazione con TypeScriptDopo dotnet publish, il file prodotto viene rinominato con estensione .node (convenzione Node.js) e caricato normalmente da TypeScript:interface RegistryAddon {
    readStringValue(keyPath: string, valueName: string): string | undefined;
}

const registry = require('./native/win32-x64/RegistryAddon.node') as RegistryAddon;

const sdkPath = registry.readStringValue(
    'SOFTWARE\\dotnet\\Setup\\InstalledVersions\\x64\\sdk',
    'InstallLocation'
);
console.log(`SDK installato in: ${sdkPath}`);Limiti e considerazioniQuesta tecnica ha un limite importante: Native AOT non supporta la cross-compilazione. Per ogni piattaforma target (Windows x64, Linux x64, macOS ARM64…) è necessario un ambiente di build separato. In pratica, questo si risolve con pipeline CI che eseguono la build su runner del sistema operativo corrispondente.Esiste anche un’alternativa di più alto livello, node-api-dotnet, che astrae molti dei dettagli mostrati qui e supporta scenari più complessi come l’esposizione di interi namespace .NET a JavaScript. L’approccio “thin wrapper” descritto in questo articolo è preferibile quando si vuole controllo totale e dipendenze minime.ConclusioniL’integrazione tra .NET Native AOT e N-API apre uno scenario interessante per i team che già lavorano con C# e devono interfacciarsi con l’ecosistema Node.js. Eliminare Python e node-gyp dal setup semplifica notevolmente l’ambiente di sviluppo e unifica le competenze necessarie intorno a un unico SDK.Il risultato è codice nativo con prestazioni paragonabili al C++, scritto con la produttività e la type safety di C# moderno, deployabile su Windows, Linux e macOS.Fonte: Writing Node.js addons with .NET Native AOT — Microsoft .NET Blog, Drew Noakes]]></description><link>https://forum.androidiani.net/topic/a0a8ba76-389e-4f9d-a652-0b5029500441/creare-addon-nativi-per-node.js-con-.net-native-aot-addio-a-python-e-node-gyp</link><guid isPermaLink="true">https://forum.androidiani.net/topic/a0a8ba76-389e-4f9d-a652-0b5029500441/creare-addon-nativi-per-node.js-con-.net-native-aot-addio-a-python-e-node-gyp</guid><dc:creator><![CDATA[blog@spcnet.it]]></dc:creator><pubDate>Wed, 22 Apr 2026 09:19:56 GMT</pubDate></item><item><title><![CDATA[Primary Constructor e Dependency Injection in C# 12: vantaggi, insidie e quando usarli]]></title><description><![CDATA[Con C# 12, Microsoft ha introdotto i primary constructors per tutte le classi e le struct — non solo per i record come in precedenza. Per chi lavora quotidianamente con ASP.NET Core e il pattern di dependency injection, questa feature merita attenzione: riduce il boilerplate in modo significativo, ma nasconde un'insidia che è bene conoscere prima di adottarla sistematicamente.Il problema: il boilerplate del costruttore classicoChi ha scritto servizi ASP.NET Core sa bene come appare il costruttore tradizionale con dependency injection:public class OrderService
{
    private readonly IOrderRepository _orderRepository;
    private readonly ILogger&lt;OrderService&gt; _logger;
    private readonly IEventBus _eventBus;
    private readonly IValidator&lt;Order&gt; _validator;

    public OrderService(
        IOrderRepository orderRepository,
        ILogger&lt;OrderService&gt; logger,
        IEventBus eventBus,
        IValidator&lt;Order&gt; validator)
    {
        _orderRepository = orderRepository;
        _logger = logger;
        _eventBus = eventBus;
        _validator = validator;
    }
}Per ogni dipendenza: una dichiarazione di campo, un parametro nel costruttore, un'assegnazione nel corpo. Con quattro dipendenze, sono già dodici righe di puro scaffolding. In un servizio con sei o sette dipendenze, il costruttore diventa il blocco di codice più lungo della classe — senza contenere logica.La soluzione con primary constructorsCon i primary constructors di C# 12, lo stesso servizio si scrive così:public class OrderService(
    IOrderRepository orderRepository,
    ILogger&lt;OrderService&gt; logger,
    IEventBus eventBus,
    IValidator&lt;Order&gt; validator)
{
    public async Task&lt;Order?&gt; GetOrderAsync(Guid id)
    {
        logger.LogInformation("Fetching order {OrderId}", id);
        return await orderRepository.GetByIdAsync(id);
    }

    public async Task PlaceOrderAsync(Order order)
    {
        await validator.ValidateAndThrowAsync(order);
        await orderRepository.SaveAsync(order);
        await eventBus.PublishAsync(new OrderPlacedEvent(order.Id));
    }
}I parametri del primary constructor diventano direttamente disponibili in tutta la classe senza dichiarazioni esplicite di campo. Il risultato è codice più snello, con meno rumore visivo e il focus immediato sulla logica di business.L'insidia della mutabilità: perché Milan Jovanović era inizialmente scetticoIl motivo di resistenza di molti sviluppatori esperti è legittimo: i parametri del primary constructor non sono campi readonly. Il compilatore non impedisce la riassegnazione accidentale:public class OrderService(IOrderRepository orderRepository)
{
    public void SomeMethod()
    {
        orderRepository = null!;  // Compila senza warning
    }
}Con i campi privati readonly tradizionali, questo codice causa un errore di compilazione. Con i primary constructors, il compilatore lo accetta silenziosamente. In un servizio DI dove le dipendenze non dovrebbero mai essere rimpiazzate a runtime, questo è un rischio concreto in team di grandi dimensioni.Mitigazione: assegnazione esplicita a readonly fieldQuando la garanzia di immutabilità è critica, si può assegnare esplicitamente il parametro a un campo readonly:public class CriticalService(IRepository repository)
{
    private readonly IRepository _repository = repository;

    // Da qui in poi si usa _repository, mai repository direttamente
}Questo reintroduce parte del boilerplate, ma mantiene la sintassi più compatta per la firma del costruttore e garantisce l'immutabilità a livello compilatore.Quando usare i primary constructorsLa valutazione pragmatica è che i primary constructors offrono il massimo vantaggio nelle classi di servizio DI tipiche di ASP.NET Core, dove i parametri vengono usati ma raramente riassegnati. In questi scenari, il rischio di mutabilità accidentale è basso e i benefici di leggibilità sono immediati.Vale la pena usarli anche per entity e value object dove i parametri di costruzione diventano proprietà read-only:public class Order(Guid customerId, Money total)
{
    public Guid Id { get; } = Guid.NewGuid();
    public Guid CustomerId { get; } = customerId;
    public Money Total { get; } = total;
    public DateTime CreatedAt { get; } = DateTime.UtcNow;
}Quando evitarliTre scenari in cui i primary constructors non sono la scelta giusta:Logica di validazione nel costruttore: se il costruttore deve eseguire guard clause o validazioni prima dell'assegnazione, il corpo esplicito del costruttore tradizionale è necessario.Multiple signature di costruttore: i primary constructors supportano una sola firma. Con overload multipli (es. per serializzazione), la sintassi tradizionale è l'unica opzione.Cinque o più dipendenze: se una classe richiede molte dipendenze, il problema non è la sintassi del costruttore ma il design della classe. Il segnale che suggerisce un refactoring verso interfacce più coese o il pattern Facade.Conclusione: adottarli con consapevolezzaI primary constructors di C# 12 non sono una rivoluzione, ma un'evoluzione pragmatica del linguaggio. Per la maggior parte delle classi di servizio in ASP.NET Core, il tradeoff è favorevole: meno boilerplate, codice più leggibile, rischio di mutabilità basso nel contesto DI. L'importante è conoscere il comportamento del compilatore e scegliere consapevolmente quando la garanzia di readonly è effettivamente necessaria.Come sempre con le feature di C#, il consiglio è adottarle dove migliorano la leggibilità del codice reale, non per seguire una moda sintattica.Fonte originale: Why I Switched to Primary Constructors for DI in C# di Milan Jovanović.]]></description><link>https://forum.androidiani.net/topic/ed9059e2-0ab3-4ef4-a625-36f90606443b/primary-constructor-e-dependency-injection-in-c-12-vantaggi-insidie-e-quando-usarli</link><guid isPermaLink="true">https://forum.androidiani.net/topic/ed9059e2-0ab3-4ef4-a625-36f90606443b/primary-constructor-e-dependency-injection-in-c-12-vantaggi-insidie-e-quando-usarli</guid><dc:creator><![CDATA[blog@spcnet.it]]></dc:creator><pubDate>Tue, 21 Apr 2026 13:28:35 GMT</pubDate></item><item><title><![CDATA[RAG in .NET con Semantic Kernel: le insidie che i tutorial non ti dicono]]></title><description><![CDATA[RAG — Retrieval-Augmented Generation — è diventato il pattern dominante per integrare conoscenza dominio-specifica nei modelli linguistici. Ma tra un tutorial “hello world” e un sistema RAG che funziona davvero in produzione c’è un abisso. Questo articolo esplora le insidie reali che i tutorial non affrontano, partendo da un’implementazione in .NET con Semantic Kernel.Il pipeline RAG in cinque fasiPrima di entrare nei dettagli critici, è utile avere una mappa mentale del pipeline completo. Un sistema RAG ben strutturato si articola in cinque fasi sequenziali:Ingestion — caricamento dei documenti sorgenteChunking — suddivisione in segmenti adatti all’embeddingEmbedding — conversione dei chunk in vettori numericiStorage — persistenza dei vettori in un vector storeRetrieval + Generation — ricerca dei chunk rilevanti e generazione della rispostaOgni fase nasconde decisioni non banali. Vediamo quelle che fanno davvero la differenza.Il chunking è la variabile più sottovalutataLa qualità del chunking influenza il retrieval più di qualsiasi altra scelta, incluso il modello di embedding. La maggior parte dei tutorial usa split naive basati su caratteri o token fissi, ignorando la struttura semantica del documento.Con Semantic Kernel, la scelta corretta per documenti Markdown è TextChunker.SplitMarkdownParagraphs(), che rispetta i confini dei paragrafi:var chunks = TextChunker.SplitMarkdownParagraphs(
    lines: markdownContent.Split('\n').ToList(),
    maxTokensPerParagraph: 512,
    overlapTokens: 50
);Due parametri critici spesso ignorati:overlapTokens: senza sovrapposizione, le frasi che cadono al confine tra due chunk vengono perse. Una sovrapposizione del 10-15% (qui 50 token su 512) risolve il problema.Pre-processing HTML→Markdown: se i sorgenti sono pagine web, convertire in Markdown prima del chunking (con librerie come HtmlAgilityPack) elimina tag irrilevanti che degradano la qualità dell’embedding.Un’altra best practice avanzata: separare i blocchi di codice dalla prosa e taggare ogni chunk con metadati (tipo di contenuto, sezione, sorgente) per poter filtrare durante il retrieval.Le soglie di rilevanza non sono universaliI tutorial usano tipicamente soglie di similarità coseno tra 0.75 e 0.80. Questo valore è quasi sempre sbagliato per il tuo corpus specifico. La soglia ottimale dipende da: qualità dell’embedding model, distribuzione semantica del corpus, tipologia delle query.L’approccio corretto:Costruire manualmente un set di valutazione di 20-30 coppie query/risposta attesaPartire da 0.70 e iterare misurando Context RecallNon affidarsi mai ai default senza validazione corpus-specificaScegliere il vector store giustoSemantic Kernel supporta diversi backend. La scelta sbagliata genera complessità inutile o performance inadeguate:VolatileMemoryStore — solo per demo, dati persi al restartSqliteMemoryStore — sviluppo locale e prime versioni production: zero infrastruttura, persistenza garantitaElasticsearch — stack esistenti con ricerca ibrida (full-text + vettoriale)Azure AI Search — produzione su Azure, gestione scalabilità automaticaQdrant / Pinecone — carichi vettoriali dedicati ad alta scalaPer molte applicazioni aziendali, SQLite è la scelta razionale fino a migliaia di documenti. Aggiungere infrastruttura vettoriale dedicata ha senso solo con volumi e requisiti di latenza che lo giustificano effettivamente.Evitare il re-embedding a ogni avvioUno degli errori più costosi (in termini economici e di latenza) è re-embeddare l’intero corpus a ogni riavvio dell’applicazione. La soluzione è semplice: verificare l’esistenza della collection prima di procedere all’ingestion:var collections = await sqliteStore.GetCollectionsAsync().ToListAsync();
if (!collections.Contains(CollectionName))
{
    await ragService.IngestDocumentsAsync(documents, CollectionName);
}Con Azure AI Search o Qdrant, la logica è analoga ma si basa sulle API specifiche del provider.Il prompt di grounding non è opzionaleLa costruzione del prompt è la difesa principale contro le allucinazioni. C’è una differenza sostanziale tra queste due istruzioni:“Usa il contesto seguente per rispondere” — il modello può integrare con la sua conoscenza generale“Rispondi SOLO usando il contesto seguente. Se la risposta non è nel contesto, dillo esplicitamente.” — vincolo semantico forteLa parola “SOLO” cambia radicalmente il comportamento del modello. In produzione, il prompt di sistema deve essere esplicito e non ambiguo.Semantic caching per ridurre latenza e costiUn ottimizzazione ad alto impatto spesso ignorata: se una query è semanticamente simile a una già elaborata, si può restituire la risposta cached senza chiamare il vector store né il modello:var cachedAnswer = await cacheService.FindSimilarAsync(query, threshold: 0.92f);
if (cachedAnswer != null)
{
    return cachedAnswer.Answer;
}Con una soglia alta (0.90-0.95), il cache serve solo query davvero simili, evitando risposte errate. Per sistemi con pattern di query ripetitivi (FAQ, assistenti documentali), questo ottimizzazione può ridurre i costi LLM del 40-60%.Osservabilità: cosa monitorareUn sistema RAG senza osservabilità è un sistema cieco. I KPI da tracciare per ogni richiesta:TopChunkScore: se costantemente sotto 0.75, il retrieval fatica. Rivedere chunking o embedding model.ChunksRetrieved: se raggiunge sempre il limite massimo, espandere la finestra di ricerca.CacheHit: se sempre false con alta latenza, la soglia del cache è troppo restrittiva.Latency: separare la latenza di retrieval da quella LLM per identificare il bottleneck.Valutazione continua e CIIl rischio silenzioso del RAG è la regressione: una modifica al chunking o alla soglia migliora alcune query e ne peggiora altre. La soluzione è integrare un set di valutazione nel pipeline CI con xUnit:Context Recall: i chunk corretti vengono recuperati?Faithfulness: la risposta rimane ancorata al contesto?Answer Correctness: corrisponde alla risposta attesa?Il set di test deve includere query facili, domande che richiedono sintesi multi-documento, e domande a cui il sistema non dovrebbe rispondere (out-of-scope) — queste ultime sono fondamentali per rilevare allucinazioni.ConclusioneCostruire un sistema RAG che funziona nei demo è relativamente semplice con Semantic Kernel. Costruirne uno che funziona in produzione — con costi controllati, latenza accettabile, assenza di allucinazioni e monitoraggio efficace — richiede decisioni architetturali precise. Il chunking con overlap, le soglie calibrate sul corpus reale, il caching semantico e l’osservabilità non sono optional: sono la differenza tra un prototipo e un sistema affidabile.Fonte originale: RAG in .NET: What the Tutorials Don’t Tell You di Jamie Maguire.]]></description><link>https://forum.androidiani.net/topic/e08c285d-52b6-45fa-a447-3192f21f02ee/rag-in-.net-con-semantic-kernel-le-insidie-che-i-tutorial-non-ti-dicono</link><guid isPermaLink="true">https://forum.androidiani.net/topic/e08c285d-52b6-45fa-a447-3192f21f02ee/rag-in-.net-con-semantic-kernel-le-insidie-che-i-tutorial-non-ti-dicono</guid><dc:creator><![CDATA[blog@spcnet.it]]></dc:creator><pubDate>Tue, 21 Apr 2026 07:55:23 GMT</pubDate></item><item><title><![CDATA[AOT-Friendly DTO Mapping in .NET: Source Generators al posto della reflection]]></title><description><![CDATA[Con l’adozione crescente di NativeAOT e il trimming in .NET, uno dei nodi critici per molti progettisti è la gestione del mapping tra oggetti: le librerie classiche come AutoMapper si basano pesantemente sulla reflection a runtime, che è incompatibile con la compilazione Ahead-of-Time. In questo articolo esploreremo ElBruno.AotMapper, una libreria che risolve il problema spostando la generazione del codice di mapping a compile time grazie ai Roslyn Source Generators.Il problema: reflection e AOT non vanno d’accordoQuando si compila un’applicazione .NET con PublishAot=true oppure si abilita il trimming aggressivo, il runtime non può più analizzare dinamicamente i tipi come farebbe normalmente. Le librerie che usano System.Reflection per scoprire proprietà e invocare setter al volo — come AutoMapper nella sua configurazione classica — provocano errori in fase di esecuzione o vengono tagliate dal trimmer.Le alternative tradizionali per chi vuole restare AOT-compatible sono:Scrivere il mapping a mano (tedioso e soggetto a errori)Usare Mapster con configurazione esplicita (verbosa)Affidarsi a Source Generators che generano il codice prima della compilazioneElBruno.AotMapper percorre la terza strada: usa un Source Generator basato su Roslyn per emettere metodi di estensione fortemente tipizzati già al momento del build, con zero reflection a runtime.Come funziona: Source Generators al posto della reflectionI Roslyn Source Generators sono componenti che vengono eseguiti durante la compilazione e possono produrre file C# aggiuntivi. In questo caso, il generator analizza le vostre classi annotate con attributi specifici e genera automaticamente i metodi di mapping corrispondenti.I vantaggi concreti di questo approccio:Gli errori di mapping emergono in fase di compilazione, non a runtimeZero overhead da reflection: il codice generato è codice C# diretto, ottimizzabile dal JIT o dall’AOT compilerCompatibilità completa con NativeAOT e applicazioni trimmateIl codice generato è visibile e debuggabile (potete ispezionarlo nelle cartelle di build)InstallazioneLa libreria si divide in due package NuGet distinti:# Attributi e interfacce core dotnet add package ElBruno.AotMapper  # Il Source Generator (solo per il progetto che contiene i DTO) dotnet add package ElBruno.AotMapper.GeneratorIl Generator va aggiunto come PrivateAssets="all" in genere, per evitare che la dipendenza si propaghi ai progetti dipendenti:&lt;PackageReference Include="ElBruno.AotMapper.Generator" Version="x.y.z"&gt;   &lt;PrivateAssets&gt;all&lt;/PrivateAssets&gt;   &lt;IncludeAssets&gt;runtime; build; native; contentfiles; analyzers&lt;/IncludeAssets&gt; &lt;/PackageReference&gt;Utilizzo degli attributi di mappingMapping base con [MapFrom]Il caso più semplice: due classi con le stesse proprietà. Si annota il DTO destinazione con [MapFrom] specificando il tipo sorgente:// Entità del dominio public class Product {     public int Id { get; set; }     public string Name { get; set; } = string.Empty;     public decimal Price { get; set; }     public DateTime CreatedAt { get; set; } }  // DTO di risposta annotato [MapFrom(typeof(Product))] public class ProductDto {     public int Id { get; set; }     public string Name { get; set; } = string.Empty;     public decimal Price { get; set; } }Il Source Generator analizza questo codice e genera automaticamente un metodo di estensione. Dopo la compilazione potrete usarlo così:var product = await dbContext.Products.FindAsync(id); var dto = product.ToProductDto(); // Metodo generato, zero reflectionRimappatura di proprietà con [MapProperty]Quando i nomi delle proprietà non corrispondono tra sorgente e destinazione, si usa [MapProperty] per indicare esplicitamente il mapping:[MapFrom(typeof(User))] public class UserSummaryDto {     public int Id { get; set; }      [MapProperty(nameof(User.FirstName))]     public string Nome { get; set; } = string.Empty;  // Diverso da FirstName      [MapProperty(nameof(User.LastName))]     public string Cognome { get; set; } = string.Empty; }Ignorare proprietà con [MapIgnore]Per escludere campi sensibili o non necessari:[MapFrom(typeof(User))] public class PublicUserDto {     public int Id { get; set; }     public string UserName { get; set; } = string.Empty;      [MapIgnore]     public string? PasswordHash { get; set; }  // Non verrà mappato }Conversioni custom con [MapConverter]Per logiche di conversione non banali, si implementa IMapConverter&lt;TSource, TDestination&gt;:public class PriceToStringConverter : IMapConverter&lt;decimal, string&gt; {     public string Convert(decimal source) =&gt; source.ToString("C2"); }  [MapFrom(typeof(Product))] public class ProductDisplayDto {     public string Name { get; set; } = string.Empty;      [MapConverter(typeof(PriceToStringConverter))]     public string FormattedPrice { get; set; } = string.Empty; }Integrazione in un Minimal API ASP.NET CoreIl package AspNetCore della libreria facilita l’integrazione con Dependency Injection. Ecco un esempio completo di endpoint:// Program.cs var builder = WebApplication.CreateBuilder(args); builder.Services.AddDbContext&lt;AppDbContext&gt;(...);  var app = builder.Build();  app.MapGet("/products/{id}", async (int id, AppDbContext db) =&gt; {     var product = await db.Products.FindAsync(id);     if (product is null) return Results.NotFound();      // ToProductDto() è un metodo generato dal Source Generator     return Results.Ok(product.ToProductDto()); });  app.Run();Quando si pubblica con dotnet publish -r win-x64 -p:PublishAot=true, il codice di mapping generato viene incluso senza problemi perché è codice C# statico, non reflection dinamica.Considerazioni praticheLa libreria è indicata soprattutto per chi:Sta migrando applicazioni verso NativeAOT o vuole abilitare il trimmingSviluppa microservizi con startup time critico (AOT avvia le app molto più velocemente)Preferisce avere errori di mapping evidenziati a compile timeVuole ispezionare il codice generato per capire esattamente cosa succedeLa libreria è ancora in evoluzione (work-in-progress secondo l’autore), quindi prima di adottarla in produzione è consigliabile valutare alternative mature come Mapperly, anch’essa basata su Source Generators e con una community più consolidata. Detto questo, ElBruno.AotMapper è un ottimo punto di partenza per capire come funziona il mapping AOT-friendly e i Source Generators in generale.ConclusioneIl passaggio a NativeAOT e al trimming in .NET è una tendenza inesorabile, specialmente per applicazioni cloud-native e microservizi che richiedono avvio rapido e footprint ridotto. Le librerie di mapping basate su reflection appartengono a un’era che sta tramontando: i Source Generators sono il futuro, e ElBruno.AotMapper mostra come si possa risolvere un problema pratico quotidiano — il mapping DTO — con questo approccio moderno.Se volete esplorare ulteriormente l’argomento, la documentazione ufficiale di Roslyn Source Generators è disponibile su Microsoft Learn, e il progetto di riferimento industriale è Mapperly.Fonte originale: AOT-Friendly DTO Mapping in .NET – El Bruno]]></description><link>https://forum.androidiani.net/topic/95d679a0-1226-4238-8ee9-92326247ec8a/aot-friendly-dto-mapping-in-.net-source-generators-al-posto-della-reflection</link><guid isPermaLink="true">https://forum.androidiani.net/topic/95d679a0-1226-4238-8ee9-92326247ec8a/aot-friendly-dto-mapping-in-.net-source-generators-al-posto-della-reflection</guid><dc:creator><![CDATA[blog@spcnet.it]]></dc:creator><pubDate>Sun, 19 Apr 2026 12:31:45 GMT</pubDate></item><item><title><![CDATA[Command Pattern in C#: guida completa con undo, redo e Dependency Injection]]></title><description><![CDATA[Il Command Pattern è uno dei design pattern comportamentali più utili nel mondo dello sviluppo software. In C#, la sua implementazione è particolarmente elegante e consente di costruire sistemi robusti con funzionalità di undo/redo, accodamento di operazioni e logging senza intaccare la logica di business. In questo articolo vedremo come implementarlo passo dopo passo, con esempi concreti e consigli pratici per applicazioni reali.Cos’è il Command Pattern?Il Command Pattern incapsula una richiesta come un oggetto autonomo, separando chi la emette da chi la esegue. Questo consente di parametrizzare i client con richieste diverse, accodare operazioni, supportare il rollback e costruire sistemi di macro o audit trail. In parole semplici: invece di chiamare direttamente un metodo, si crea un oggetto che “sa come” eseguire quella chiamata, e lo si passa all’esecutore.I casi d’uso più classici includono:Editor di testo o grafica con undo/redo illimitatoSistemi di workflow con operazioni reversibiliTransaction scripts in architetture DDDLogging e auditing di operazioni criticheMacro recording nelle applicazioni di produttivitàI componenti del patternUn’implementazione corretta del Command Pattern in C# prevede quattro attori principali:ICommand: l’interfaccia contrattuale che definisce Execute(), Undo() e una proprietà descrittivaReceiver: la classe che contiene la logica di business effettiva, ignara del patternConcrete Command: le implementazioni di ICommand, che catturano i parametri in costruzione e delegano al ReceiverInvoker: gestisce la coda di esecuzione e le stack di undo/redoImplementazione passo dopo passoStep 1: definire l’interfaccia ICommandIl contratto deve essere minimale. Tutti i dati necessari all’esecuzione viaggiano dentro il comando stesso, non vengono passati ai metodi:public interface ICommand {     string Description { get; }     void Execute();     void Undo(); }La scelta di metodi senza parametri è deliberata: favorisce l’immutabilità del comando dopo la costruzione e impedisce il condizionamento da stato esterno.Step 2: creare il ReceiverIl Receiver contiene la logica reale. Non sa nulla di comandi, stack o undo. È testabile in isolamento:public class TextDocument {     private readonly List&lt;string&gt; _lines = new();      public IReadOnlyList&lt;string&gt; Lines =&gt; _lines.AsReadOnly();      public void AddLine(string text) =&gt; _lines.Add(text);      public void RemoveLine(int index)     {         if (index &gt;= 0 &amp;&amp; index &lt; _lines.Count)             _lines.RemoveAt(index);     } }Step 3: implementare i Concrete CommandsOgni comando cattura i propri dati al momento della costruzione e implementa sia Execute che Undo. Notare che lo stato necessario per l’undo viene salvato durante Execute:public sealed class AddLineCommand : ICommand {     private readonly TextDocument _document;     private readonly string _text;      public string Description =&gt; $"Aggiungi riga: "{_text}"";      public AddLineCommand(TextDocument document, string text)     {         _document = document ?? throw new ArgumentNullException(nameof(document));         _text = text ?? throw new ArgumentNullException(nameof(text));     }      public void Execute() =&gt; _document.AddLine(_text);      public void Undo() =&gt; _document.RemoveLine(_document.Lines.Count - 1); }  public sealed class RemoveLineCommand : ICommand {     private readonly TextDocument _document;     private readonly int _index;     private string? _removedText;      public string Description =&gt; $"Rimuovi riga {_index}";      public RemoveLineCommand(TextDocument document, int index)     {         _document = document;         _index = index;     }      public void Execute()     {         // Salviamo lo stato per poter fare undo         _removedText = _document.Lines[_index];         _document.RemoveLine(_index);     }      public void Undo()     {         if (_removedText is not null)             _document.AddLine(_removedText);     } }Il punto critico è la cattura dello snapshot in Execute(): RemoveLineCommand ricorda il testo rimosso prima di cancellarlo, rendendo possibile il ripristino.Step 4: costruire l’Invoker con historyL’Invoker mantiene due stack: uno per l’undo e uno per il redo. Quando si esegue un nuovo comando, la redo stack viene svuotata per evitare storie ramificate incoerenti:public class CommandInvoker {     private readonly Stack&lt;ICommand&gt; _undoStack = new();     private readonly Stack&lt;ICommand&gt; _redoStack = new();      public void ExecuteCommand(ICommand command)     {         command.Execute();         _undoStack.Push(command);         _redoStack.Clear(); // Nuova azione invalida il redo     }      public bool CanUndo =&gt; _undoStack.Count &gt; 0;     public bool CanRedo =&gt; _redoStack.Count &gt; 0;      public void Undo()     {         if (!CanUndo) return;         var cmd = _undoStack.Pop();         cmd.Undo();         _redoStack.Push(cmd);     }      public void Redo()     {         if (!CanRedo) return;         var cmd = _redoStack.Pop();         cmd.Execute();         _undoStack.Push(cmd);     }      public IEnumerable&lt;string&gt; GetHistory() =&gt;         _undoStack.Select(c =&gt; c.Description); }Step 5: Macro Commands (Composite)Il Command Pattern si combina naturalmente con il Composite Pattern per costruire macro che raggruppano più operazioni atomiche:public sealed class MacroCommand : ICommand {     private readonly IReadOnlyList&lt;ICommand&gt; _commands;      public string Description =&gt; $"Macro ({_commands.Count} operazioni)";      public MacroCommand(IEnumerable&lt;ICommand&gt; commands)     {         _commands = commands.ToList();     }      public void Execute()     {         foreach (var cmd in _commands)             cmd.Execute();     }      public void Undo()     {         // L'undo avviene in ordine inverso         for (int i = _commands.Count - 1; i &gt;= 0; i--)             _commands[i].Undo();     } }Step 6: integrazione con Dependency InjectionIn applicazioni .NET moderne, il pattern si integra perfettamente con il DI container di ASP.NET Core:// Program.cs builder.Services.AddSingleton&lt;TextDocument&gt;(); builder.Services.AddSingleton&lt;CommandInvoker&gt;(); builder.Services.AddTransient&lt;DocumentCommandFactory&gt;();  // Factory per creare comandi on-demand public class DocumentCommandFactory {     private readonly TextDocument _document;      public DocumentCommandFactory(TextDocument document)     {         _document = document;     }      public ICommand AddLine(string text) =&gt; new AddLineCommand(_document, text);     public ICommand RemoveLine(int index) =&gt; new RemoveLineCommand(_document, index); }I componenti longevi (TextDocument, CommandInvoker) sono Singleton; i comandi vengono creati on-demand tramite factory e rimangono short-lived.Errori comuni da evitareChi si avvicina al Command Pattern per la prima volta tende a commettere questi errori:Logica nel comando invece che nel Receiver: i comandi devono orchestrare, non implementare. La logica di business appartiene al Receiver, dove può essere testata in isolamento.Mancato snapshot dello stato per l’undo: se dimenticate di catturare lo stato prima dell’esecuzione, l’undo diventa impossibile o inaffidabile.Stato condiviso tra comandi: ogni comando deve essere autosufficiente. Lo stato mutabile condiviso porta a bug sottili quando i comandi vengono eseguiti in ordine diverso.Undo implementato in modo forzato: per operazioni genuinamente non reversibili (come l’invio di un’email), meglio lanciare un’eccezione o documentare esplicitamente il limite, piuttosto che fingere un undo inesistente.ConclusioneIl Command Pattern è uno strumento potente e sottovalutato. Quando il vostro sistema ha bisogno di storico operazioni, undo/redo, accodamento differito o audit trail, questo pattern vi offre una soluzione elegante e testabile. La separazione netta tra Receiver (logica) e Command (orchestrazione) rende il codice mantenibile anche su larga scala.Se state sviluppando con C# e .NET, consideratelo ogni volta che la vostra architettura richiede operazioni reversibili o la composizione di azioni complesse.Fonte originale: How to Implement Command Pattern in C# – DevLeader.ca]]></description><link>https://forum.androidiani.net/topic/a3f374d1-8259-452e-9bcb-2ae70c1f51dc/command-pattern-in-c-guida-completa-con-undo-redo-e-dependency-injection</link><guid isPermaLink="true">https://forum.androidiani.net/topic/a3f374d1-8259-452e-9bcb-2ae70c1f51dc/command-pattern-in-c-guida-completa-con-undo-redo-e-dependency-injection</guid><dc:creator><![CDATA[blog@spcnet.it]]></dc:creator><pubDate>Sun, 19 Apr 2026 09:12:09 GMT</pubDate></item><item><title><![CDATA[LINQ Max e i tipi valore nullable in C#: il comportamento inatteso che causa eccezioni a runtime]]></title><description><![CDATA[Chi usa LINQ in C# con una certa frequenza prima o poi si imbatte in un comportamento del metodo Max che, a prima vista, appare del tutto irrazionale: due chiamate sintatticamente quasi identiche producono risultati radicalmente diversi, e una delle due lancia un’eccezione a runtime su una sequenza vuota. Vediamo perché accade e come risolverlo.Il problema: Max con proiezioni su tipi diversiConsideriamo questo tipo record:public record WithValues(string Label, int Number, DateTimeOffset Date);Ora proviamo a chiamare Max su un array vuoto con tre proiezioni diverse:WithValues[] empty = [];  string? maxLabel  = empty.Max(x =&gt; x.Label);   // Restituisce null var     maxDate   = empty.Max(d =&gt; d.Date);     // Lancia InvalidOperationException int?    maxNumber = empty.Max(x =&gt; x.Number);   // Lancia InvalidOperationExceptionLa firma del metodo è:public static TResult? Max&lt;TSource, TResult&gt;(     this IEnumerable&lt;TSource&gt; source,     Func&lt;TSource, TResult&gt; selector);Il tipo di ritorno è dichiarato come TResult? — nullable. Eppure il comportamento cambia radicalmente in base a ciò che la funzione di proiezione restituisce.La causa: come Max distingue i tipi internamenteInternamente, l’implementazione di Max usa un test per decidere come comportarsi su una sequenza vuota:TResult val = default; if (val == null) {     // Tipo reference o nullable: restituisce null per sequenze vuote } else {     // Tipo valore non nullable: lancia eccezione per sequenze vuote }Questo porta a tre comportamenti distinti:Tipi reference (string): default(string) è null, quindi il ramo “restituisce null” viene eseguito correttamente.Tipi valore non nullable (int, DateTimeOffset): i loro default non sono null, quindi viene eseguito il ramo che lancia InvalidOperationException.Tipi valore nullable (int?, DateTimeOffset?): default(int?) è null, quindi rientra nel primo ramo e restituisce null.Perché questo design?Il ragionamento alla base è comprensibile: per i tipi valore, restituire il valore di default per una sequenza vuota sarebbe ambiguo. Se Max su una lista vuota di interi restituisse 0, come distinguere tra “il massimo è zero” e “la lista era vuota”? L’eccezione rimuove questa ambiguità, ma lo fa in modo sorprendente data la firma del metodo.Il problema di fondo è storico: C# ha sviluppato la nullabilità in tre fasi distinte che non si integrano uniformemente nel codice generico:I tipi reference erano sempre nullable (fin dalle origini del linguaggio).I tipi valore nullable (Nullable&lt;T&gt;, ovvero T?) sono stati introdotti in C# 2.0.Le nullable reference types (NRT) sono arrivate in C# 8.0 come feature opzionale.La soluzione: cast esplicito al tipo nullableLa correzione è semplice: basta fare il cast esplicito della proiezione al tipo nullable corrispondente.// Invece di: var maxDate    = empty.Max(d =&gt; d.Date); int? maxNumber = empty.Max(x =&gt; x.Number);  // Usare: DateTimeOffset? maxDate    = empty.Max(d =&gt; (DateTimeOffset?)d.Date); int?            maxNumber  = empty.Max(x =&gt; (int?)x.Number);Il cast forza il compilatore a inferire TResult = DateTimeOffset? (o int?), il cui default è null, e Max segue quindi il percorso corretto, restituendo null invece di lanciare un’eccezione.Alternativa: DefaultIfEmptyUn’altra soluzione è preporre DefaultIfEmpty prima di Max:int maxNumber = empty     .Select(x =&gt; x.Number)     .DefaultIfEmpty(0)     .Max();Questa alternativa è utile quando si vuole un valore di fallback esplicito invece di null, ma va usata con attenzione: il valore di default deve essere scelto consapevolmente per non introdurre ambiguità semantica nel risultato.Quando questo si manifesta in produzioneIl problema diventa insidioso quando si lavora con sequenze filtrate dinamicamente che possono risultare vuote in certi contesti, o quando il codice viene testato sempre con dati non vuoti e crasha in produzione su edge case inaspettati. Una buona pratica difensiva è usare sempre il cast a tipo nullable quando si usa Max (o Min, che ha lo stesso comportamento) su tipi valore, a meno che non si abbia la certezza assoluta che la sequenza non sarà mai vuota.ConclusioniIl comportamento di LINQ.Max con i tipi valore è uno di quei casi in cui l’implementazione è tecnicamente coerente, ma la firma del metodo lascia intendere qualcosa di diverso da ciò che avviene a runtime. La regola da ricordare è semplice: se la proiezione restituisce un tipo valore non nullable e la sequenza potrebbe essere vuota, usare sempre un cast esplicito a T?. Un piccolo accorgimento che previene eccezioni difficili da diagnosticare in produzione.Fonte originale: LINQ Max and nullable value types (Ian Griffiths, endjin.com) — tratto dal briefing Dew Drop del 17 aprile 2026]]></description><link>https://forum.androidiani.net/topic/5b9e87b8-d2f4-470c-b430-6bea359bb061/linq-max-e-i-tipi-valore-nullable-in-c-il-comportamento-inatteso-che-causa-eccezioni-a-runtime</link><guid isPermaLink="true">https://forum.androidiani.net/topic/5b9e87b8-d2f4-470c-b430-6bea359bb061/linq-max-e-i-tipi-valore-nullable-in-c-il-comportamento-inatteso-che-causa-eccezioni-a-runtime</guid><dc:creator><![CDATA[blog@spcnet.it]]></dc:creator><pubDate>Sat, 18 Apr 2026 20:28:35 GMT</pubDate></item><item><title><![CDATA[Confronto tra stringhe in C#: Equals, OrdinalIgnoreCase, StringComparer e le insidie culturali]]></title><description><![CDATA[Il confronto tra stringhe è una delle operazioni più comuni in qualsiasi applicazione .NET, eppure è anche una delle fonti più insidiose di bug difficili da riprodurre — specialmente quando l’applicazione viene eseguita in ambienti con culture diverse o in pipeline CI/CD con impostazioni locali variabili. In questo articolo vediamo come funzionano correttamente string.Equals(), OrdinalIgnoreCase, StringComparer e come evitare le trappole più comuni legate alla cultura.L’operatore == e il confronto ordinaleL’operatore == su stringhe esegue un confronto ordinale case-sensitive, basato sui valori Unicode dei caratteri, senza alcuna considerazione culturale:var a = "Hello";
var b = "hello";
Console.WriteLine(a == b);       // False
Console.WriteLine(a == "Hello"); // True
Questo è corretto e prevedibile per confronti interni al codice (chiavi di dizionari, nomi di variabili, costanti). Il problema nasce quando si vuole un confronto case-insensitive, o quando si confrontano stringhe con caratteri soggetti a regole culturali.string.Equals() con StringComparisonLa regola d’oro è: passare sempre esplicitamente un parametro StringComparison. Senza di esso, alcuni overload usano CurrentCulture, creando comportamenti potenzialmente incoerenti tra ambienti diversi.// Confronto case-insensitive, senza dipendenze culturali
bool uguale = string.Equals("admin", input, StringComparison.OrdinalIgnoreCase);

// Equivalente con metodo d'istanza
bool ancheUguale = "admin".Equals(input, StringComparison.OrdinalIgnoreCase);
Panoramica dei valori di StringComparisonValoreCaseCulturaUso consigliatoOrdinalSensibileNessunaFile, chiavi, dati binariOrdinalIgnoreCaseInsensibileNessunaComandi, URL, identificatoriInvariantCultureSensibileInvarianteTesto serializzato/persistitoInvariantCultureIgnoreCaseInsensibileInvarianteTesto serializzato, case-indipendenteCurrentCultureSensibileLocale utenteTesto mostrato all’utenteCurrentCultureIgnoreCaseInsensibileLocale utenteRicerca/filtro lato UILa trappola di ToLower() e ToUpper()Uno degli antipattern più diffusi è usare ToLower() per normalizzare le stringhe prima del confronto:// Antipattern: alloca una nuova stringa inutilmente
if (input.ToLower() == "admin") { }
if (input.ToLowerInvariant() == "admin") { }

// Corretto: nessuna allocazione, semantica esplicita
if (string.Equals(input, "admin", StringComparison.OrdinalIgnoreCase)) { }
Il problema non è solo di prestazioni (allocazione di una stringa temporanea), ma di correttezza semantica. La versione con ToLower() dipende dalla cultura corrente del thread, mentre OrdinalIgnoreCase è culturalmente neutro e deterministico.Il problema della “i” TurcaQuesto è forse il bug più famoso legato alla cultura nelle stringhe. In turco esistono quattro varianti della lettera i: la “i” minuscola con punto diventa “İ” maiuscola con punto (non “I” come in italiano), e la “ı” minuscola senza punto diventa “I” maiuscola. Il risultato:var culture = new System.Globalization.CultureInfo("tr-TR");

// Bug su locale turco!
bool sbagliato = "file".ToUpper(culture) == "FILE"; // False! "file" diventa "FİLE" in turco

// OrdinalIgnoreCase usa regole invarianti
bool corretto = string.Equals("file", "FILE", StringComparison.OrdinalIgnoreCase); // True
Questo bug si manifesta tipicamente in applicazioni multi-tenant o globali dove il server ha una cultura diversa dall’ambiente di sviluppo. La soluzione è sempre usare OrdinalIgnoreCase per confronti tecnici (nomi di file, comandi, URL, header HTTP) e riservare CurrentCulture solo al testo destinato all’utente finale.StringComparer per collezioniStringComparer implementa sia IComparer&lt;string&gt; che IEqualityComparer&lt;string&gt;, rendendolo ideale per strutture dati come Dictionary, HashSet e SortedSet:Dizionario case-insensitive per gli header HTTP// "Content-Type" e "content-type" devono essere equivalenti
var headers = new Dictionary&lt;string, string&gt;(StringComparer.OrdinalIgnoreCase);
headers["Content-Type"] = "application/json";

Console.WriteLine(headers["content-type"]); // application/json
Console.WriteLine(headers["CONTENT-TYPE"]); // application/json
SortedSet case-insensitivevar comandi = new SortedSet&lt;string&gt;(StringComparer.OrdinalIgnoreCase);
comandi.Add("Start");
comandi.Add("stop");

bool haStart = comandi.Contains("START"); // True
// I duplicati vengono rilevati correttamente
comandi.Add("START"); // Non aggiunge, esiste già come "Start"
Console.WriteLine(comandi.Count); // 2
Confronto ad alte prestazioni con Span&lt;char&gt;Per scenari con requisiti di performance elevati (parsing di protocolli, hot paths), .NET offre confronti allocation-free tramite ReadOnlySpan&lt;char&gt;:var riga = "Content-Type: application/json";

// Confronto senza allocare nuove stringhe
bool isContentType = riga.AsSpan(0, 12).Equals(
    "Content-Type".AsSpan(),
    StringComparison.OrdinalIgnoreCase);

// StartsWith su Span
bool isHttps = url.AsSpan().StartsWith(
    "https://".AsSpan(),
    StringComparison.OrdinalIgnoreCase);
Questo approccio è particolarmente utile in middleware HTTP, parser di configurazione e codice che elabora grandi volumi di testo.Pattern Matching con SwitchIl pattern matching di C# non supporta nativamente il confronto case-insensitive negli switch, ma esistono due approcci corretti:// Approccio 1: Guard clause con Equals
var risultato = comando switch
{
    _ when string.Equals(comando, "start", StringComparison.OrdinalIgnoreCase) =&gt; "Avvio...",
    _ when string.Equals(comando, "stop", StringComparison.OrdinalIgnoreCase) =&gt; "Arresto...",
    _ =&gt; "Comando non riconosciuto"
};

// Approccio 2: Normalizzazione con ToUpperInvariant (accettabile per switch)
var risultato2 = comando.ToUpperInvariant() switch
{
    "START" =&gt; "Avvio...",
    "STOP" =&gt; "Arresto...",
    _ =&gt; "Comando non riconosciuto"
};
Esempio completo: parser di configurazionepublic sealed class ConfigParser
{
    private readonly FrozenDictionary&lt;string, string&gt; _impostazioni;

    public ConfigParser(IEnumerable&lt;KeyValuePair&lt;string, string&gt;&gt; rawSettings)
    {
        // FrozenDictionary è ottimizzato per letture frequenti (immutabile dopo la creazione)
        _impostazioni = rawSettings.ToFrozenDictionary(
            kvp =&gt; kvp.Key,
            kvp =&gt; kvp.Value,
            StringComparer.OrdinalIgnoreCase);
    }

    public string? Get(string chiave) =&gt;
        _impostazioni.TryGetValue(chiave, out var valore) ? valore : null;
}

// Utilizzo
var config = new ConfigParser(new[]
{
    new KeyValuePair&lt;string, string&gt;("DatabaseUrl", "Server=..."),
    new KeyValuePair&lt;string, string&gt;("MaxConnections", "100"),
});

Console.WriteLine(config.Get("databaseurl"));    // "Server=..."
Console.WriteLine(config.Get("MAXCONNECTIONS")); // "100"
Riepilogo: regole praticheUsa OrdinalIgnoreCase per chiavi, identificatori, URL, nomi di file, comandi, header HTTP.Usa CurrentCulture solo per testo mostrato all’utente, quando le regole locali sono rilevanti.Non usare ToLower() per confronti: alloca inutilmente e dipende dalla cultura.Specifica sempre StringComparison esplicitamente nelle chiamate a Equals e Compare.Usa StringComparer quando passi logica di confronto a strutture dati.Usa MemoryExtensions su Span&lt;char&gt; per hot path ad alta frequenza e senza allocazioni.Seguendo queste linee guida, si eliminano intere classi di bug difficili da riprodurre e si ottiene codice più robusto, portabile e performante.Fonte: DevLeader — C# String Comparison: Equals, OrdinalIgnoreCase, StringComparer, and Culture Pitfalls]]></description><link>https://forum.androidiani.net/topic/ef168328-663f-4da0-84d4-b9b0687112a9/confronto-tra-stringhe-in-c-equals-ordinalignorecase-stringcomparer-e-le-insidie-culturali</link><guid isPermaLink="true">https://forum.androidiani.net/topic/ef168328-663f-4da0-84d4-b9b0687112a9/confronto-tra-stringhe-in-c-equals-ordinalignorecase-stringcomparer-e-le-insidie-culturali</guid><dc:creator><![CDATA[blog@spcnet.it]]></dc:creator><pubDate>Thu, 16 Apr 2026 14:14:54 GMT</pubDate></item><item><title><![CDATA[.NET 11 Preview 3: tutte le novità del terzo rilascio anticipato]]></title><description><![CDATA[.NET 11 è ancora in fase di sviluppo, ma il terzo Preview rilasciato il 14 aprile 2026 porta con sé una serie di novità concrete e già sperimentabili. Dalle librerie di sistema al runtime JIT, dall’SDK alla toolchain, questo rilascio intermedio offre uno sguardo chiaro su dove sta andando l’ecosistema .NET. Vediamo nel dettaglio cosa c’è di nuovo e cosa vale la pena testare subito nei propri progetti.Novità nel Runtime e JITIl team del runtime ha rimosso il requisito del flag di opt-in per le API async previewed nelle versioni precedenti: alcune funzionalità asincrone del runtime sono ora accessibili senza annotazioni speciali. Sul fronte delle prestazioni, il JIT compiler è stato ottimizzato in tre aree chiave:Switch statement: migliorata la generazione di codice macchina per costrutti switch complessiBounds check: eliminati controlli ridondanti sugli array in scenari comuniType cast: ridotta la latenza nelle operazioni di cast tra tipi gerarchicamente correlatiSul fronte WebAssembly, .NET 11 introduce il formato WebCIL, un nuovo formato di packaging per i moduli .wasm, insieme a miglioramenti al debugging in ambiente browser. Chi sviluppa applicazioni Blazor WebAssembly beneficerà di esperienze di debug più fluide.Librerie di Sistema: JSON, Compressione e RegexSystem.Text.Json riceve controllo più granulare sulla naming strategy e sulla gestione dei valori di default. Sarà possibile configurare i serializzatori per ignorare specifiche proprietà con valori predefiniti in modo più preciso rispetto a quanto offriva il parametro DefaultIgnoreCondition.La novità più interessante lato compressione è l’aggiunta di Zstandard (zstd) in System.IO.Compression. Zstandard è l’algoritmo di compressione sviluppato da Meta, noto per offrire un ottimo trade-off tra velocità e rapporto di compressione — spesso superiore a gzip e deflate. Esempio di utilizzo:using System.IO.Compression;

// Compressione con Zstandard
using var input = File.OpenRead("dati.json");
using var output = File.Create("dati.json.zst");
using var compressor = new ZstandardStream(output, CompressionMode.Compress);
await input.CopyToAsync(compressor);
In aggiunta, la lettura di file ZIP ora valida i checksum CRC32 automaticamente, rendendo più robusta la gestione degli archivi corrotti o manomessi.Per le espressioni regolari, il motore riconosce ora tutte le sequenze Unicode di newline, migliorando la portabilità del parsing testuale cross-platform.SDK: Nuove Funzionalità da CLIL’SDK riceve alcune aggiunte molto pratiche per chi lavora da terminale:Passare variabili d’ambiente con dotnet run -e# Passare una variabile d'ambiente al processo durante lo sviluppo
dotnet run -e ASPNETCORE_ENVIRONMENT=Development -e ConnectionStrings__Default="Server=..."
Finalmente si può sovrascrivere variabili d’ambiente senza toccare launchSettings.json o esportare variabili nello shell corrente.Top-level programs multi-fileI programmi con top-level statements (introdotti in C# 9) potranno ora distribuirsi su più file sorgente. Questo alleggerisce il vincolo di avere tutto in un unico Program.cs per i progetti di tipo file-based, come script o tool CLI leggeri.dotnet watch miglioratoLo strumento dotnet watch ha ricevuto tre miglioramenti rilevanti: supporto ai progetti .NET Aspire, crash recovery automatico e miglioramenti specifici per applicazioni Windows desktop (WPF/WinForms).C# 14: Union Types in AnteprimaPreview 3 include un’anteprima degli union types in C# 14. Questa funzionalità, molto attesa, consente di dichiarare tipi che possono contenere uno tra un insieme definito di tipi, avvicinando C# a pattern già presenti in F#, TypeScript e Rust:// Sintassi ipotetica - ancora in preview, soggetta a variazioni
union Result&lt;T&gt;
{
    T Value,
    Exception Error
}

var result = GetData();
var output = result switch
{
    Result&lt;string&gt;.Value(var v) =&gt; $"Successo: {v}",
    Result&lt;string&gt;.Error(var e) =&gt; $"Errore: {e.Message}"
};
Attenzione: la sintassi è ancora sperimentale e potrebbe cambiare nelle release successive.ASP.NET Core in .NET 11 Preview 3Tre aggiornamenti principali per il framework web:Compressione Zstandard per le risposte HTTPASP.NET Core ora supporta la compressione zstd nelle risposte HTTP e la decompressione delle richieste in arrivo. Da configurare in Program.cs:builder.Services.AddResponseCompression(options =&gt;
{
    options.Providers.Add&lt;ZstandardCompressionProvider&gt;();
    options.EnableForHttps = true;
});
Blazor Virtualize con altezze variabiliIl componente &lt;Virtualize&gt; di Blazor ora si adatta a elementi di altezza variabile a runtime, risolvendo un limite storico che richiedeva di specificare un’altezza fissa per gli elementi della lista virtualizzata.HTTP/3 più reattivoL’avvio dell’elaborazione delle richieste HTTP/3 è anticipato, riducendo la latenza percepita nelle prime connessioni su protocollo QUIC.Entity Framework Core: GetEntriesForState()EF Core in .NET 11 introduce ChangeTracker.GetEntriesForState(), un metodo che consente di interrogare le entry per stato (Added, Modified, Deleted) senza scatenare un cycle di change detection — operazione potenzialmente costosa in grafi di oggetti complessi:// Prima: scatenava change detection su tutto il grafo
var modified = context.ChangeTracker.Entries()
    .Where(e =&gt; e.State == EntityState.Modified)
    .ToList();

// Ora: accesso diretto per stato, senza overhead di detection
var modified = context.ChangeTracker
    .GetEntriesForState(EntityState.Modified)
    .ToList();
Container Images FirmatiLe immagini container ufficiali di .NET sono ora firmate digitalmente, permettendo la verifica della catena di custodia prima del deployment. Questo è un passo importante per le pipeline DevSecOps che richiedono provenance verificabile dei componenti software.Come Provare .NET 11 Preview 3# Scaricare il SDK da https://dotnet.microsoft.com/download/dotnet/11.0
dotnet --version
# Output atteso: 11.0.0-preview.3.xxxxx

# Creare un progetto di test
dotnet new console -n test-dotnet11 --framework net11.0
Conclusione.NET 11 Preview 3 è un rilascio ricco, che tocca trasversalmente runtime, SDK, ASP.NET Core ed EF Core. Le novità più impattanti per la produzione futura sono il supporto a Zstandard, gli union types in C# 14 e le ottimizzazioni JIT. Si consiglia di testare le nuove funzionalità dell’SDK — in particolare dotnet run -e — già ora, poiché entreranno probabilmente stabili con il rilascio di novembre 2026.Fonte: devblogs.microsoft.com — .NET 11 Preview 3 is now available!, 14 aprile 2026]]></description><link>https://forum.androidiani.net/topic/d40d5390-17ee-4516-ad75-1af67d56d8d4/.net-11-preview-3-tutte-le-novità-del-terzo-rilascio-anticipato</link><guid isPermaLink="true">https://forum.androidiani.net/topic/d40d5390-17ee-4516-ad75-1af67d56d8d4/.net-11-preview-3-tutte-le-novità-del-terzo-rilascio-anticipato</guid><dc:creator><![CDATA[blog@spcnet.it]]></dc:creator><pubDate>Thu, 16 Apr 2026 07:47:38 GMT</pubDate></item><item><title><![CDATA[LLM Chat in .NET con IChatClient: guida completa all’integrazione]]></title><description><![CDATA[Introduzione: l’astrazione che unifica i servizi LLMIntegrare Large Language Model in .NET ha sempre comportato un problema: ogni servizio (OpenAI, Azure OpenAI, Ollama, Claude) ha il proprio SDK con API diverse. IChatClient della libreria Microsoft.Extensions.AI risolve questo problema fornendo un’astrazione unificata. Scrivi una volta, cambia provider senza modificare la logica applicativa.Cosa è IChatClient?IChatClient è un’interfaccia che rappresenta un client per servizi AI con capacità chat. Astrae i dettagli di comunicazione con LLM remoti o locali, permettendo di:Inviare e ricevere messaggi con contenuto multi-modale (testo, immagini, audio)Ottenere risposte complete o streaming incrementaleMantenere contesto di conversazioneUsare funzionalità avanzate come tool calling e structured outputsL’interfaccia fa parte del pacchetto Microsoft.Extensions.AI.Abstractions, mentre Microsoft.Extensions.AI aggiunge middleware per telemetria, caching, function calling automatico e patterns familiari di dependency injection.Setup iniziale con DIIl punto di partenza è registrare il chat client nel contenitore di dependency injection. Ecco l’approccio canonico:var builder = Host.CreateApplicationBuilder();
builder.Services.AddChatClient(
    new OllamaChatClient(new Uri("http://localhost:11434"), "llama3"));
var app = builder.Build();
var chatClient = app.Services.GetRequiredService&lt;IChatClient&gt;();
In questo esempio, usiamo Ollama con il modello llama3 locale. La bellezza di questa astrazione: la stessa registrazione funziona con OpenAI, Azure OpenAI o qualsiasi provider che implementi IChatClient. Il codice che usa il client rimane invariato.Risposta semplice da un LLMIl caso più basilare: inviare un prompt e ottenere una risposta:var response = await chatClient.GetResponseAsync("What is .NET? Reply in 50 words max.");
Console.WriteLine(response.Message.Text);
Il metodo GetResponseAsync restituisce un oggetto ChatCompletion con il messaggio della risposta. Semplice, sincrono dal punto di vista dello sviluppatore (anche se asincrono sottostante).Streaming per risposte lunghePer applicazioni interattive come chatbot, lo streaming è essenziale. Permette all’utente di vedere il testo apparire gradualmente, come in ChatGPT:var chatResponse = "";
await foreach (var item in chatClient.GetStreamingResponseAsync(chatHistory))
{
    Console.Write(item.Text);
    chatResponse += item.Text;
}
Il metodo GetStreamingResponseAsync ritorna un IAsyncEnumerable&lt;StreamingChatCompletionUpdate&gt;. Ogni item contiene un frammento di testo che puoi visualizzare in tempo reale.Conversazioni multi-turno con cronologiaMantenere una conversazione richiede di raccogliere la storia dei messaggi. Ecco un loop interattivo completo:var chatHistory = new List&lt;ChatMessage&gt;();
while (true)
{
    Console.Write("You: ");
    var userPrompt = Console.ReadLine();
    
    chatHistory.Add(new ChatMessage(ChatRole.User, userPrompt));
    
    var chatResponse = "";
    Console.Write("Assistant: ");
    await foreach (var item in chatClient.GetStreamingResponseAsync(chatHistory))
    {
        Console.Write(item.Text);
        chatResponse += item.Text;
    }
    Console.WriteLine();
    
    chatHistory.Add(new ChatMessage(ChatRole.Assistant, chatResponse));
}
Ogni turno aggiunge alla lista: il user message, poi il response dell’assistant. Al turno successivo, passi l’intera cronologia a GetStreamingResponseAsync. L’LLM usa questo contesto per mantenere coerenza conversazionale.Structured output: JSON tipizzatoSpesso vuoi che l’LLM restituisca dati strutturati (JSON). Puoi chiederlo esplicitamente nel prompt:var prompt = $"""
You will receive an article and extract its metadata.
Respond ONLY with valid JSON following this format without any deviation.

{{
    "title": "...",
    "summary": "...",
    "keywords": ["...", "..."]
}}

Article:
{File.ReadAllText("article.md")}
""";

var response = await chatClient.GetResponseAsync(prompt);
var jsonText = response.Message.Text;
var metadata = JsonSerializer.Deserialize&lt;ArticleMetadata&gt;(jsonText);
L’approccio funziona, ma richiede gestione manuale di parsing e validazione. C’è una soluzione migliore.Deserialization tipizzata con genericsLa libreria Microsoft.Extensions.AI supporta il generic GetResponseAsync&lt;T&gt; che deserializza automaticamente il JSON in una classe C#:public class ArticleMetadata
{
    public string Title { get; set; } = string.Empty;
    public string Summary { get; set; } = string.Empty;
    public string[] Keywords { get; set; } = [];
}

var metadata = await chatClient.GetResponseAsync&lt;ArticleMetadata&gt;(prompt);
Console.WriteLine($"Title: {metadata.Result.Title}");
Console.WriteLine($"Keywords: {string.Join(", ", metadata.Result.Keywords)}");
Questa API offre sicurezza in fase di compilazione e supporto IDE completo per il refactoring. Se cambi la struttura di ArticleMetadata, il compilatore avvisa i punti di utilizzo.Portabilità tra provider: da locale a cloudUna delle promesse di IChatClient è la portabilità. Ecco come implementare una strategia “local in dev, cloud in prod”:// Avvio locale con Ollama
if (app.Environment.IsDevelopment())
{
    builder.Services.AddChatClient(
        new OllamaChatClient(new Uri("http://localhost:11434"), "mistral"));
}
else
{
    // Avvio cloud con Azure OpenAI
    builder.Services.AddChatClient(
        new AzureOpenAIClient(
            new Uri(azureEndpoint),
            new DefaultAzureCredential()).AsChatClient());
}
Il resto dell’applicazione non cambia. Chiede semplicemente IChatClient al DI container e riceve l’implementazione appropriata. Niente hardcoding, niente API specifiche sparse nel codice.Middleware per telemetria e cachingIl pacchetto Microsoft.Extensions.AI fornisce middleware composabile. Uno uso comune è aggiungere OpenTelemetry:var builder = Host.CreateApplicationBuilder();

// Registra OpenTelemetry
builder.Services.AddOpenTelemetry()
    .WithTracing(tracing =&gt; tracing
        .AddAspNetCoreInstrumentation()
        .AddHttpClientInstrumentation());

// Registra il chat client con middleware di telemetria
builder.Services.AddChatClient(baseChatClient)
    .UseOpenTelemetry(builder.Services.BuildServiceProvider()
        .GetRequiredService&lt;ILoggerFactory&gt;());
Con questo setup, ogni chiamata a IChatClient genera automaticamente span OpenTelemetry tracciabili in strumenti come Application Insights o Jaeger. Nessuna strumentazione manuale necessaria.Integrazione con il framework AgentIl framework Agent di Microsoft costruisce sopra IChatClient aggiungendo astrazioni a livello agent: gestione persistente del contesto, tool calling automatico, prompt di sistema, API streaming pulita. Se usi agent, IChatClient rimane il cuore della comunicazione LLM.ConclusioneIChatClient rappresenta una maturazione nell’integrazione LLM in .NET. Invece di accoppiare il codice a provider specifici, definisci un’astrazione e lascia che l’infrastruttura scelga l’implementazione. Lo streaming, la deserialization tipizzata, la composizione di middleware e la portabilità del provider diventano proprietà di prima classe dell’architettura.Per qualsiasi team che integra LLM in .NET 2026, IChatClient è il fondamento su cui costruire. Richiede poca configurazione iniziale e ripaga con flessibilità architetturale a lungo termine.Fonte originale: Microsoft.Extensions.AI libraries – .NET | Microsoft Learn e Working with LLMs in .NET using Microsoft.Extensions.AI]]></description><link>https://forum.androidiani.net/topic/4329c0e7-ff1a-4866-9c16-fabfef7a617b/llm-chat-in-.net-con-ichatclient-guida-completa-all-integrazione</link><guid isPermaLink="true">https://forum.androidiani.net/topic/4329c0e7-ff1a-4866-9c16-fabfef7a617b/llm-chat-in-.net-con-ichatclient-guida-completa-all-integrazione</guid><dc:creator><![CDATA[blog@spcnet.it]]></dc:creator><pubDate>Wed, 15 Apr 2026 14:37:06 GMT</pubDate></item><item><title><![CDATA[Agent Skills in .NET: tre paradigmi di composizione per gli agenti AI]]></title><description><![CDATA[Introduzione: L’evoluzione dei skill negli agent .NETGli agent AI richiedono un modo flessibile e modulare di estendere le loro capacità: questo è il ruolo dei skill. Con il framework Agent di Microsoft per .NET, gli sviluppatori dispongono di tre paradigmi complementari per definire e comporre skill, permettendo ai team di scegliere l’approccio più adatto al loro contesto.I tre paradigmi per creare skill1. Skill basati su file (File-Based Skills)L’approccio più dichiarativo parte da una struttura di directory semplice. Ogni skill è organizzato come una cartella contenente:Un file SKILL.md con metadati nel frontmatter YAMLUna sottocartella opzionale scripts/ con il codice eseguibileUna sottocartella opzionale references/ con documentazione di supportoQuesto paradigma è particolarmente vantaggioso per i team che vogliono gestire i skill come assets indipendenti dentro un repository condiviso. Il caricamento è automatico: l’agent scopre e carica i skill quando l’utente ne fa richiesta.Ecco come si registra un provider file-based:var skillsProvider = new AgentSkillsProvider(
    Path.Combine(AppContext.BaseDirectory, "skills"),
    SubprocessScriptRunner.RunAsync);
Il vantaggio decisivo è la separazione tra definizione del skill e implementazione. Non è necessario riconfigurare il codice C# per aggiungere nuovi skill; basta creare una nuova directory.2. Skill basati su classe (Class-Based Skills)Per chi preferisce la sicurezza dei tipi e il supporto IDE completo, gli skill basati su classe offrono un’alternativa fortemente tipizzata. Si eredita da AgentClassSkill&lt;T&gt; e si usano attributi di reflection per marcare le risorse e gli script:public sealed class BenefitsEnrollmentSkill : AgentClassSkill&lt;BenefitsEnrollmentSkill&gt;
{
    [AgentSkillResource("available-plans")]
    public string AvailablePlans =&gt; "Plan A, Plan B, Plan C...";
    
    [AgentSkillScript("enroll")]
    private static string Enroll(string employeeId, string planCode)
    {
        // Logica di iscrizione
        return $"Iscrizione di {employeeId} al piano {planCode} completata";
    }
}
Questo approccio è ideale per skill complessi che richiedono logica C# sofisticata. Gli attributi [AgentSkillResource] e [AgentSkillScript] permettono al framework di scoprire automaticamente quali metodi e proprietà esporre all’agent.Un vantaggio cruciale: i team possono sviluppare e distribuire skill indipendentemente come pacchetti NuGet, mantenendo il proprio ciclo di rilascio e permettendo il riuso tra progetti.3. Skill inline (Inline Code-Defined Skills)Il terzo paradigma è il più flessibile: skill definiti a runtime usando AgentInlineSkill. Sono perfetti per bridge temporanei, skill generati dinamicamente o implementazioni condizionate dallo stato dell’applicazione:var timeOffSkill = new AgentInlineSkill(
    name: "time-off-balance",
    description: "Calcola i giorni di ferie e malattia rimanenti per un dipendente...")
    .AddScript("calculate-balance", (employeeId, leaveType) =&gt; 
    {
        // Logica runtime
        return $"Giorni rimanenti: {remaining}";
    });
I skill inline supportano anche risorse dinamiche:.AddResource("policies", () =&gt; PolicyRepository.GetActivePolicies());
Questa capacità di aggiungere risorse come delegate è cruciale: le politiche possono aggiornarsi senza ricompilare l’applicazione.Composizione flessibile con AgentSkillsProviderBuilderLa vera potenza del design emerge quando si combinano tutti e tre i paradigmi in un’unica applicazione. Il builder pattern permette una composizione dichiarativa:var skillsProvider = new AgentSkillsProviderBuilder()
    .UseFileSkill(Path.Combine(AppContext.BaseDirectory, "skills"))
    .UseSkill(new BenefitsEnrollmentSkill())
    .UseSkill(timeOffSkill)
    .UseFileScriptRunner(SubprocessScriptRunner.RunAsync)
    .Build();
In questa configurazione:I skill nel filesystem vengono caricati e resi disponibiliLa classe BenefitsEnrollmentSkill registra i suoi metodi annotatiLo skill inline timeOffSkill aggiunge capacità runtimeIl framework astrae completamente il “come” carica ogni tipo di skill; l’agent li vede come una superficie unificata.Funzionalità avanzateApprovazione degli scriptPer ambienti ad alto rischio, è possibile richiedere una revisione umana prima dell’esecuzione:.UseScriptApproval(true)
In questo caso, l’agent formula il comando ma non lo esegue autonomamente; un operatore deve approvare.Filtraggio di sicurezzaQuando si condividono directory di skill tra team, il filtraggio garantisce che solo gli skill approvati siano disponibili:.UseFilter(skill =&gt; approvedSkills.Contains(skill.Frontmatter.Name))
Iniezione di dipendenzeI metodi degli skill possono ricevere IServiceProvider come parametro. Questo consente l’accesso a servizi registrati nel contenitore DI, indipendentemente dal paradigma di skill:[AgentSkillScript("send-notification")]
private static string SendNotification(string userId, IServiceProvider services)
{
    var emailService = services.GetRequiredService&lt;IEmailService&gt;();
    return emailService.SendAsync(userId, "Notification");
}
ConclusioneIl design tripartito dei skill in .NET Agent Framework non è una complicazione: è un’architettura di composizione che rispetta gli usi diversi. Gli skill basati su file servono la semplicità e la dinamica; quelli basati su classe offrono sicurezza e riusabilità via NuGet; quelli inline forniscono agilità runtime.Per i team che costruiscono sistemi agent complessi, questa flessibilità è fondamentale. Permette di iniziare in semplicità (skill inline), evolversi verso la modularità (skill basati su classe in NuGet) e mantenere agilità operativa (skill file-based per aggiustamenti dinamici) — tutto nello stesso agent, senza compromessi architetturali.Fonte originale: Agent Skills in .NET: Three Ways to Author, One Provider to Run Them — Microsoft Agent Framework Blog]]></description><link>https://forum.androidiani.net/topic/cd793990-3175-4b79-b45d-0f25c7036e88/agent-skills-in-.net-tre-paradigmi-di-composizione-per-gli-agenti-ai</link><guid isPermaLink="true">https://forum.androidiani.net/topic/cd793990-3175-4b79-b45d-0f25c7036e88/agent-skills-in-.net-tre-paradigmi-di-composizione-per-gli-agenti-ai</guid><dc:creator><![CDATA[blog@spcnet.it]]></dc:creator><pubDate>Wed, 15 Apr 2026 13:16:15 GMT</pubDate></item><item><title><![CDATA[Il Pattern Saga in .NET con Wolverine: gestire workflow distribuiti a lungo termine]]></title><description><![CDATA[Nei sistemi distribuiti, la gestione di processi di business che si estendono su più servizi e nel tempo rappresenta una delle sfide più complesse. Il Pattern Saga nasce proprio per risolvere questo problema: coordinare una sequenza di operazioni distribuite in modo affidabile, garantendo la consistenza dei dati anche in caso di errori parziali.In questo articolo esploriamo come implementare il Pattern Saga in .NET utilizzando Wolverine, un framework moderno che semplifica notevolmente la gestione di messaggi e workflow complessi grazie al suo approccio convention-driven.Cos’è il Pattern Saga?Il Pattern Saga è un meccanismo di gestione delle transazioni distribuite che sostituisce le transazioni ACID tradizionali nei sistemi a microservizi. Invece di eseguire una sequenza di operazioni come un’unica transazione atomica, la saga suddivide il processo in una serie di passi indipendenti, ciascuno con la propria logica di compensazione in caso di fallimento.Il principio fondamentale è semplice: se un passo fallisce o va in timeout, la saga esegue la logica di compensazione invece di lasciare il sistema in uno stato inconsistente. Questo approccio è particolarmente utile per processi di lunga durata come:Onboarding di nuovi utenti con email di verificaProcessi di ordine e pagamento in e-commerceWorkflow di approvazione multi-stepProcessi di provisioning di risorse cloudPerché Wolverine?Wolverine è un framework per .NET che adotta un approccio convention-driven alla messaggistica e ai workflow. A differenza di soluzioni come MassTransit o Rebus, Wolverine gestisce automaticamente routing dei messaggi, persistenza dello stato e correlazione, senza richiedere un’estesa configurazione tramite DSL per state machine.Le principali dipendenze per iniziare sono:WolverineFx (5.16.2)
WolverineFx.Postgresql (5.16.2)
WolverineFx.RabbitMQ (5.16.2)
Configurazione del progettoLa configurazione di Wolverine richiede pochi passaggi. Nell’entry point dell’applicazione, si configura il framework per utilizzare RabbitMQ come message broker e PostgreSQL per la persistenza dello stato:builder.Host.UseWolverine(options =&gt;
{
    options.UseRabbitMqUsingNamedConnection("rmq")
        .AutoProvision()
        .UseConventionalRouting();

    options.Policies.DisableConventionalLocalRouting();
    options.PersistMessagesWithPostgresql(connectionString!);
});
Dettagli importanti di questa configurazione:AutoProvision(): crea automaticamente exchange e code in RabbitMQUseConventionalRouting(): instrada i messaggi in base ai nomi dei tipiDisableConventionalLocalRouting(): forza tutti i messaggi attraverso RabbitMQPersistMessagesWithPostgresql(): archivia stato della saga e messaggi; crea una tabella per saga con serializzazione JSONDefinizione dei messaggiOgni passo della saga è rappresentato da un messaggio. Definiamo tutti i tipi di messaggio per un processo di onboarding utente:public record SendVerificationEmail(Guid UserId, string Email);
public record VerificationEmailSent(Guid Id);
public record VerifyUserEmail(Guid Id);
public record SendWelcomeEmail(Guid UserId, string Email, string FirstName);
public record WelcomeEmailSent(Guid Id);
public record OnboardingTimedOut(Guid Id) : TimeoutMessage(5.Minutes());
Il record OnboardingTimedOut estende TimeoutMessage di Wolverine: questo fa sì che il messaggio venga consegnato automaticamente dopo 5 minuti, eliminando la necessità di scheduler esterni per gestire i timeout.Implementazione della classe SagaLa saga viene implementata come una classe che estende Saga. Lo stato viene mantenuto come proprietà della classe:public class UserOnboardingSaga : Saga
{
    public Guid Id { get; set; }
    public string Email { get; set; } = string.Empty;
    public string FirstName { get; set; } = string.Empty;
    public string LastName { get; set; } = string.Empty;
    public bool IsVerificationEmailSent { get; set; }
    public bool IsEmailVerified { get; set; }
    public bool IsWelcomeEmailSent { get; set; }
}
Il metodo Start: avvio della sagaIl metodo statico Start è il factory method che inizia la saga. Restituisce una tupla contenente l’istanza della saga, il comando iniziale e il messaggio di timeout pianificato:public static (
    UserOnboardingSaga,
    SendVerificationEmail,
    OnboardingTimedOut) Start(
        UserRegistered @event,
        ILogger&lt;UserOnboardingSaga&gt; logger)
{
    var saga = new UserOnboardingSaga
    {
        Id = @event.Id,
        Email = @event.Email,
        FirstName = @event.FirstName,
        LastName = @event.LastName,
    };

    return (
        saga,
        new SendVerificationEmail(saga.Id, saga.Email),
        new OnboardingTimedOut(saga.Id));
}
Wolverine persiste automaticamente la saga e consegna tutti i messaggi restituiti. Elegante e senza boilerplate.Metodi Handle: gestione degli eventiI metodi Handle elaborano i messaggi in arrivo. Se restituiscono void, aggiornano solo lo stato; se restituiscono un messaggio, causano l’invio del passo successivo:public void Handle(VerificationEmailSent @event, ILogger&lt;UserOnboardingSaga&gt; logger)
{
    logger.LogInformation("Email di verifica inviata per l'utente {UserId}", Id);
    IsVerificationEmailSent = true;
}

public SendWelcomeEmail Handle(VerifyUserEmail command, ILogger&lt;UserOnboardingSaga&gt; logger)
{
    logger.LogInformation("Email verificata per l'utente {UserId}", Id);
    IsEmailVerified = true;
    return new SendWelcomeEmail(Id, Email, FirstName);
}

public void Handle(WelcomeEmailSent @event, ILogger&lt;UserOnboardingSaga&gt; logger)
{
    logger.LogInformation("Onboarding completato per l'utente {UserId}", Id);
    IsWelcomeEmailSent = true;
    MarkCompleted(); // Elimina lo stato dal database
}
Gestione del timeout e compensazioneIl timeout è un cittadino di prima classe in Wolverine. Se l’utente non verifica l’email entro 5 minuti, il handler del timeout gestisce la compensazione:public void Handle(OnboardingTimedOut timeout, ILogger&lt;UserOnboardingSaga&gt; logger)
{
    if (IsEmailVerified)
    {
        logger.LogInformation(
            "Timeout ignorato - email già verificata per {UserId}", Id);
        return;
    }

    logger.LogWarning(
        "Onboarding scaduto per {UserId} - email non verificata", Id);
    MarkCompleted();
}
Gestione dei messaggi “orfani” con NotFoundWolverine richiede la gestione esplicita del caso in cui arrivi un messaggio per una saga già terminata, tramite metodi statici NotFound:public static void NotFound(VerifyUserEmail command, ILogger&lt;UserOnboardingSaga&gt; logger)
{
    logger.LogWarning("VerifyEmail ricevuto ma la saga {Id} non esiste più", command.Id);
}

public static void NotFound(OnboardingTimedOut timeout, ILogger&lt;UserOnboardingSaga&gt; logger)
{
    logger.LogInformation("Timeout per la saga già completata {Id}", timeout.Id);
}
Correlazione dei messaggi e gestione della concorrenzaWolverine correla automaticamente i messaggi alle istanze della saga cercando nell’ordine: attributo [SagaIdentity], proprietà {SagaTypeName}Id, oppure proprietà Id. Non è necessaria alcuna configurazione esplicita per i casi standard.Per la concorrenza, Wolverine applica di default il controllo di concorrenza ottimistico: quando più messaggi per la stessa saga arrivano contemporaneamente, uno riesce mentre gli altri vengono ritentati automaticamente. Attenzione: non invocare IMessageBus.InvokeAsync() all’interno dei handler della stessa saga, ma usare sempre i messaggi in cascata (valori di ritorno) per evitare problemi con dati obsoleti.Opzioni di persistenzaWolverine supporta tre strategie per la persistenza dello stato della saga:Lightweight Storage: serializza lo stato come JSON in tabelle dedicate per saga, zero configurazione ORMMarten: archivia le saghe come documenti con concorrenza ottimistica e ID fortemente tipizzatiEntity Framework Core: mappa le saghe su tabelle queryabili, abilitando commit in singola transazione con altri datiIl flusso completoIl percorso “happy path” dell’onboarding segue questi passi:L’evento UserRegistered attiva il metodo Start()Viene creata l’istanza della saga, inviato SendVerificationEmail e pianificato OnboardingTimedOutVerificationEmailSent aggiorna lo stato della sagaVerifyUserEmail ricevuto, viene inviato in cascata SendWelcomeEmailWelcomeEmailSent completa il workflow, la saga viene eliminataSe VerifyUserEmail non arriva entro 5 minuti, OnboardingTimedOut gestisce la compensazione e termina la saga.ConclusioneWolverine offre un approccio sorprendentemente pulito all’implementazione del Pattern Saga in .NET. La scelta convention-driven elimina gran parte del boilerplate tipico di altri framework, consentendo di concentrarsi sulla logica di business. La gestione automatica di persistenza, routing e correlazione dei messaggi, unita al supporto nativo per timeout e compensazione, lo rende una scelta solida per workflow distribuiti complessi.Per i team che lavorano con architetture a microservizi in .NET, Wolverine merita certamente una valutazione approfondita come alternativa moderna ai pattern tradizionali di orchestrazione.Fonte originale: Implementing the Saga Pattern With Wolverine — Milan Jovanović]]></description><link>https://forum.androidiani.net/topic/2bf98642-986c-48ff-aff2-db6c32607d8a/il-pattern-saga-in-.net-con-wolverine-gestire-workflow-distribuiti-a-lungo-termine</link><guid isPermaLink="true">https://forum.androidiani.net/topic/2bf98642-986c-48ff-aff2-db6c32607d8a/il-pattern-saga-in-.net-con-wolverine-gestire-workflow-distribuiti-a-lungo-termine</guid><dc:creator><![CDATA[blog@spcnet.it]]></dc:creator><pubDate>Tue, 14 Apr 2026 07:46:54 GMT</pubDate></item><item><title><![CDATA[.NET Aspire 13.2: la modalità isolata risolve i conflitti di porta nello sviluppo parallelo]]></title><description><![CDATA[Chiunque abbia lavorato con .NET Aspire su progetti reali si è prima o poi scontrato con il classico errore: “Port 17370 is already in use”. Capita quando si prova ad avviare una seconda istanza dell’AppHost — magari su un altro branch, o in un altro terminale — e le porte predefinite sono già occupate dalla prima istanza in esecuzione. Con Aspire 13.2, questo problema ha finalmente una soluzione elegante: la modalità isolata (--isolated).In questo articolo vediamo nel dettaglio come funziona questa nuova funzionalità, i casi d’uso pratici, e le altre novità rilevanti di questa release.Il problema: conflitti di porta nello sviluppo paralleloIn un tipico progetto .NET Aspire, l’AppHost configura i binding delle porte per tutti i servizi nell’orchestrazione: la dashboard su una porta, l’API su un’altra, il database su un’altra ancora. Questi binding sono statici per default, e questo crea problemi immediati quando si vuole eseguire due istanze dello stesso AppHost contemporaneamente:Sviluppo su due branch in parallelo con git worktreesTest di integrazione che richiedono un AppHost “live” mentre si continua a sviluppareAgenti AI che creano automaticamente worktree separati per task paralleliPipeline CI/CD locali che eseguono più istanze dello stesso progettoLa soluzione tradizionale era modificare manualmente i port binding nella configurazione — un approccio fragile, soggetto a errori e difficile da gestire in team.La soluzione: la flag --isolatedAspire 13.2 introduce la flag --isolated che risolve il problema alla radice. L’utilizzo è semplicissimo:aspire run --isolated # oppure aspire start --isolatedQuando si passa --isolated, la CLI genera un identificativo univoco per l’istanza corrente, e questo ID guida due comportamenti fondamentali:1. Randomizzazione automatica delle porteInvece di usare le porte definite staticamente nell’AppHost, ogni istanza isolata riceve un range di porte casuali disponibili. Dove un run normale potrebbe bindare i servizi su 8080, 8081, 8082, due istanze isolate potrebbero usare rispettivamente:Istanza 1: 15234, 15235, 15236Istanza 2: 22891, 22892, 22893La cosa notevole è che il codice dell’applicazione non necessita alcuna modifica: il service discovery di Aspire risolve gli endpoint dinamicamente a runtime, quindi i servizi si “trovano” a prescindere dalle porte assegnate.2. Isolamento dei user secretsLa configurazione rimane completamente separata per ogni istanza. Connection string, chiavi API e altre variabili d’ambiente non si “contaminano” tra run diversi, anche quando puntano a risorse Azure o database con nomi diversi. Questo è particolarmente importante in scenari di test dove ogni istanza deve operare in modo completamente autonomo.Casi d’uso praticiGit worktrees multipliIl caso d’uso più comune: sviluppo su due branch in parallelo.# Terminale 1 - branch principale cd ~/projects/myapp-main aspire run --isolated  # Terminale 2 - feature branch cd ~/projects/myapp-feature-xyz aspire run --isolatedEntrambe le istanze partono senza conflitti, con porte diverse assegnate automaticamente. La dashboard di Aspire di ciascuna istanza è accessibile su porte diverse, e i servizi di ciascuna istanza sono completamente separati.Test di integrazione con AppHost liveUn pattern molto utile: eseguire test di integrazione contro un AppHost “live” mentre si continua a sviluppare sull’AppHost principale.# AppHost per sviluppo interattivo aspire run --isolated  # In un altro terminale: avvia i test che usano il loro AppHost dedicato dotnet test --isolated-apphostCon la modalità isolata, i test non interferiscono con l’ambiente di sviluppo e viceversa.Sviluppo agenticoQuesto è il caso d’uso che ha spinto direttamente lo sviluppo di questa feature. Gli agenti AI in VS Code Copilot possono creare automaticamente git worktree separati per task paralleli. Con --isolated, ogni agente può avviare il proprio AppHost nella sua directory di lavoro senza conflitti con la sessione principale dello sviluppatore.Aspire 13.2 include anche il comando aspire agent init (rinominato da aspire mcp init) che configura automaticamente gli agenti per usare --isolated con i worktree git.Nuovi comandi CLI in Aspire 13.2La modalità isolata non è l’unica novità della CLI. Aspire 13.2 introduce una serie di nuovi comandi operativi che rendono la gestione delle istanze molto più potente:aspire ps — lista delle istanze attiveElenca tutti gli AppHost Aspire in esecuzione sulla macchina, con le relative informazioni (porte, stato, ID istanza). Utile specialmente quando si hanno più istanze isolate attive contemporaneamente e si vuole sapere cosa sta girando.aspire ps # Output: # ID           PROJECT          STATUS    DASHBOARD # abc123       myapp-main       Running   http://localhost:15234 # def456       myapp-feature    Running   http://localhost:22891aspire describe — dettagli sulle risorseAccede ai dettagli di una risorsa specifica direttamente dal terminale, senza dover aprire la dashboard:aspire describe api # Mostra endpoint, variabili d'ambiente, stato health, ecc.aspire doctor — diagnostica dell’ambienteEsegue un controllo completo dell’ambiente di sviluppo: verifica che tutte le dipendenze siano installate correttamente (Docker, .NET SDK, ecc.) e segnala eventuali problemi di configurazione.aspire wait — attesa su uno stato specificoBlocca l’esecuzione in script di automazione finché una risorsa non raggiunge uno stato specifico. Utile in pipeline CI/CD o in script di startup:aspire run --isolated &amp; aspire wait --resource api --state Running # Ora l'API è sicuramente up, posso eseguire i testaspire export — export di telemetria e datiCattura telemetria e dati delle risorse in formato JSON per analisi offline o per integrazione con altri strumenti.TypeScript AppHost in previewUna delle novità più interessanti di Aspire 13.2 è il supporto preview per scrivere l’AppHost in TypeScript. Fino ad ora, l’AppHost era necessariamente un progetto C#. Con questa release, è possibile usare TypeScript con una sintassi idiomatica:import { createBuilder } from '@aspire/hosting';  const builder = await createBuilder();  // Aggiunge Redis come risorsa const cache = await builder.addRedis("cache");  // Aggiunge un servizio Node.js con dipendenza da Redis const api = await builder.addNpmApp("api", "../api")     .withReference(cache);  await builder.build().run();Il TypeScript AppHost funziona come un processo guest che comunica tramite JSON-RPC con l’orchestrator .NET sottostante. La CLI gestisce automaticamente la generazione degli SDK TypeScript quando si esegue aspire add, e aspire restore li rigenera se necessario.Questa funzionalità è ancora in preview e non è raccomandata per produzione, ma è un segnale chiaro della direzione che sta prendendo Aspire: abbracciare anche gli sviluppatori TypeScript/Node.js, non solo quelli .NET.Miglioramenti alla dashboardExport e import di telemetriaLa dashboard introduce un dialog centralizzato “Manage logs and telemetry” che permette di:Esportare risorse e telemetria come JSONEsportare variabili d’ambiente come file .envImportare dati da sessioni precedentiAPI HTTP per telemetriaNuovo endpoint /api/telemetry sulla dashboard che permette query programmatiche dei dati di telemetria con supporto streaming NDJSON. Utile per integrare la telemetria di Aspire con strumenti di monitoring esterni o script di analisi.Impostazione parametri dalla UIÈ ora possibile impostare i parametri delle risorse direttamente dalla dashboard, con opzione di salvataggio nei user secrets. Questo elimina la necessità di modificare manualmente i file di configurazione per cambiare un parametro durante il debug.Miglioramenti al visualizzatore GenAIChi usa Aspire con workload AI troverà utili i miglioramenti al GenAI visualizer: migliore gestione di schemi complessi, payload troncati, testo non-ASCII e navigazione tra definizioni di tool.Altre novità rilevantiEndpoint MCP per i serviziÈ possibile dichiarare server Model Context Protocol (MCP) direttamente nell’AppHost con il nuovo metodo WithMcpServer():var api = builder.AddProject&lt;Projects.MyApi&gt;("api")     .WithMcpServer("/mcp");Aspire gestirà automaticamente la discovery dell’endpoint MCP, rendendolo disponibile agli agenti AI che operano nell’ambiente.Docker Compose publishing stabileL’integrazione con Docker Compose passa da prerelease a stabile. È ora possibile generare un docker-compose.yaml completo direttamente dal modello di app Aspire con aspire publish --format docker-compose.Azure Virtual NetworkNuovo pacchetto Aspire.Hosting.Azure.Network per la gestione di reti virtuali Azure:var vnet = builder.AddAzureVirtualNetwork("vnet"); var subnet = vnet.AddSubnet("web", "10.0.1.0/24"); var natGateway = vnet.AddNatGateway("nat"); Breaking changes da tenere a menteSe stai aggiornando un progetto Aspire esistente a 13.2, ci sono alcune breaking changes da considerare:Variabili Service Discovery: usano ora lo schema endpoint invece del nome endpointFile di configurazione: preferenza per aspire.config.json unificato (migrazione automatica al primo run)Comandi risorse: resource-start → start, resource-stop → stopDashboard API: ora opt-in per dashboard standalonePacchetto AIFoundry: Aspire.Hosting.Azure.AIFoundry → Aspire.Hosting.FoundryWithSecretBuildArg: rinominato in WithBuildSecretPer aggiornare, usa:aspire update --self   # aggiorna la CLI aspire update          # aggiorna i pacchetti del progettoConclusioneAspire 13.2 è una release sostanziosa che affronta problemi concreti del workflow di sviluppo. La modalità --isolated è probabilmente la novità più impattante per il day-to-day: risolve un pain point reale in modo elegante, senza richiedere modifiche al codice dell’applicazione.L’aggiunta del TypeScript AppHost in preview è un segnale importante della direzione di Aspire verso un ecosistema più inclusivo, mentre i nuovi comandi CLI (ps, describe, doctor, wait) rendono Aspire molto più adatto a workflow di automazione e sviluppo agentico.Chi lavora già con Aspire troverà questo aggiornamento decisamente consigliato. Chi non lo ha ancora provato, potrebbe essere il momento giusto per iniziare — soprattutto se lavora con architetture microservizi in .NET.Fonti: Running Multiple Instances of an Aspire AppHost Without Port Conflicts · What’s new in Aspire 13.2]]></description><link>https://forum.androidiani.net/topic/515087a3-6537-4ff3-9a75-f1e4bc491841/.net-aspire-13.2-la-modalità-isolata-risolve-i-conflitti-di-porta-nello-sviluppo-parallelo</link><guid isPermaLink="true">https://forum.androidiani.net/topic/515087a3-6537-4ff3-9a75-f1e4bc491841/.net-aspire-13.2-la-modalità-isolata-risolve-i-conflitti-di-porta-nello-sviluppo-parallelo</guid><dc:creator><![CDATA[blog@spcnet.it]]></dc:creator><pubDate>Mon, 13 Apr 2026 13:42:44 GMT</pubDate></item></channel></rss>