<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="it"><generator uri="https://jekyllrb.com/" version="4.3.3">Jekyll</generator><link href="https://www.devpills.net/feed.xml" rel="self" type="application/atom+xml" /><link href="https://www.devpills.net/" rel="alternate" type="text/html" hreflang="it" /><updated>2026-09-28T21:56:14+02:00</updated><id>https://www.devpills.net/feed.xml</id><title type="html">DevPills</title><subtitle>Tutorial pratici, buone pratiche e approfondimenti tecnici per sviluppatori</subtitle><author><name>Alessandro Mengoli</name></author><entry xml:lang="it"><title type="html">Costruire applicazioni .NET resilienti</title><link href="https://www.devpills.net/microsoft-extensions-resilience/" rel="alternate" type="text/html" title="Costruire applicazioni .NET resilienti" /><published>2026-09-27T00:00:00+02:00</published><updated>2026-09-27T00:00:00+02:00</updated><id>https://www.devpills.net/microsoft-extensions-resilience</id><content type="html" xml:base="https://www.devpills.net/microsoft-extensions-resilience/"><![CDATA[<p>Quando sviluppiamo un’applicazione tendiamo spesso, forse con un eccesso di ottimismo, a dare per scontato che tutto andrà bene. Questo è particolarmente vero quando comunichiamo con altri servizi o sistemi di terze parti.</p>

<p>Diamo per scontato che il servizio esterno sia sempre disponibile, che la rete funzioni, che le risposte arrivino velocemente e che, una volta stabilita la comunicazione, tutto continui a funzionare nello stesso modo.</p>

<p>Prendiamo un caso molto semplice:</p>

<p><img src="/assets/images/posts/service-dependency.svg" alt="Our API comunica con Catalog API" /></p>

<p>Normalmente Catalog API risponde in 100 ms e tutto funziona perfettamente.</p>

<p>Poi, per qualche secondo, Catalog API restituisce <code class="language-plaintext highlighter-rouge">503 Service Unavailable</code>. Oppure impiega 8 secondi a rispondere, smette del tutto di rispondere o viene sommersa di richieste quando la nostra applicazione riceve dieci volte il traffico previsto.</p>

<p>Una dependency può diventare non disponibile, lenta oppure sovraccaricata. E il nostro sistema deve essere progettato sapendo che, prima o poi, succederà.</p>

<p>È qui che entra in gioco la <strong>resilienza</strong>.</p>

<h2 id="da-fault-handling-a-resilience">Da fault handling a resilience</h2>

<p>La prima soluzione che viene naturale è gestire l’errore.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">try</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">response</span> <span class="p">=</span> <span class="k">await</span> <span class="n">catalogClient</span><span class="p">.</span><span class="nf">GetAsync</span><span class="p">(</span><span class="s">"/products/42"</span><span class="p">);</span>
    <span class="n">response</span><span class="p">.</span><span class="nf">EnsureSuccessStatusCode</span><span class="p">();</span>
<span class="p">}</span>
<span class="k">catch</span> <span class="p">(</span><span class="n">HttpRequestException</span> <span class="n">ex</span><span class="p">)</span>
<span class="p">{</span>
    <span class="c1">// Handle error</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Non c’è nulla di sbagliato, ma abbiamo semplicemente deciso cosa fare <strong>dopo</strong> che l’operazione è fallita.</p>

<p>Rendere resiliente un’applicazione significa fare un passo in più.</p>

<p>Dobbiamo iniziare a chiederci:</p>

<ul>
  <li>Cosa succede se la chiamata fallisce?</li>
  <li>Cosa succede se il servizio è lento?</li>
  <li>Cosa succede se continua a fallire?</li>
  <li>Cosa succede se stiamo inviando troppe richieste?</li>
  <li>Cosa succede se il fallimento è solo temporaneo?</li>
</ul>

<p>La resilienza consiste quindi nel progettare esplicitamente come l’applicazione deve comportarsi in presenza di failure, cercando dove possibile di recuperare e, soprattutto, evitando che il problema di una dependency si propaghi al resto del sistema.</p>

<p>Ed è qui che <code class="language-plaintext highlighter-rouge">Microsoft.Extensions.Resilience</code> viene in nostro soccorso.</p>

<h2 id="microsoftextensionsresilience">Microsoft.Extensions.Resilience</h2>

<p>Chi sviluppa applicazioni .NET resilienti da qualche anno probabilmente conosce già Polly, una delle librerie più diffuse nell’ecosistema .NET per implementare strategie come retry, circuit breaker e timeout.</p>

<p>Con .NET 8, Microsoft ha introdotto due nuovi package costruiti sopra Polly v8: <code class="language-plaintext highlighter-rouge">Microsoft.Extensions.Resilience</code> e <code class="language-plaintext highlighter-rouge">Microsoft.Extensions.Http.Resilience</code>, come spiega nell’<a href="https://devblogs.microsoft.com/dotnet/building-resilient-cloud-services-with-dotnet-8/">articolo sulle novità di .NET 8</a>.</p>

<p>Oggi abbiamo quindi:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">Microsoft.Extensions.Resilience</code>, che fornisce i meccanismi generali per costruire e integrare resilience pipeline.</li>
  <li><code class="language-plaintext highlighter-rouge">Microsoft.Extensions.Http.Resilience</code>, che aggiunge funzionalità specifiche per <code class="language-plaintext highlighter-rouge">HttpClient</code>.</li>
</ul>

<p>Il precedente package <code class="language-plaintext highlighter-rouge">Microsoft.Extensions.Http.Polly</code> è invece deprecato e Microsoft raccomanda di utilizzare i nuovi package per le nuove applicazioni. La <a href="https://learn.microsoft.com/en-us/dotnet/core/resilience/">documentazione sulla resilienza in .NET</a> raccoglie le indicazioni aggiornate.</p>

<p>Per chi arriva da Polly il modello concettuale rimane familiare, anche perché i nuovi package Microsoft sono costruiti proprio sopra Polly.
Prima di vedere il codice, però, conviene capire quali problemi possiamo risolvere.</p>

<h2 id="una-panoramica-delle-strategie">Una panoramica delle strategie</h2>

<p>Una <strong>resilience pipeline</strong> è una sequenza di strategie che avvolge l’operazione che vogliamo eseguire.</p>

<p>Nel caso di una chiamata HTTP possiamo immaginarla così:</p>

<p><img src="/assets/images/posts/resilience-pipeline.svg" alt="Pipeline standard di resilienza HTTP" /></p>

<p>Questa è proprio la pipeline standard fornita da <code class="language-plaintext highlighter-rouge">Microsoft.Extensions.Http.Resilience</code>. Microsoft la compone, nell’ordine, con rate limiter, timeout complessivo, retry, circuit breaker e timeout del singolo tentativo.</p>

<p>Per ora ci basta capire a grandi linee il ruolo di ognuna.</p>

<p>Il rate limiter controlla quante operazioni possono essere eseguite contemporaneamente verso la dependency (di default 1000, senza coda). L’obiettivo è evitare di esercitare una pressione eccessiva sul servizio che stiamo chiamando.</p>

<p>Il total timeout definisce quanto tempo concediamo all’intera operazione, compresi eventuali tentativi successivi.</p>

<p>Il retry permette di eseguire nuovamente un’operazione quando il failure potrebbe essere transitorio.</p>

<p>Il circuit breaker interrompe temporaneamente le chiamate quando la percentuale di failure verso una dependency supera una soglia (di default il 10% delle richieste in una finestra di 30 secondi, con almeno 100 richieste).</p>

<p>L’attempt timeout limita invece il tempo concesso a ogni singolo tentativo.</p>

<p>E configurarli può essere sorprendentemente semplice. Dopo aver aggiunto il package:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dotnet add package Microsoft.Extensions.Http.Resilience
</code></pre></div></div>

<p>basta una riga:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">services</span>
    <span class="p">.</span><span class="nf">AddHttpClient</span><span class="p">(</span><span class="s">"catalog"</span><span class="p">,</span> <span class="n">client</span> <span class="p">=&gt;</span>
    <span class="p">{</span>
        <span class="n">client</span><span class="p">.</span><span class="n">BaseAddress</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Uri</span><span class="p">(</span><span class="s">"https://catalog.example.com"</span><span class="p">);</span>
    <span class="p">})</span>
    <span class="p">.</span><span class="nf">AddStandardResilienceHandler</span><span class="p">();</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">AddStandardResilienceHandler()</code> associa al nostro <code class="language-plaintext highlighter-rouge">HttpClient</code> proprio la pipeline standard appena vista.</p>

<p>Questo non significa, però, che sia sufficiente aggiungere quella riga per rendere magicamente resiliente qualsiasi applicazione.</p>

<h2 id="non-sempre-riprovare-è-una-buona-idea">Non sempre riprovare è una buona idea</h2>

<p>Prendiamo il retry.</p>

<p>Se ricevessimo <code class="language-plaintext highlighter-rouge">503 Service Unavailable</code>, il problema potrebbe essere temporaneo e ritentare l’operazione può avere perfettamente senso.</p>

<p>Ma se invece ricevessimo <code class="language-plaintext highlighter-rouge">400 Bad Request</code> aspettare un secondo e inviare esattamente la stessa richiesta difficilmente cambierà il risultato. Infatti la pipeline standard non lo fa: retry e circuit breaker gestiscono solo gli errori <code class="language-plaintext highlighter-rouge">5xx</code>, <code class="language-plaintext highlighter-rouge">408 Request Timeout</code>, <code class="language-plaintext highlighter-rouge">429 Too Many Requests</code>, le <code class="language-plaintext highlighter-rouge">HttpRequestException</code> e i timeout.</p>

<p>Ancora più interessante è il caso di <code class="language-plaintext highlighter-rouge">POST /payments</code> non idempotente. Se non sappiamo se il primo tentativo sia stato effettivamente processato, eseguire automaticamente la stessa operazione una seconda volta potrebbe significare effettuare due pagamenti.</p>

<p>E qui la pipeline standard non ci protegge: di default ritenta le richieste <strong>per qualsiasi metodo HTTP</strong>, <code class="language-plaintext highlighter-rouge">POST</code> compreso. Se il nostro client esegue operazioni non idempotenti, dobbiamo dirlo esplicitamente:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">services</span>
    <span class="p">.</span><span class="nf">AddHttpClient</span><span class="p">(</span><span class="s">"payments"</span><span class="p">,</span> <span class="n">client</span> <span class="p">=&gt;</span>
    <span class="p">{</span>
        <span class="n">client</span><span class="p">.</span><span class="n">BaseAddress</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Uri</span><span class="p">(</span><span class="s">"https://payments.example.com"</span><span class="p">);</span>
    <span class="p">})</span>
    <span class="p">.</span><span class="nf">AddStandardResilienceHandler</span><span class="p">(</span><span class="n">options</span> <span class="p">=&gt;</span>
    <span class="p">{</span>
        <span class="c1">// Nessun retry per POST, PUT, PATCH, DELETE e CONNECT</span>
        <span class="n">options</span><span class="p">.</span><span class="n">Retry</span><span class="p">.</span><span class="nf">DisableForUnsafeHttpMethods</span><span class="p">();</span>
    <span class="p">});</span>
</code></pre></div></div>

<p>Ogni strategia di resilienza introduce quindi anche dei <strong>trade-off</strong>.</p>

<p>Persino un retry apparentemente innocuo genera traffico aggiuntivo. Se centinaia di istanze iniziano contemporaneamente a effettuare retry verso un servizio già in difficoltà, possiamo contribuire noi stessi a peggiorare il problema. La pipeline standard mitiga questo rischio con un backoff esponenziale con jitter, che distribuisce nel tempo i tentativi, e con il circuit breaker, che smette di chiamare una dependency chiaramente in difficoltà. Ma mitigare non significa eliminare.</p>

<p>La resilienza, quindi, non significa riprovare sempre. Significa capire quale failure stiamo cercando di gestire e quale comportamento vogliamo ottenere.</p>

<h2 id="e-polly">E Polly?</h2>

<p>Dato che <code class="language-plaintext highlighter-rouge">Microsoft.Extensions.Resilience</code> è costruito sopra Polly, vale la pena aprire una piccola parentesi.</p>

<p>Nel luglio 2026 Polly ha <a href="https://thepollyproject.org/2026/07/14/polly-osmf-announcement.html">annunciato l’adozione dell’Open Source Maintenance Fee (OSMF)</a>. Dal 16 novembre 2026, per le organizzazioni che rientrano nei criteri indicati dal progetto (ricavano almeno 20.000 dollari da prodotti che utilizzano Polly), la Maintenance Fee annunciata è di 20 dollari al mese per organizzazione. Il codice continua comunque a essere open source e la licenza di Polly non cambia.</p>

<p>C’è però una distinzione importante: secondo le <a href="https://opensourcemaintenancefee.org/consumers/faq/">FAQ per chi usa software soggetto a OSMF</a>, la fee riguarda le dipendenze dirette, non quelle transitive. Se utilizziamo <code class="language-plaintext highlighter-rouge">Microsoft.Extensions.Resilience</code> senza referenziare direttamente Polly, Polly rimane una dipendenza transitiva del package Microsoft.</p>

<p>Attenzione però: la distinzione riguarda i package referenziati, non il codice che scriviamo. Usare nel nostro codice tipi di Polly esposti dai package Microsoft, come <code class="language-plaintext highlighter-rouge">RetryStrategyOptions</code> o il namespace <code class="language-plaintext highlighter-rouge">Polly</code>, non la cambia. Aggiungere un <code class="language-plaintext highlighter-rouge">PackageReference</code> esplicito a <code class="language-plaintext highlighter-rouge">Polly.Core</code> o <code class="language-plaintext highlighter-rouge">Polly.Extensions</code>, invece, rende Polly una dipendenza diretta.</p>

<p>Chi utilizza Polly direttamente dovrà quindi valutare il proprio caso; chi lo utilizza transitivamente attraverso <code class="language-plaintext highlighter-rouge">Microsoft.Extensions.Resilience</code> si trova in una situazione differente secondo le regole OSMF attualmente pubblicate.</p>

<p>Chiusa la parentesi, torniamo alla resilience.</p>

<h2 id="resilience-by-design">Resilience by design</h2>

<p>Eravamo partiti da una semplice chiamata. A prima vista sembrava non esserci molto da progettare: mandiamo una richiesta, riceviamo una risposta. Ma appena smettiamo di assumere che tutto andrà sempre bene, dobbiamo decidere come comportarci se la chiamata fallisce, rallenta o mette sotto pressione il servizio esterno.</p>

<p>Una resilience pipeline ci permette di trasformare queste domande in comportamenti espliciti della nostra applicazione.</p>

<p>Il concetto chiave diventa quindi:</p>

<blockquote>
  <p>Un’applicazione resiliente non è un’applicazione che non fallisce.<br />
È un’applicazione progettata sapendo che qualcosa, prima o poi, fallirà.</p>
</blockquote>

<p>Nel prossimo articolo entreremo dentro una resilience pipeline.</p>

<p>Faremo fallire intenzionalmente la nostra chiamata a Catalog API e vedremo, passo dopo passo, come si comportano timeout, retry, circuit breaker, rate limiting e hedging, ma soprattutto quando utilizzarli e quando possono invece peggiorare la situazione.</p>]]></content><author><name>Alessandro Mengoli</name></author><category term=".NET" /><category term="Best Practices" /><category term="dotnet" /><category term="resilience" /><category term="polly" /><category term="httpclient" /><category term="best-practices" /><summary type="html"><![CDATA[Una dependency può diventare unavailable, slow oppure overloaded. Cos'è la resilience, perché non basta un try/catch e cosa ci offrono Microsoft.Extensions.Resilience e la pipeline standard per HttpClient.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.devpills.net/assets/images/posts/resilience-pipeline.svg" /><media:content medium="image" url="https://www.devpills.net/assets/images/posts/resilience-pipeline.svg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="es"><title type="html">Cómo crear aplicaciones .NET resilientes</title><link href="https://www.devpills.net/es/aplicaciones-dotnet-resilientes/" rel="alternate" type="text/html" title="Cómo crear aplicaciones .NET resilientes" /><published>2026-09-27T00:00:00+02:00</published><updated>2026-09-27T00:00:00+02:00</updated><id>https://www.devpills.net/es/resilient-dotnet-es</id><content type="html" xml:base="https://www.devpills.net/es/aplicaciones-dotnet-resilientes/"><![CDATA[<p>Al desarrollar una aplicación solemos dar por sentado, quizá con demasiado optimismo, que todo funcionará. Esto ocurre especialmente cuando nos comunicamos con otros servicios o sistemas de terceros.</p>

<p>Suponemos que el servicio externo siempre estará disponible, que la red funcionará, que las respuestas llegarán rápido y que, una vez establecida la comunicación, todo seguirá funcionando igual.</p>

<p>Veamos un ejemplo sencillo:</p>

<p><img src="/assets/images/posts/service-dependency.svg" alt="Nuestra API se comunica con Catalog API" /></p>

<p>Normalmente, Catalog API responde en 100 ms y todo funciona perfectamente.</p>

<p>Pero durante unos segundos Catalog API devuelve <code class="language-plaintext highlighter-rouge">503 Service Unavailable</code>. O tarda ocho segundos en responder, deja de responder por completo o se ve desbordada cuando nuestra aplicación recibe diez veces el tráfico previsto.</p>

<p>Una dependencia puede dejar de estar disponible, responder despacio o sobrecargarse. Debemos diseñar el sistema sabiendo que, tarde o temprano, esto ocurrirá.</p>

<p>Aquí entra en juego la <strong>resiliencia</strong>.</p>

<h2 id="de-gestionar-errores-a-diseñar-resiliencia">De gestionar errores a diseñar resiliencia</h2>

<p>Lo primero que se nos ocurre es gestionar el error:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">try</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">response</span> <span class="p">=</span> <span class="k">await</span> <span class="n">catalogClient</span><span class="p">.</span><span class="nf">GetAsync</span><span class="p">(</span><span class="s">"/products/42"</span><span class="p">);</span>
    <span class="n">response</span><span class="p">.</span><span class="nf">EnsureSuccessStatusCode</span><span class="p">();</span>
<span class="p">}</span>
<span class="k">catch</span> <span class="p">(</span><span class="n">HttpRequestException</span> <span class="n">ex</span><span class="p">)</span>
<span class="p">{</span>
    <span class="c1">// Gestionar el error</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Este código no tiene nada de malo, pero solo decide qué hacer <strong>después</strong> de que la operación haya fallado.</p>

<p>Crear una aplicación resiliente exige ir un paso más allá. Debemos preguntarnos:</p>

<ul>
  <li>¿Qué ocurre si la llamada falla?</li>
  <li>¿Qué ocurre si el servicio responde lentamente?</li>
  <li>¿Qué ocurre si sigue fallando?</li>
  <li>¿Qué ocurre si enviamos demasiadas peticiones?</li>
  <li>¿Qué ocurre si el fallo es temporal?</li>
</ul>

<p>La resiliencia consiste en diseñar explícitamente cómo se comportará la aplicación ante los fallos: recuperarse cuando sea posible y, sobre todo, impedir que un problema en una dependencia se propague al resto del sistema.</p>

<p>Aquí es donde ayuda <code class="language-plaintext highlighter-rouge">Microsoft.Extensions.Resilience</code>.</p>

<h2 id="microsoftextensionsresilience">Microsoft.Extensions.Resilience</h2>

<p>Si llevas tiempo creando aplicaciones .NET resilientes, probablemente conozcas Polly, una de las bibliotecas más utilizadas en el ecosistema .NET para estrategias como reintentos, circuit breakers y timeouts.</p>

<p>Con .NET 8, Microsoft introdujo dos paquetes construidos sobre Polly v8: <code class="language-plaintext highlighter-rouge">Microsoft.Extensions.Resilience</code> y <code class="language-plaintext highlighter-rouge">Microsoft.Extensions.Http.Resilience</code>, como explica en su <a href="https://devblogs.microsoft.com/dotnet/building-resilient-cloud-services-with-dotnet-8/">artículo sobre resiliencia en .NET 8</a>.</p>

<p>Hoy disponemos de:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">Microsoft.Extensions.Resilience</code>, que ofrece mecanismos generales para crear e integrar pipelines de resiliencia.</li>
  <li><code class="language-plaintext highlighter-rouge">Microsoft.Extensions.Http.Resilience</code>, que añade funciones específicas para <code class="language-plaintext highlighter-rouge">HttpClient</code>.</li>
</ul>

<p>El paquete anterior, <code class="language-plaintext highlighter-rouge">Microsoft.Extensions.Http.Polly</code>, está obsoleto. Microsoft recomienda los paquetes nuevos para aplicaciones nuevas; su <a href="https://learn.microsoft.com/en-us/dotnet/core/resilience/">documentación de resiliencia en .NET</a> recoge las indicaciones actualizadas.</p>

<p>Si ya conoces Polly, el modelo conceptual te resultará familiar, pues los paquetes de Microsoft se basan en él. Antes de ver el código, conviene entender qué problemas resuelven estas estrategias.</p>

<h2 id="un-resumen-de-las-estrategias">Un resumen de las estrategias</h2>

<p>Una <strong>pipeline de resiliencia</strong> es una secuencia de estrategias que rodean la operación que queremos ejecutar.</p>

<p>Para una llamada HTTP, podemos imaginarla así:</p>

<p><img src="/assets/images/posts/resilience-pipeline.svg" alt="Pipeline estándar de resiliencia HTTP" /></p>

<p>Esta es la pipeline estándar que proporciona <code class="language-plaintext highlighter-rouge">Microsoft.Extensions.Http.Resilience</code>. Por orden, contiene un limitador de concurrencia, un timeout global, una estrategia de reintentos, un circuit breaker y un timeout para cada intento.</p>

<p>Por ahora basta con entender la función de cada estrategia:</p>

<ul>
  <li>El <strong>rate limiter</strong> controla cuántas operaciones pueden ejecutarse a la vez contra la dependencia. Su valor predeterminado es de 1000 operaciones concurrentes, sin cola. Su objetivo es evitar una presión excesiva sobre el servicio.</li>
  <li>El <strong>total timeout</strong> limita la duración de toda la operación, incluidos los intentos posteriores.</li>
  <li>La estrategia de <strong>retry</strong> repite una operación cuando el fallo podría ser transitorio.</li>
  <li>El <strong>circuit breaker</strong> detiene temporalmente las llamadas cuando la tasa de fallos de una dependencia supera un umbral. Por defecto, ese umbral es el 10 % de las peticiones durante una ventana de 30 segundos, con un mínimo de 100 peticiones.</li>
  <li>El <strong>attempt timeout</strong> limita la duración de cada intento individual.</li>
</ul>

<p>La configuración puede ser sorprendentemente sencilla. Después de añadir el paquete:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dotnet add package Microsoft.Extensions.Http.Resilience
</code></pre></div></div>

<p>basta una línea para añadir el handler estándar:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">services</span>
    <span class="p">.</span><span class="nf">AddHttpClient</span><span class="p">(</span><span class="s">"catalog"</span><span class="p">,</span> <span class="n">client</span> <span class="p">=&gt;</span>
    <span class="p">{</span>
        <span class="n">client</span><span class="p">.</span><span class="n">BaseAddress</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Uri</span><span class="p">(</span><span class="s">"https://catalog.example.com"</span><span class="p">);</span>
    <span class="p">})</span>
    <span class="p">.</span><span class="nf">AddStandardResilienceHandler</span><span class="p">();</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">AddStandardResilienceHandler()</code> asocia la pipeline estándar a nuestro <code class="language-plaintext highlighter-rouge">HttpClient</code>. Sin embargo, añadir esa línea por sí sola no convierte mágicamente en resiliente a cualquier aplicación.</p>

<h2 id="reintentar-no-siempre-es-buena-idea">Reintentar no siempre es buena idea</h2>

<p>Supongamos que recibimos <code class="language-plaintext highlighter-rouge">503 Service Unavailable</code>. El problema podría ser temporal, así que reintentar puede tener sentido.</p>

<p>Si recibimos <code class="language-plaintext highlighter-rouge">400 Bad Request</code>, esperar un segundo y enviar la misma petición difícilmente cambiará el resultado. La pipeline estándar no la reintenta: las estrategias de retry y circuit breaker gestionan errores <code class="language-plaintext highlighter-rouge">5xx</code>, <code class="language-plaintext highlighter-rouge">408 Request Timeout</code>, <code class="language-plaintext highlighter-rouge">429 Too Many Requests</code>, <code class="language-plaintext highlighter-rouge">HttpRequestException</code> y timeouts.</p>

<p>El caso de un <code class="language-plaintext highlighter-rouge">POST /payments</code> no idempotente es más delicado. Si no sabemos si el primer intento se procesó, repetirlo automáticamente podría provocar dos pagos.</p>

<p>La pipeline estándar <strong>no</strong> nos protege aquí. Por defecto, reintenta peticiones hechas con <strong>cualquier método HTTP</strong>, incluido <code class="language-plaintext highlighter-rouge">POST</code>. Si nuestro cliente realiza operaciones no idempotentes, debemos configurarlo explícitamente:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">services</span>
    <span class="p">.</span><span class="nf">AddHttpClient</span><span class="p">(</span><span class="s">"payments"</span><span class="p">,</span> <span class="n">client</span> <span class="p">=&gt;</span>
    <span class="p">{</span>
        <span class="n">client</span><span class="p">.</span><span class="n">BaseAddress</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Uri</span><span class="p">(</span><span class="s">"https://payments.example.com"</span><span class="p">);</span>
    <span class="p">})</span>
    <span class="p">.</span><span class="nf">AddStandardResilienceHandler</span><span class="p">(</span><span class="n">options</span> <span class="p">=&gt;</span>
    <span class="p">{</span>
        <span class="c1">// Sin reintentos para POST, PUT, PATCH, DELETE ni CONNECT</span>
        <span class="n">options</span><span class="p">.</span><span class="n">Retry</span><span class="p">.</span><span class="nf">DisableForUnsafeHttpMethods</span><span class="p">();</span>
    <span class="p">});</span>
</code></pre></div></div>

<p>Cada estrategia de resiliencia tiene <strong>ventajas y costes</strong>. Incluso un reintento aparentemente inocuo genera tráfico adicional. Si cientos de instancias reintentan al mismo tiempo contra un servicio que ya tiene problemas, pueden empeorar la situación. La pipeline estándar reduce este riesgo con backoff exponencial y jitter, que distribuyen los intentos en el tiempo, y con un circuit breaker que deja de llamar a una dependencia claramente en dificultades. Reducir el riesgo no significa eliminarlo.</p>

<p>La resiliencia no consiste en reintentarlo todo. Consiste en entender qué fallo quieres gestionar y qué comportamiento buscas.</p>

<h2 id="y-polly">¿Y Polly?</h2>

<p>Como <code class="language-plaintext highlighter-rouge">Microsoft.Extensions.Resilience</code> se basa en Polly, hay otro aspecto que conviene tener en cuenta.</p>

<p>En julio de 2026, Polly <a href="https://thepollyproject.org/2026/07/14/polly-osmf-announcement.html">anunció que adoptaría la Open Source Maintenance Fee (OSMF)</a>. A partir del 16 de noviembre de 2026, la cuota anunciada es de 20 dólares al mes por organización para quienes cumplan los criterios del proyecto: al menos 20 000 dólares de ingresos procedentes de productos que utilicen Polly. El código sigue siendo de código abierto y la licencia de Polly no cambia.</p>

<p>Hay una distinción importante: según las <a href="https://opensourcemaintenancefee.org/consumers/faq/">preguntas frecuentes de OSMF para consumidores de software</a>, la cuota afecta a las dependencias directas, pero no a las transitivas. Si usamos <code class="language-plaintext highlighter-rouge">Microsoft.Extensions.Resilience</code> sin referenciar Polly directamente, Polly sigue siendo una dependencia transitiva del paquete de Microsoft.</p>

<p>La distinción se refiere a los paquetes referenciados, no al código que escribimos. Usar tipos de Polly expuestos por los paquetes de Microsoft, como <code class="language-plaintext highlighter-rouge">RetryStrategyOptions</code> o el espacio de nombres <code class="language-plaintext highlighter-rouge">Polly</code>, no la altera. En cambio, añadir un <code class="language-plaintext highlighter-rouge">PackageReference</code> explícito a <code class="language-plaintext highlighter-rouge">Polly.Core</code> o <code class="language-plaintext highlighter-rouge">Polly.Extensions</code> convierte Polly en una dependencia directa.</p>

<p>Los equipos que referencien Polly directamente deberán evaluar su caso. Quienes lo utilicen de forma transitiva a través de <code class="language-plaintext highlighter-rouge">Microsoft.Extensions.Resilience</code> se encuentran en otra situación según las reglas de OSMF publicadas hasta ahora.</p>

<p>Volvamos a la resiliencia.</p>

<h2 id="resiliencia-desde-el-diseño">Resiliencia desde el diseño</h2>

<p>Empezamos con una llamada HTTP sencilla. Al principio parecía que había poco que diseñar: enviamos una petición y recibimos una respuesta. Pero en cuanto dejamos de suponer que todo funcionará siempre, debemos decidir qué ocurre si la llamada falla, se ralentiza o ejerce demasiada presión sobre el servicio externo.</p>

<p>Una pipeline de resiliencia nos permite convertir esas preguntas en comportamientos explícitos de la aplicación.</p>

<p>La idea clave es:</p>

<blockquote>
  <p>Una aplicación resiliente no es una aplicación que nunca falla.<br />
Es una aplicación diseñada sabiendo que, tarde o temprano, algo fallará.</p>
</blockquote>

<p>En el siguiente artículo entraremos en una pipeline de resiliencia. Haremos fallar intencionadamente la llamada a Catalog API y veremos cómo se comportan los timeouts, los reintentos, los circuit breakers, el rate limiting y el hedging: cuándo utilizarlos y cuándo pueden empeorar la situación.</p>]]></content><author><name>Alessandro Mengoli</name></author><category term=".NET" /><category term="Buenas prácticas" /><category term="dotnet" /><category term="resiliencia" /><category term="polly" /><category term="httpclient" /><category term="buenas-practicas" /><summary type="html"><![CDATA[Una dependencia puede dejar de estar disponible, responder despacio o sobrecargarse. Descubre por qué try/catch no basta y cómo ayuda la pipeline estándar de HttpClient.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.devpills.net/assets/images/posts/resilience-pipeline.svg" /><media:content medium="image" url="https://www.devpills.net/assets/images/posts/resilience-pipeline.svg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="en"><title type="html">Building resilient .NET applications</title><link href="https://www.devpills.net/en/building-resilient-dotnet-applications/" rel="alternate" type="text/html" title="Building resilient .NET applications" /><published>2026-09-27T00:00:00+02:00</published><updated>2026-09-27T00:00:00+02:00</updated><id>https://www.devpills.net/en/resilient-dotnet-en</id><content type="html" xml:base="https://www.devpills.net/en/building-resilient-dotnet-applications/"><![CDATA[<p>When we develop an application, we often assume, perhaps a little too optimistically, that everything will work. This is especially true when we communicate with other services or third-party systems.</p>

<p>We assume the external service will always be available, the network will work, responses will arrive quickly, and once communication is established, it will keep working the same way.</p>

<p>Consider a simple example:</p>

<p><img src="/assets/images/posts/service-dependency.svg" alt="Our API communicates with Catalog API" /></p>

<p>Normally, Catalog API responds in 100 ms and everything works perfectly.</p>

<p>Then, for a few seconds, Catalog API returns <code class="language-plaintext highlighter-rouge">503 Service Unavailable</code>. Or it takes eight seconds to respond, stops responding altogether, or gets overwhelmed when our application receives ten times its expected traffic.</p>

<p>A dependency can become unavailable, slow, or overloaded. We need to design our system knowing that sooner or later this will happen.</p>

<p>This is where <strong>resilience</strong> comes in.</p>

<h2 id="from-fault-handling-to-resilience">From fault handling to resilience</h2>

<p>The first instinct is to handle the error:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">try</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">response</span> <span class="p">=</span> <span class="k">await</span> <span class="n">catalogClient</span><span class="p">.</span><span class="nf">GetAsync</span><span class="p">(</span><span class="s">"/products/42"</span><span class="p">);</span>
    <span class="n">response</span><span class="p">.</span><span class="nf">EnsureSuccessStatusCode</span><span class="p">();</span>
<span class="p">}</span>
<span class="k">catch</span> <span class="p">(</span><span class="n">HttpRequestException</span> <span class="n">ex</span><span class="p">)</span>
<span class="p">{</span>
    <span class="c1">// Handle error</span>
<span class="p">}</span>
</code></pre></div></div>

<p>There is nothing wrong with this code, but it only decides what to do <strong>after</strong> the operation has failed.</p>

<p>Making an application resilient means going a step further. We need to ask:</p>

<ul>
  <li>What happens if the call fails?</li>
  <li>What happens if the service is slow?</li>
  <li>What happens if it keeps failing?</li>
  <li>What happens if we send too many requests?</li>
  <li>What happens if the failure is only temporary?</li>
</ul>

<p>Resilience means explicitly designing how an application behaves when failures occur: recovering where possible and, above all, preventing a problem in one dependency from spreading through the rest of the system.</p>

<p>This is where <code class="language-plaintext highlighter-rouge">Microsoft.Extensions.Resilience</code> helps.</p>

<h2 id="microsoftextensionsresilience">Microsoft.Extensions.Resilience</h2>

<p>If you have been building resilient .NET applications for a while, you probably know Polly, one of the most widely used .NET libraries for strategies such as retries, circuit breakers, and timeouts.</p>

<p>With .NET 8, Microsoft introduced two packages built on Polly v8: <code class="language-plaintext highlighter-rouge">Microsoft.Extensions.Resilience</code> and <code class="language-plaintext highlighter-rouge">Microsoft.Extensions.Http.Resilience</code>, as described in its <a href="https://devblogs.microsoft.com/dotnet/building-resilient-cloud-services-with-dotnet-8/">article on .NET 8 resilience</a>.</p>

<p>Today we have:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">Microsoft.Extensions.Resilience</code>, which provides general mechanisms for building and integrating resilience pipelines.</li>
  <li><code class="language-plaintext highlighter-rouge">Microsoft.Extensions.Http.Resilience</code>, which adds features specifically for <code class="language-plaintext highlighter-rouge">HttpClient</code>.</li>
</ul>

<p>The older <code class="language-plaintext highlighter-rouge">Microsoft.Extensions.Http.Polly</code> package is deprecated. Microsoft recommends the newer packages for new applications; its <a href="https://learn.microsoft.com/en-us/dotnet/core/resilience/">.NET resilience documentation</a> has the current guidance.</p>

<p>The conceptual model will feel familiar if you have used Polly, since the Microsoft packages are built on it. Before looking at code, let’s see what problems these strategies solve.</p>

<h2 id="an-overview-of-the-strategies">An overview of the strategies</h2>

<p>A <strong>resilience pipeline</strong> is a sequence of strategies wrapped around the operation we want to execute.</p>

<p>For an HTTP call, we can picture it like this:</p>

<p><img src="/assets/images/posts/resilience-pipeline.svg" alt="Standard HTTP resilience pipeline" /></p>

<p>This is the standard pipeline provided by <code class="language-plaintext highlighter-rouge">Microsoft.Extensions.Http.Resilience</code>. In order, it contains a rate limiter, an overall timeout, a retry strategy, a circuit breaker, and a timeout for each attempt.</p>

<p>For now, a high-level understanding of each strategy is enough:</p>

<ul>
  <li>The <strong>rate limiter</strong> controls how many operations can run concurrently against the dependency. Its default is 1,000 concurrent operations with no queue. The aim is to avoid putting excessive pressure on the called service.</li>
  <li>The <strong>total timeout</strong> limits the whole operation, including any subsequent attempts.</li>
  <li>The <strong>retry</strong> strategy repeats an operation when a failure might be transient.</li>
  <li>The <strong>circuit breaker</strong> temporarily stops calls when the failure rate for a dependency exceeds a threshold. By default, that threshold is 10% of requests within a 30-second window, with at least 100 requests.</li>
  <li>The <strong>attempt timeout</strong> limits the duration of each individual attempt.</li>
</ul>

<p>Configuration can be surprisingly simple. After adding the package:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dotnet add package Microsoft.Extensions.Http.Resilience
</code></pre></div></div>

<p>one line adds the standard handler:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">services</span>
    <span class="p">.</span><span class="nf">AddHttpClient</span><span class="p">(</span><span class="s">"catalog"</span><span class="p">,</span> <span class="n">client</span> <span class="p">=&gt;</span>
    <span class="p">{</span>
        <span class="n">client</span><span class="p">.</span><span class="n">BaseAddress</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Uri</span><span class="p">(</span><span class="s">"https://catalog.example.com"</span><span class="p">);</span>
    <span class="p">})</span>
    <span class="p">.</span><span class="nf">AddStandardResilienceHandler</span><span class="p">();</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">AddStandardResilienceHandler()</code> attaches the standard pipeline to our <code class="language-plaintext highlighter-rouge">HttpClient</code>. Adding that line alone, however, does not magically make every application resilient.</p>

<h2 id="retrying-is-not-always-a-good-idea">Retrying is not always a good idea</h2>

<p>Suppose we receive <code class="language-plaintext highlighter-rouge">503 Service Unavailable</code>. The problem may be temporary, so retrying can make sense.</p>

<p>If we receive <code class="language-plaintext highlighter-rouge">400 Bad Request</code>, waiting a second and sending the same request again is unlikely to change the result. The standard pipeline does not retry it: retry and circuit-breaker strategies handle <code class="language-plaintext highlighter-rouge">5xx</code>, <code class="language-plaintext highlighter-rouge">408 Request Timeout</code>, <code class="language-plaintext highlighter-rouge">429 Too Many Requests</code>, <code class="language-plaintext highlighter-rouge">HttpRequestException</code>, and timeouts.</p>

<p>The case of a non-idempotent <code class="language-plaintext highlighter-rouge">POST /payments</code> is more interesting. If we do not know whether the first attempt was processed, automatically repeating it might result in two payments.</p>

<p>The standard pipeline does <strong>not</strong> protect us here. By default, it retries requests made with <strong>any HTTP method</strong>, including <code class="language-plaintext highlighter-rouge">POST</code>. If our client performs non-idempotent operations, we must configure this explicitly:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">services</span>
    <span class="p">.</span><span class="nf">AddHttpClient</span><span class="p">(</span><span class="s">"payments"</span><span class="p">,</span> <span class="n">client</span> <span class="p">=&gt;</span>
    <span class="p">{</span>
        <span class="n">client</span><span class="p">.</span><span class="n">BaseAddress</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Uri</span><span class="p">(</span><span class="s">"https://payments.example.com"</span><span class="p">);</span>
    <span class="p">})</span>
    <span class="p">.</span><span class="nf">AddStandardResilienceHandler</span><span class="p">(</span><span class="n">options</span> <span class="p">=&gt;</span>
    <span class="p">{</span>
        <span class="c1">// No retries for POST, PUT, PATCH, DELETE, or CONNECT</span>
        <span class="n">options</span><span class="p">.</span><span class="n">Retry</span><span class="p">.</span><span class="nf">DisableForUnsafeHttpMethods</span><span class="p">();</span>
    <span class="p">});</span>
</code></pre></div></div>

<p>Every resilience strategy introduces <strong>trade-offs</strong>. Even an apparently harmless retry adds traffic. If hundreds of instances retry at the same time against a struggling service, they can make its problems worse. The standard pipeline reduces this risk through exponential backoff with jitter, which spreads attempts over time, and a circuit breaker that stops calling a clearly struggling dependency. Reducing the risk does not eliminate it.</p>

<p>Resilience does not mean retrying everything. It means understanding the failure you are trying to handle and the behavior you want.</p>

<h2 id="what-about-polly">What about Polly?</h2>

<p>Because <code class="language-plaintext highlighter-rouge">Microsoft.Extensions.Resilience</code> is built on Polly, there is one more point to consider.</p>

<p>In July 2026, Polly <a href="https://thepollyproject.org/2026/07/14/polly-osmf-announcement.html">announced its adoption of the Open Source Maintenance Fee (OSMF)</a>. Starting November 16, 2026, the announced fee is US$20 per month per organization for those meeting the project’s criteria: at least US$20,000 in revenue from products that use Polly. The code remains open source and Polly’s license does not change.</p>

<p>There is an important distinction: according to the <a href="https://opensourcemaintenancefee.org/consumers/faq/">OSMF FAQ for software consumers</a>, the fee concerns direct dependencies, not transitive ones. If we use <code class="language-plaintext highlighter-rouge">Microsoft.Extensions.Resilience</code> without referencing Polly directly, Polly remains a transitive dependency of the Microsoft package.</p>

<p>The distinction concerns referenced packages, not the code we write. Using Polly types exposed by the Microsoft packages, such as <code class="language-plaintext highlighter-rouge">RetryStrategyOptions</code> or the <code class="language-plaintext highlighter-rouge">Polly</code> namespace, does not change it. Adding an explicit <code class="language-plaintext highlighter-rouge">PackageReference</code> to <code class="language-plaintext highlighter-rouge">Polly.Core</code> or <code class="language-plaintext highlighter-rouge">Polly.Extensions</code> does make Polly a direct dependency.</p>

<p>Teams that reference Polly directly need to assess their own case. Teams that use it transitively through <code class="language-plaintext highlighter-rouge">Microsoft.Extensions.Resilience</code> are in a different position under the OSMF rules currently published.</p>

<p>With that aside, let’s return to resilience.</p>

<h2 id="resilience-by-design">Resilience by design</h2>

<p>We started with a simple HTTP call. At first, there seemed to be little to design: send a request, receive a response. Once we stop assuming everything will always work, however, we have to decide what happens if the call fails, slows down, or puts pressure on the external service.</p>

<p>A resilience pipeline lets us turn these questions into explicit application behavior.</p>

<p>The key idea is:</p>

<blockquote>
  <p>A resilient application is not one that never fails.<br />
It is one designed with the knowledge that something will eventually fail.</p>
</blockquote>

<p>In the next article we will step inside a resilience pipeline. We will deliberately make our call to Catalog API fail and examine timeouts, retries, circuit breakers, rate limiting, and hedging: how they behave, when to use them, and when they can make things worse.</p>]]></content><author><name>Alessandro Mengoli</name></author><category term=".NET" /><category term="Best Practices" /><category term="dotnet" /><category term="resilience" /><category term="polly" /><category term="httpclient" /><category term="best-practices" /><summary type="html"><![CDATA[A dependency can become unavailable, slow, or overloaded. Learn why try/catch is not enough and how the standard HttpClient resilience pipeline helps.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.devpills.net/assets/images/posts/resilience-pipeline.svg" /><media:content medium="image" url="https://www.devpills.net/assets/images/posts/resilience-pipeline.svg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="es"><title type="html">MCP 2026-07-28: qué cambia y por qué</title><link href="https://www.devpills.net/es/mcp-2026-07-28-que-cambia/" rel="alternate" type="text/html" title="MCP 2026-07-28: qué cambia y por qué" /><published>2026-08-20T00:00:00+02:00</published><updated>2026-08-20T00:00:00+02:00</updated><id>https://www.devpills.net/es/mcp-revision-es</id><content type="html" xml:base="https://www.devpills.net/es/mcp-2026-07-28-que-cambia/"><![CDATA[<p>Desde su introducción a finales de 2024, Model Context Protocol ha tenido una adopción rápida y constante. Como parte de su evolución, y en respuesta a las peticiones recibidas, la versión <strong>2026-07-28</strong> introdujo cambios importantes.</p>

<p>En los artículos anteriores de la serie vimos <a href="/mcp-intro/">qué es MCP</a> y <a href="/mcp-server-dotnet/">cómo construir un servidor en .NET</a> (ambos en italiano). Aquí veremos qué ha cambiado y por qué.</p>

<h2 id="el-paso-de-un-modelo-con-estado-a-uno-sin-estado">El paso de un modelo con estado a uno sin estado</h2>

<p>El cambio más importante es el paso de un modelo con estado a uno sin estado. Todos los demás cambios, grandes y pequeños, se derivan de él.</p>

<p>Al principio, el protocolo se diseñó principalmente para conectar procesos locales mediante una tubería, donde cliente y servidor pueden escribir en cualquier momento. Esa simetría no existe en HTTP: el cliente habla y el servidor responde. Para permitir también la comunicación del servidor al cliente, se introdujeron mecanismos específicos: una sesión que permanecía abierta, identificada mediante <code class="language-plaintext highlighter-rouge">Mcp-Session-Id</code>, por la que el servidor podía enviar mensajes cuando los necesitara.</p>

<p>Esto tiene dos costes. El primero es la necesidad de mantener el estado. El segundo, más molesto, es que la solicitud del cliente queda vinculada a la instancia concreta del servidor que abrió la sesión. Esto dificulta el escalado: el balanceador de carga debe enviar siempre al mismo cliente a la misma instancia y, si esta falla, la sesión desaparece con ella.</p>

<p>La nueva revisión cambia este flujo para adaptarse mejor a HTTP y, sobre todo, mejorar la escalabilidad y la fiabilidad.</p>

<h2 id="mrtr-multi-round-trip-requests">MRTR: Multi Round-Trip Requests</h2>

<p>Si el servidor necesita información del cliente, simplemente responde: en su respuesta pide lo que necesita y termina el intercambio. Una vez obtenida la información, el cliente vuelve a llamar al servidor con la misma solicitud, ampliada con la respuesta.</p>

<p><img src="/assets/images/posts/mcp-mrtr-vs-sessione.svg" alt="A la izquierda, una llamada HTTP permanece abierta mientras el servidor consulta al usuario; a la derecha, dos llamadas independientes están vinculadas mediante requestState" /></p>

<p>Veamos un ejemplo concreto. La herramienta <code class="language-plaintext highlighter-rouge">close_ticket</code> cierra un ticket de soporte, pero primero pide confirmar el motivo. Es el caso más sencillo que requiere una interacción a mitad de la llamada, y precisamente donde divergen las dos revisiones.</p>

<p>En la <strong>ronda 1</strong>, el cliente llama a <code class="language-plaintext highlighter-rouge">tools/call</code>. El servidor todavía no conoce el motivo del cierre, así que no puede terminar el trabajo. Responde con <code class="language-plaintext highlighter-rouge">resultType: input_required</code>, la pregunta que debe trasladarse al usuario y un <code class="language-plaintext highlighter-rouge">requestState</code>. <strong>La llamada HTTP termina aquí</strong>: el servidor libera la conexión, el controlador finaliza y no queda nada en memoria. El <code class="language-plaintext highlighter-rouge">requestState</code> es el único vínculo entre las dos rondas. Lo genera el servidor y el cliente lo devuelve sin modificarlo.</p>

<p>En la <strong>ronda 2</strong>, el cliente repite la misma llamada <code class="language-plaintext highlighter-rouge">tools/call</code>, añadiendo el <code class="language-plaintext highlighter-rouge">requestState</code> recibido y la respuesta del usuario dentro de <code class="language-plaintext highlighter-rouge">inputResponses</code>. Esta vez el servidor tiene todo lo necesario y termina con <code class="language-plaintext highlighter-rouge">resultType: complete</code>.</p>

<p>El campo clave es <code class="language-plaintext highlighter-rouge">resultType</code>: <code class="language-plaintext highlighter-rouge">input_required</code> significa «me falta algo, vuelve a llamarme»; <code class="language-plaintext highlighter-rouge">complete</code> indica que el intercambio ha terminado.</p>

<p>La diferencia entre los dos modelos se aprecia enseguida en las trazas de la misma herramienta, ejecutada primero con una sesión y después con MRTR.</p>

<p><img src="/assets/images/posts/mcp-wf-stateful.jpg" alt="Traza del modelo antiguo con sesión: initialize, canal GET abierto, elicitation anidada y DELETE final" /></p>

<p>Con sesión: <strong>26 spans, profundidad 8</strong>. Se ven <code class="language-plaintext highlighter-rouge">initialize</code>, <code class="language-plaintext highlighter-rouge">elicitation/create</code> anidada dentro de <code class="language-plaintext highlighter-rouge">tools/call</code> y el <code class="language-plaintext highlighter-rouge">DELETE</code> final que cierra la sesión. Entre medias hay un <code class="language-plaintext highlighter-rouge">GET /</code> que dura por sí solo 0,15 segundos en una traza de 0,37 segundos: es el canal del servidor al cliente, abierto durante toda la sesión.</p>

<p><img src="/assets/images/posts/mcp-wf-mrtr.jpg" alt="Traza de MRTR: server/discover, tools/list y dos llamadas tools/call al mismo nivel, sin GET ni DELETE" /></p>

<p>Con MRTR: <strong>19 spans, profundidad 4</strong>. Las dos llamadas <code class="language-plaintext highlighter-rouge">tools/call</code> están al mismo nivel, no una dentro de la otra. No hay barra <code class="language-plaintext highlighter-rouge">GET</code> ni <code class="language-plaintext highlighter-rouge">DELETE</code>.</p>

<p>La diferencia de profundidad muestra el cambio de enfoque: antes una llamada contenía una conversación; ahora hay dos llamadas independientes.</p>

<h2 id="adiós-al-intercambio-inicial">Adiós al intercambio inicial</h2>

<p>Como parte del paso a un modelo sin estado, también se ha eliminado el mecanismo de inicialización: el intercambio <code class="language-plaintext highlighter-rouge">initialize</code>/<code class="language-plaintext highlighter-rouge">notifications/initialized</code> y <code class="language-plaintext highlighter-rouge">Mcp-Session-Id</code> dejan paso a solicitudes independientes.</p>

<p>El intercambio inicial cumplía dos funciones, y cada una ha seguido un camino distinto.</p>

<p><strong>La información del cliente</strong> — versión del protocolo, capacidades e identidad — servía para que el servidor supiera con quién estaba hablando. Ahora viaja en cada solicitud dentro de <code class="language-plaintext highlighter-rouge">_meta</code>, bajo tres claves:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>io.modelcontextprotocol/protocolVersion
io.modelcontextprotocol/clientCapabilities
io.modelcontextprotocol/clientInfo
</code></pre></div></div>

<p>Se trata, literalmente, del contenido de <code class="language-plaintext highlighter-rouge">initialize</code> repetido en cada solicitud. Y estos campos son obligatorios: si falta alguno, el servidor rechaza la solicitud.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{"error":{"code":-32602,"message":
  "Requests using protocol version '2026-07-28' must include
   '_meta/io.modelcontextprotocol/protocolVersion'."}}
</code></pre></div></div>

<p><strong>La información del servidor</strong> — <code class="language-plaintext highlighter-rouge">serverInfo</code> y <code class="language-plaintext highlighter-rouge">serverCapabilities</code> — permitía al cliente saber qué podía hacer el servidor. Ahora es una solicitud ordinaria, <code class="language-plaintext highlighter-rouge">server/discover</code>, y es opcional: un cliente que ya sabe qué herramienta necesita puede omitirla e ir directamente a <code class="language-plaintext highlighter-rouge">tools/call</code>.</p>

<p>Se pasa así de una negociación, en la que ambas partes llegaban a un acuerdo y lo recordaban, a una declaración repetida en cada solicitud.</p>

<div class="hint-box p-4 my-6 rounded-r border-l-4 border-blue-500">
  <div class="flex items-start">
    <p class="text-sm text-blue-700 dark:text-blue-300">
      👉 Un consejo práctico: con el SDK de .NET, una solicitud GET al endpoint de un servidor sin estado devuelve <code>405 Method Not Allowed</code>, porque en ese modo ni siquiera se registran las rutas GET y DELETE. La especificación no garantiza este comportamiento, pero es una forma rápida de averiguar desde fuera cómo funciona un servidor.
    </p>
  </div>
</div>

<h2 id="los-demás-cambios">Los demás cambios</h2>

<ul>
  <li><strong>Enrutamiento basado en cabeceras.</strong> Se introducen <code class="language-plaintext highlighter-rouge">Mcp-Method</code> y <code class="language-plaintext highlighter-rouge">Mcp-Name</code>. Como todas las solicitudes son POST al mismo endpoint, antes un proxy tenía que leer y analizar el cuerpo JSON para enrutarlas. Ahora le bastan dos cabeceras.</li>
  <li><strong>Listas que se pueden almacenar en caché.</strong> Las respuestas de <code class="language-plaintext highlighter-rouge">tools/list</code>, <code class="language-plaintext highlighter-rouge">prompts/list</code>, <code class="language-plaintext highlighter-rouge">resources/list</code> y <code class="language-plaintext highlighter-rouge">resources/read</code> incluyen ahora <code class="language-plaintext highlighter-rouge">ttlMs</code> y <code class="language-plaintext highlighter-rouge">cacheScope</code>: el cliente sabe durante cuánto tiempo puede guardarlas y con qué alcance. Sin depender de una sesión, la caché resulta posible.</li>
  <li><strong>Roots y Sampling en desuso.</strong> Eran solicitudes iniciadas por el servidor hacia el cliente. Seguirán funcionando durante al menos doce meses, pero las nuevas implementaciones deberían evitar adoptarlas.</li>
  <li><strong>Logging en desuso.</strong> Era una notificación, no una pregunta a la espera de respuesta, así que ni siquiera puede utilizar MRTR. La solución pasa a ser OpenTelemetry; si quieres profundizar, consulta <a href="/es/series/">la serie dedicada</a> en este blog.</li>
  <li><strong>Elicitation sigue vigente.</strong> No se ha dejado de usar: solo cambia el mecanismo de comunicación y se convierte en el principal caso de uso de MRTR.</li>
  <li><strong>Autorización.</strong> También hay novedades aquí, pero merecen un artículo propio.</li>
</ul>

<h2 id="ventajas-inconvenientes-y-aspectos-a-vigilar">Ventajas, inconvenientes y aspectos a vigilar</h2>

<p>Como ocurre con cualquier cambio, hay ventajas, inconvenientes y detalles que requieren atención.</p>

<p>El sistema escala mejor, pero las rondas adicionales aumentan la latencia: una interacción que antes cabía en una llamada ahora necesita dos. En una red local pueden ser milisegundos; en una conexión lenta, bastante más.</p>

<p>Sobre todo, debemos prestar atención a cómo escribimos el servidor, porque ahora hay que gestionar la idempotencia.</p>

<div class="warning-box p-4 my-6 rounded-r border-l-4 border-red-500 bg-red-50 dark:bg-gray-800 dark:border-red-400">
  <div class="flex items-start">
    <span class="text-red-600 dark:text-red-400 text-lg mr-3">⚠️</span>
    <div class="text-sm text-red-800 dark:text-gray-100">
      <span class="font-bold">Atención:</span> las rondas 1 y 2 son dos llamadas a la misma herramienta con los mismos argumentos. Todo lo que hace la herramienta <em>antes</em> de pedir la información que le falta se ejecuta dos veces. Los efectos secundarios deben trasladarse al punto en que ya dispone de todo lo necesario, o protegerse con una clave de deduplicación.
    </div>
  </div>
</div>

<h2 id="conclusión">Conclusión</h2>

<p>MCP ha pasado de «el servidor puede volver a llamar al cliente en mitad de una solicitud» a «el servidor responde y se olvida de ti». El fin de las sesiones, las nuevas cabeceras, las listas que se pueden almacenar en caché y las funciones en desuso se derivan de esta decisión.</p>

<p>El precio son algunas rondas adicionales y la responsabilidad de gestionar la idempotencia. A cambio, el servidor puede situarse detrás de un balanceador de carga corriente sin una configuración especial y reiniciarse sin efectos drásticos sobre las conversaciones en curso.</p>

<p>En el próximo artículo pasaremos a la práctica: <a href="/mcp-dotnet-2026-07-28/">cómo escribir en .NET un servidor para la nueva revisión y migrar los antiguos</a> (en italiano). Actualizar el paquete compila sin avisos, pero después falla en tiempo de ejecución.</p>

<h2 id="recursos-útiles">Recursos útiles</h2>

<ul>
  <li><a href="https://blog.modelcontextprotocol.io/posts/2026-07-28/">Anuncio oficial de la revisión 2026-07-28</a></li>
  <li><a href="https://modelcontextprotocol.io">Documentación oficial de MCP</a></li>
</ul>]]></content><author><name>Alessandro Mengoli</name></author><category term="AI" /><category term="Protocols" /><category term="mcp" /><category term="ai" /><category term="llm" /><category term="protocols" /><category term="http" /><summary type="html"><![CDATA[La revisión 2026-07-28 de Model Context Protocol pasa de un modelo con estado a uno sin estado. Veamos MRTR, el fin del intercambio initialize y lo que esto implica para escalar un servidor.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.devpills.net/assets/images/posts/mcp-mrtr-vs-sessione.svg" /><media:content medium="image" url="https://www.devpills.net/assets/images/posts/mcp-mrtr-vs-sessione.svg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="en"><title type="html">MCP 2026-07-28: what changed and why</title><link href="https://www.devpills.net/en/mcp-2026-07-28-what-changed/" rel="alternate" type="text/html" title="MCP 2026-07-28: what changed and why" /><published>2026-08-20T00:00:00+02:00</published><updated>2026-08-20T00:00:00+02:00</updated><id>https://www.devpills.net/en/mcp-revision-en</id><content type="html" xml:base="https://www.devpills.net/en/mcp-2026-07-28-what-changed/"><![CDATA[<p>Since its introduction in late 2024, the Model Context Protocol has seen rapid, steady adoption. As the protocol evolved in response to requests from its users, version <strong>2026-07-28</strong> introduced substantial changes.</p>

<p>Earlier articles in this series covered <a href="/mcp-intro/">what MCP is</a> and <a href="/mcp-server-dotnet/">how to build a server in .NET</a> (both in Italian). Here we look at what changed and why.</p>

<h2 id="from-stateful-to-stateless">From stateful to stateless</h2>

<p>The biggest change is the move from stateful to stateless operation. All the other changes, large and small, follow from it.</p>

<p>MCP was originally designed mainly to connect local processes through a pipe, where client and server can each write whenever they want. HTTP does not have that symmetry: the client speaks and the server replies. To enable server-to-client communication anyway, the protocol introduced a persistent session, identified by an <code class="language-plaintext highlighter-rouge">Mcp-Session-Id</code>, through which the server could send messages as needed.</p>

<p>This has two costs. First, you have to maintain state. Second, and more troublesome, a client request becomes tied to the particular server instance that opened the session. That makes scaling harder: the load balancer must keep routing a client to the same instance, and the session dies if that instance fails.</p>

<p>The new revision changes this flow to fit HTTP better and, above all, to improve scalability and reliability.</p>

<h2 id="mrtr-multi-round-trip-requests">MRTR: Multi Round-Trip Requests</h2>

<p>If the server needs information from the client, it simply responds: its response asks for what it needs and ends the exchange. Once the client has gathered the information, it calls the server again with the same request, supplemented by the answer.</p>

<p><img src="/assets/images/posts/mcp-mrtr-vs-sessione.svg" alt="On the left, one HTTP call stays open while the server asks the user a question; on the right, two independent calls are linked by requestState" /></p>

<p>Consider a concrete example. The <code class="language-plaintext highlighter-rouge">close_ticket</code> tool closes a support ticket, but first asks the user to confirm the reason. This is the smallest case that needs an interaction in the middle of a call, and exactly where the two revisions diverge.</p>

<p>In <strong>round 1</strong>, the client calls <code class="language-plaintext highlighter-rouge">tools/call</code>. The server does not have the closing reason, so it cannot finish the job. It responds with <code class="language-plaintext highlighter-rouge">resultType: input_required</code>, a question for the user, and a <code class="language-plaintext highlighter-rouge">requestState</code>. <strong>The HTTP call ends here</strong>: the server releases the connection, the handler finishes, and nothing remains in memory. The <code class="language-plaintext highlighter-rouge">requestState</code> is the only continuity between the two rounds. The server produces it, and the client sends it back unchanged.</p>

<p>In <strong>round 2</strong>, the client repeats the same <code class="language-plaintext highlighter-rouge">tools/call</code>, adding the received <code class="language-plaintext highlighter-rouge">requestState</code> and the user’s answer in <code class="language-plaintext highlighter-rouge">inputResponses</code>. Now the server has everything it needs and finishes with <code class="language-plaintext highlighter-rouge">resultType: complete</code>.</p>

<p>The field to watch is <code class="language-plaintext highlighter-rouge">resultType</code>: <code class="language-plaintext highlighter-rouge">input_required</code> means “I need something else; call me again,” while <code class="language-plaintext highlighter-rouge">complete</code> means the exchange is over.</p>

<p>You can see the difference at a glance in the traces of the very same tool, run first with a session and then with MRTR.</p>

<p><img src="/assets/images/posts/mcp-wf-stateful.jpg" alt="Trace of the old session model: initialize, an open GET channel, nested elicitation, and a final DELETE" /></p>

<p>With a session: <strong>26 spans, depth 8</strong>. You can see <code class="language-plaintext highlighter-rouge">initialize</code>, <code class="language-plaintext highlighter-rouge">elicitation/create</code> nested inside <code class="language-plaintext highlighter-rouge">tools/call</code>, and the final <code class="language-plaintext highlighter-rouge">DELETE</code> that tears down the session. In between, a <code class="language-plaintext highlighter-rouge">GET /</code> lasts 0.15 seconds on a 0.37-second trace: that is the server-to-client channel, open for the whole session.</p>

<p><img src="/assets/images/posts/mcp-wf-mrtr.jpg" alt="MRTR trace: server/discover, tools/list, and two sibling tools/call calls, with no GET or DELETE" /></p>

<p>With MRTR: <strong>19 spans, depth 4</strong>. The two <code class="language-plaintext highlighter-rouge">tools/call</code> calls are siblings, not one nested inside the other. There is no <code class="language-plaintext highlighter-rouge">GET</code> bar and no <code class="language-plaintext highlighter-rouge">DELETE</code>.</p>

<p>The change in depth makes the shift in approach visible: one call used to contain a conversation; now there are two independent calls.</p>

<h2 id="goodbye-handshake">Goodbye handshake</h2>

<p>As part of the move to stateless operation, the initialization mechanism is gone too: the <code class="language-plaintext highlighter-rouge">initialize</code>/<code class="language-plaintext highlighter-rouge">notifications/initialized</code> exchange and <code class="language-plaintext highlighter-rouge">Mcp-Session-Id</code> give way to standalone requests.</p>

<p>The handshake did two jobs, and those jobs have taken different paths.</p>

<p><strong>Client information</strong> — protocol version, capabilities, and identity — told the server whom it was talking to. It now travels with every request inside <code class="language-plaintext highlighter-rouge">_meta</code>, under three keys:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>io.modelcontextprotocol/protocolVersion
io.modelcontextprotocol/clientCapabilities
io.modelcontextprotocol/clientInfo
</code></pre></div></div>

<p>This is, quite literally, the content of <code class="language-plaintext highlighter-rouge">initialize</code> repeated in every request. These fields are mandatory: if one is missing, the server rejects the request.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{"error":{"code":-32602,"message":
  "Requests using protocol version '2026-07-28' must include
   '_meta/io.modelcontextprotocol/protocolVersion'."}}
</code></pre></div></div>

<p><strong>Server information</strong> — <code class="language-plaintext highlighter-rouge">serverInfo</code> and <code class="language-plaintext highlighter-rouge">serverCapabilities</code> — let the client learn what the server could do. This is now an ordinary, optional <code class="language-plaintext highlighter-rouge">server/discover</code> request: a client that already knows which tool to call can skip it and go straight to <code class="language-plaintext highlighter-rouge">tools/call</code>.</p>

<p>In other words, a negotiation in which both sides agreed and remembered the result has become a declaration repeated in every request.</p>

<div class="hint-box p-4 my-6 rounded-r border-l-4 border-blue-500">
  <div class="flex items-start">
    <p class="text-sm text-blue-700 dark:text-blue-300">
      👉 A practical tip: with the .NET SDK, a GET request to a stateless server endpoint returns <code>405 Method Not Allowed</code>, because GET and DELETE routes are not even registered in that mode. The specification does not guarantee this behavior, but it is a quick external clue to how a server is running.
    </p>
  </div>
</div>

<h2 id="the-other-changes">The other changes</h2>

<ul>
  <li><strong>Header-based routing.</strong> <code class="language-plaintext highlighter-rouge">Mcp-Method</code> and <code class="language-plaintext highlighter-rouge">Mcp-Name</code> have been introduced. Since requests all use POST to the same endpoint, a proxy previously had to read and parse the JSON body to route them. Now two headers are enough.</li>
  <li><strong>Cacheable lists.</strong> Responses from <code class="language-plaintext highlighter-rouge">tools/list</code>, <code class="language-plaintext highlighter-rouge">prompts/list</code>, <code class="language-plaintext highlighter-rouge">resources/list</code>, and <code class="language-plaintext highlighter-rouge">resources/read</code> now carry <code class="language-plaintext highlighter-rouge">ttlMs</code> and <code class="language-plaintext highlighter-rouge">cacheScope</code>, telling the client how long and within what scope to cache them. Without a session to depend on, caching becomes possible.</li>
  <li><strong>Roots and Sampling deprecated.</strong> These were requests initiated by the server toward the client. They will keep working for at least twelve months, but new implementations should avoid adopting them.</li>
  <li><strong>Logging deprecated.</strong> Logging was a notification, not a question awaiting an answer, so it cannot even use MRTR. OpenTelemetry is the solution here; if you want to explore it, see <a href="/en/series/">the dedicated series</a> on this blog.</li>
  <li><strong>Elicitation remains.</strong> It has not been deprecated: only its transport changes, and it becomes the main use case for MRTR.</li>
  <li><strong>Authorization.</strong> There are changes here too, but they deserve an article of their own.</li>
</ul>

<h2 id="pros-cons-and-what-to-watch-for">Pros, cons, and what to watch for</h2>

<p>As with any change, there are trade-offs and details that need attention.</p>

<p>The system scales more easily, but extra round trips add latency: an interaction that used to fit in one call now needs two. On a local network that may mean milliseconds; on a slow connection, much more.</p>

<p>Most of all, we need to pay attention to how we write the server, because idempotency now needs to be handled.</p>

<div class="warning-box p-4 my-6 rounded-r border-l-4 border-red-500 bg-red-50 dark:bg-gray-800 dark:border-red-400">
  <div class="flex items-start">
    <span class="text-red-600 dark:text-red-400 text-lg mr-3">⚠️</span>
    <div class="text-sm text-red-800 dark:text-gray-100">
      <span class="font-bold">Watch out:</span> rounds 1 and 2 are two calls to the same tool with the same arguments. Anything the tool does <em>before</em> asking for the missing information runs twice. Move side effects after the point where the tool has everything it needs, or protect them with a deduplication key.
    </div>
  </div>
</div>

<h2 id="conclusion">Conclusion</h2>

<p>MCP has moved from “the server can call back to the client in the middle of a request” to “the server replies and forgets about you.” The end of sessions, new headers, cacheable lists, and deprecated features all follow from that decision.</p>

<p>The price is a few more round trips and the responsibility of handling idempotency. In return, a server can sit behind an ordinary load balancer without special configuration and restart without drastic effects on ongoing conversations.</p>

<p>The next article gets practical: <a href="/mcp-dotnet-2026-07-28/">how to write a .NET server for the new revision and migrate older servers</a> (in Italian). Simply upgrading the package compiles without a warning, then breaks at runtime.</p>

<h2 id="useful-resources">Useful resources</h2>

<ul>
  <li><a href="https://blog.modelcontextprotocol.io/posts/2026-07-28/">Official announcement of the 2026-07-28 revision</a></li>
  <li><a href="https://modelcontextprotocol.io">Official MCP documentation</a></li>
</ul>]]></content><author><name>Alessandro Mengoli</name></author><category term="AI" /><category term="Protocols" /><category term="mcp" /><category term="ai" /><category term="llm" /><category term="protocols" /><category term="http" /><summary type="html"><![CDATA[The 2026-07-28 revision of the Model Context Protocol moves from stateful to stateless operation. A look at MRTR, the end of the initialize handshake, and what this means for scaling a server.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.devpills.net/assets/images/posts/mcp-mrtr-vs-sessione.svg" /><media:content medium="image" url="https://www.devpills.net/assets/images/posts/mcp-mrtr-vs-sessione.svg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="it"><title type="html">MCP 2026-07-28 in .NET: scrivere un server nuovo e migrare quelli vecchi</title><link href="https://www.devpills.net/mcp-dotnet-2026-07-28/" rel="alternate" type="text/html" title="MCP 2026-07-28 in .NET: scrivere un server nuovo e migrare quelli vecchi" /><published>2026-08-20T00:00:00+02:00</published><updated>2026-08-20T00:00:00+02:00</updated><id>https://www.devpills.net/mcp-dotnet-2026-07-28</id><content type="html" xml:base="https://www.devpills.net/mcp-dotnet-2026-07-28/"><![CDATA[<p>Nell’<a href="/mcp-2026-07-28/">articolo precedente</a> abbiamo visto cosa cambia con la revisione 2026-07-28 e perché. Qui passiamo alla pratica in due tempi: prima scriviamo un server nuovo con la forma giusta, poi prendiamo un server 1.x che esiste già e lo portiamo di là.</p>

<p>Il codice completo è su <a href="https://github.com/amengoli9/tutorial-devpills/tree/main/AI_projects/MCP">GitHub</a>.</p>

<h2 id="il-progetto">Il progetto</h2>

<p>Serve il .NET 10 SDK. Partiamo da un progetto web vuoto e aggiungiamo il pacchetto:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dotnet new web <span class="nt">-n</span> MCP_Server_MRTR
<span class="nb">cd </span>MCP_Server_MRTR
dotnet add package ModelContextProtocol.AspNetCore
</code></pre></div></div>

<p>Al momento in cui scrivo la versione stabile è la <strong>2.2.0</strong>. La 2.0.0 è uscita il 28 luglio 2026, lo stesso giorno della revisione del protocollo.</p>

<h2 id="il-server">Il server</h2>

<p>Questo è tutto il <code class="language-plaintext highlighter-rouge">Program.cs</code>:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">TicketServer.Tools</span><span class="p">;</span>

<span class="kt">var</span> <span class="n">builder</span> <span class="p">=</span> <span class="n">WebApplication</span><span class="p">.</span><span class="nf">CreateBuilder</span><span class="p">(</span><span class="n">args</span><span class="p">);</span>

<span class="n">builder</span><span class="p">.</span><span class="n">Services</span>
    <span class="p">.</span><span class="nf">AddMcpServer</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">WithHttpTransport</span><span class="p">()</span>
    <span class="p">.</span><span class="n">WithTools</span><span class="p">&lt;</span><span class="n">TicketTools</span><span class="p">&gt;();</span>

<span class="kt">var</span> <span class="n">app</span> <span class="p">=</span> <span class="n">builder</span><span class="p">.</span><span class="nf">Build</span><span class="p">();</span>

<span class="n">app</span><span class="p">.</span><span class="nf">MapMcp</span><span class="p">();</span>

<span class="n">app</span><span class="p">.</span><span class="nf">Run</span><span class="p">();</span>
</code></pre></div></div>

<p>Se hai letto la <a href="/mcp-server-dotnet/">guida al primo server</a> ti sembrerà identico a prima, e infatti lo è. <strong>La cosa interessante è quello che non c’è</strong>: nessuna configurazione per lo stateless, perché dalla 2.x è il default. <code class="language-plaintext highlighter-rouge">HttpServerTransportOptions.Stateless</code> vale <code class="language-plaintext highlighter-rouge">true</code> se non lo tocchi.</p>

<p>Vale la pena fermarsi un secondo. Il server che scala, quello che puoi mettere dietro un load balancer qualsiasi e riavviare senza pensarci, è quello che ottieni <strong>non scrivendo niente</strong>. Il caso che richiede configurazione esplicita ora è l’altro.</p>

<h2 id="il-tool-che-deve-chiedere-qualcosa">Il tool che deve chiedere qualcosa</h2>

<p>L’esempio è lo stesso dell’articolo precedente: <code class="language-plaintext highlighter-rouge">close_ticket</code> chiude un ticket di assistenza, ma prima vuole farsi confermare il motivo.</p>

<p>Il tool ha tre strade, e conviene leggerle nell’ordine in cui il codice le incontra. Se il chiamante ha già passato il motivo non c’è niente da chiedere: un round e via. Se il motivo manca, il tool solleva una <code class="language-plaintext highlighter-rouge">InputRequiredException</code> — che non è un errore: l’SDK la trasforma in un risultato con <code class="language-plaintext highlighter-rouge">resultType: "input_required"</code>, e la chiamata HTTP finisce lì. Se il client torna con la risposta, la trova in <code class="language-plaintext highlighter-rouge">context.Params.InputResponses</code>.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="n">McpServerToolType</span><span class="p">]</span>
<span class="k">public</span> <span class="k">class</span> <span class="nc">TicketTools</span>
<span class="p">{</span>
    <span class="k">private</span> <span class="k">const</span> <span class="kt">string</span> <span class="n">DefaultCloseReason</span> <span class="p">=</span> <span class="s">"completed"</span><span class="p">;</span>

    <span class="p">[</span><span class="nf">McpServerTool</span><span class="p">(</span><span class="n">Name</span> <span class="p">=</span> <span class="s">"close_ticket"</span><span class="p">)]</span>
    <span class="p">[</span><span class="nf">Description</span><span class="p">(</span><span class="s">"Chiude un ticket di assistenza registrando il motivo."</span><span class="p">)]</span>
    <span class="k">public</span> <span class="k">static</span> <span class="kt">string</span> <span class="nf">CloseTicket</span><span class="p">(</span>
        <span class="n">McpServer</span> <span class="n">server</span><span class="p">,</span>
        <span class="n">RequestContext</span><span class="p">&lt;</span><span class="n">CallToolRequestParams</span><span class="p">&gt;</span> <span class="n">context</span><span class="p">,</span>
        <span class="p">[</span><span class="nf">Description</span><span class="p">(</span><span class="s">"L'ID del ticket da chiudere"</span><span class="p">)]</span> <span class="kt">long</span> <span class="n">ticketId</span><span class="p">,</span>
        <span class="p">[</span><span class="nf">Description</span><span class="p">(</span><span class="s">"Il motivo della chiusura"</span><span class="p">)]</span> <span class="kt">string</span><span class="p">?</span> <span class="n">closeReason</span> <span class="p">=</span> <span class="k">null</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="c1">// Round 2: il client è tornato con la risposta</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">context</span><span class="p">.</span><span class="n">Params</span><span class="p">?.</span><span class="n">InputResponses</span><span class="p">?.</span><span class="nf">TryGetValue</span><span class="p">(</span><span class="s">"closeReason"</span><span class="p">,</span> <span class="k">out</span> <span class="kt">var</span> <span class="n">response</span><span class="p">)</span> <span class="k">is</span> <span class="k">true</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="kt">var</span> <span class="n">elicited</span> <span class="p">=</span> <span class="n">response</span><span class="p">.</span><span class="nf">Deserialize</span><span class="p">(</span><span class="n">InputResponse</span><span class="p">.</span><span class="n">ElicitResultJsonTypeInfo</span><span class="p">);</span>

            <span class="k">if</span> <span class="p">(</span><span class="n">elicited</span><span class="p">?.</span><span class="n">IsAccepted</span> <span class="k">is</span> <span class="k">not</span> <span class="k">true</span><span class="p">)</span>
            <span class="p">{</span>
                <span class="k">return</span> <span class="s">"Chiusura annullata"</span><span class="p">;</span>
            <span class="p">}</span>

            <span class="kt">var</span> <span class="n">confirmed</span> <span class="p">=</span> <span class="n">elicited</span><span class="p">.</span><span class="n">Content</span><span class="p">?.</span><span class="nf">TryGetValue</span><span class="p">(</span><span class="s">"closeReason"</span><span class="p">,</span> <span class="k">out</span> <span class="kt">var</span> <span class="k">value</span><span class="p">)</span> <span class="k">is</span> <span class="k">true</span>
                <span class="p">?</span> <span class="k">value</span><span class="p">.</span><span class="nf">GetString</span><span class="p">()</span>
                <span class="p">:</span> <span class="k">null</span><span class="p">;</span>

            <span class="k">return</span> <span class="nf">Close</span><span class="p">(</span><span class="n">ticketId</span><span class="p">,</span> <span class="n">confirmed</span><span class="p">);</span>
        <span class="p">}</span>

        <span class="c1">// Il chiamante aveva già tutto: un solo round</span>
        <span class="k">if</span> <span class="p">(!</span><span class="kt">string</span><span class="p">.</span><span class="nf">IsNullOrWhiteSpace</span><span class="p">(</span><span class="n">closeReason</span><span class="p">))</span>
        <span class="p">{</span>
            <span class="k">return</span> <span class="nf">Close</span><span class="p">(</span><span class="n">ticketId</span><span class="p">,</span> <span class="n">closeReason</span><span class="p">);</span>
        <span class="p">}</span>

        <span class="c1">// Round 1: chiedi, poi togliti di mezzo</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">server</span><span class="p">.</span><span class="n">IsMrtrSupported</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="k">throw</span> <span class="k">new</span> <span class="nf">InputRequiredException</span><span class="p">(</span>
                <span class="n">inputRequests</span><span class="p">:</span> <span class="k">new</span> <span class="n">Dictionary</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">,</span> <span class="n">InputRequest</span><span class="p">&gt;</span>
                <span class="p">{</span>
                    <span class="p">[</span><span class="s">"closeReason"</span><span class="p">]</span> <span class="p">=</span> <span class="n">InputRequest</span><span class="p">.</span><span class="nf">ForElicitation</span><span class="p">(</span><span class="k">new</span> <span class="n">ElicitRequestParams</span>
                    <span class="p">{</span>
                        <span class="n">Message</span> <span class="p">=</span> <span class="s">$"Chiudere il ticket </span><span class="p">{</span><span class="n">ticketId</span><span class="p">}</span><span class="s">?"</span><span class="p">,</span>
                        <span class="n">RequestedSchema</span> <span class="p">=</span> <span class="k">new</span><span class="p">()</span>
                        <span class="p">{</span>
                            <span class="n">Properties</span> <span class="p">=</span>
                            <span class="p">{</span>
                                <span class="p">[</span><span class="s">"closeReason"</span><span class="p">]</span> <span class="p">=</span> <span class="k">new</span> <span class="n">ElicitRequestParams</span><span class="p">.</span><span class="n">StringSchema</span>
                                <span class="p">{</span>
                                    <span class="n">Title</span> <span class="p">=</span> <span class="s">"Motivo di chiusura"</span><span class="p">,</span>
                                    <span class="n">Default</span> <span class="p">=</span> <span class="n">DefaultCloseReason</span><span class="p">,</span>
                                <span class="p">},</span>
                            <span class="p">},</span>
                        <span class="p">},</span>
                    <span class="p">}),</span>
                <span class="p">},</span>
                <span class="n">requestState</span><span class="p">:</span> <span class="n">ticketId</span><span class="p">.</span><span class="nf">ToString</span><span class="p">());</span>
        <span class="p">}</span>

        <span class="c1">// Un client vecchio senza sessione non può essere interrogato</span>
        <span class="k">return</span> <span class="s">"Per chiudere un ticket serve un motivo: richiama passando `closeReason`."</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="k">private</span> <span class="k">static</span> <span class="kt">string</span> <span class="nf">Close</span><span class="p">(</span><span class="kt">long</span> <span class="n">ticketId</span><span class="p">,</span> <span class="kt">string</span><span class="p">?</span> <span class="n">reason</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">reason</span> <span class="p">=</span> <span class="kt">string</span><span class="p">.</span><span class="nf">IsNullOrWhiteSpace</span><span class="p">(</span><span class="n">reason</span><span class="p">)</span> <span class="p">?</span> <span class="n">DefaultCloseReason</span> <span class="p">:</span> <span class="n">reason</span><span class="p">;</span>
        <span class="k">return</span> <span class="s">$"Ticket </span><span class="p">{</span><span class="n">ticketId</span><span class="p">}</span><span class="s"> chiuso: </span><span class="p">{</span><span class="n">reason</span><span class="p">}</span><span class="s">"</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Il <code class="language-plaintext highlighter-rouge">requestState</code> è la sola cosa che lega i due round. Lo produce il server, il client lo rimanda indietro senza guardarci dentro.</p>

<h2 id="provarlo">Provarlo</h2>

<p>Brutta notizia per chi si era abituato a <a href="/mcp-server-dotnet/">MCP Inspector</a>: <strong>non sa ancora eseguire MRTR</strong>. Il suo client per la revisione 2026 è in preview e sui risultati <code class="language-plaintext highlighter-rouge">input_required</code> si ferma con un errore invece di gestirli.</p>

<p>Serve quindi un client vero, che sono una quarantina di righe. La parte che conta è l’handler:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">options</span> <span class="p">=</span> <span class="k">new</span> <span class="n">McpClientOptions</span>
<span class="p">{</span>
    <span class="n">Handlers</span> <span class="p">=</span> <span class="k">new</span> <span class="n">McpClientHandlers</span>
    <span class="p">{</span>
        <span class="n">ElicitationHandler</span> <span class="p">=</span> <span class="p">(</span><span class="n">requestParams</span><span class="p">,</span> <span class="n">ct</span><span class="p">)</span> <span class="p">=&gt;</span>
        <span class="p">{</span>
            <span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="s">$"  [richiesta] </span><span class="p">{</span><span class="n">requestParams</span><span class="p">?.</span><span class="n">Message</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>

            <span class="k">return</span> <span class="n">ValueTask</span><span class="p">.</span><span class="nf">FromResult</span><span class="p">(</span><span class="k">new</span> <span class="n">ElicitResult</span>
            <span class="p">{</span>
                <span class="n">Action</span> <span class="p">=</span> <span class="s">"accept"</span><span class="p">,</span>
                <span class="n">Content</span> <span class="p">=</span> <span class="k">new</span> <span class="n">Dictionary</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">,</span> <span class="n">JsonElement</span><span class="p">&gt;</span>
                <span class="p">{</span>
                    <span class="p">[</span><span class="s">"closeReason"</span><span class="p">]</span> <span class="p">=</span> <span class="n">JsonSerializer</span><span class="p">.</span><span class="nf">SerializeToElement</span><span class="p">(</span><span class="s">"duplicato"</span><span class="p">),</span>
                <span class="p">},</span>
            <span class="p">});</span>
        <span class="p">},</span>
    <span class="p">},</span>
<span class="p">};</span>

<span class="k">await</span> <span class="k">using</span> <span class="nn">var</span> <span class="n">client</span> <span class="p">=</span> <span class="k">await</span> <span class="n">McpClient</span><span class="p">.</span><span class="nf">CreateAsync</span><span class="p">(</span><span class="n">transport</span><span class="p">,</span> <span class="n">options</span><span class="p">);</span>

<span class="kt">var</span> <span class="n">result</span> <span class="p">=</span> <span class="k">await</span> <span class="n">client</span><span class="p">.</span><span class="nf">CallToolAsync</span><span class="p">(</span>
    <span class="s">"close_ticket"</span><span class="p">,</span>
    <span class="k">new</span> <span class="n">Dictionary</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">,</span> <span class="kt">object</span><span class="p">?&gt;</span> <span class="p">{</span> <span class="p">[</span><span class="s">"ticketId"</span><span class="p">]</span> <span class="p">=</span> <span class="m">1234L</span> <span class="p">});</span>
</code></pre></div></div>

<p>Nota cosa <strong>non</strong> c’è: il round 2. Chiamiamo il tool una volta sola. L’SDK client riceve <code class="language-plaintext highlighter-rouge">input_required</code>, usa l’handler per procurarsi la risposta e rifà da solo la stessa <code class="language-plaintext highlighter-rouge">tools/call</code> allegando <code class="language-plaintext highlighter-rouge">requestState</code> e <code class="language-plaintext highlighter-rouge">inputResponses</code>.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>connessione a http://localhost:5250/
  [tools] close_ticket
  [richiesta] Chiudere il ticket 1234?
  [risultato] Ticket 1234 chiuso: duplicato
</code></pre></div></div>

<p>La prima risposta, quella che chiude il round 1, è questa:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"result"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"inputRequests"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"closeReason"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"method"</span><span class="p">:</span><span class="w"> </span><span class="s2">"elicitation/create"</span><span class="p">,</span><span class="w"> </span><span class="err">...</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">},</span><span class="w">
    </span><span class="nl">"requestState"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1234"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"resultType"</span><span class="p">:</span><span class="w"> </span><span class="s2">"input_required"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"_meta"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"io.modelcontextprotocol/serverInfo"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"MCP_Server_MRTR"</span><span class="p">,</span><span class="w"> </span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1.0.0.0"</span><span class="w">
      </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="nl">"jsonrpc"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2.0"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>C’è una simmetria che vale la pena notare: l’identità del client sale nel <code class="language-plaintext highlighter-rouge">_meta</code> di ogni <strong>richiesta</strong>, quella del server torna nel <code class="language-plaintext highlighter-rouge">_meta</code> di ogni <strong>risposta</strong>. Quello che prima veniva scambiato una volta con <code class="language-plaintext highlighter-rouge">initialize</code> e poi ricordato, ora viene ripetuto e dimenticato.</p>

<h2 id="migrare-un-server-che-esiste-già">Migrare un server che esiste già</h2>

<p>Fin qui il campo era libero. Vediamo ora cosa succede a un server 1.x, dove lo stesso tool era scritto nell’unico modo che la 1.x permetteva:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">static</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">&gt;</span> <span class="nf">CloseTicketAsync</span><span class="p">(</span>
    <span class="n">McpServer</span> <span class="n">server</span><span class="p">,</span> <span class="kt">long</span> <span class="n">ticketId</span><span class="p">,</span> <span class="n">CancellationToken</span> <span class="n">cancellationToken</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">result</span> <span class="p">=</span> <span class="k">await</span> <span class="n">server</span><span class="p">.</span><span class="nf">ElicitAsync</span><span class="p">(</span>
        <span class="k">new</span> <span class="n">ElicitRequestParams</span> <span class="p">{</span> <span class="n">Message</span> <span class="p">=</span> <span class="s">$"Chiudere il ticket </span><span class="p">{</span><span class="n">ticketId</span><span class="p">}</span><span class="s">?"</span><span class="p">,</span> <span class="p">...</span> <span class="p">},</span>
        <span class="n">cancellationToken</span><span class="p">);</span>

    <span class="k">if</span> <span class="p">(!</span><span class="n">result</span><span class="p">.</span><span class="n">IsAccepted</span><span class="p">)</span> <span class="k">return</span> <span class="s">"Chiusura annullata"</span><span class="p">;</span>
    <span class="c1">// ...</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Quell’<code class="language-plaintext highlighter-rouge">await</code> è il punto. Il server manda <code class="language-plaintext highlighter-rouge">elicitation/create</code> giù per lo stream GET che il client tiene aperto e aspetta: l’handler, la sessione e questo specifico processo restano vivi per tutto il tempo che la persona impiega a decidere.</p>

<h3 id="passo-1-alza-il-pacchetto">Passo 1: alza il pacchetto</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dotnet add package ModelContextProtocol.AspNetCore
</code></pre></div></div>

<p>Dalla <code class="language-plaintext highlighter-rouge">1.4.1</code> alla <code class="language-plaintext highlighter-rouge">2.2.0</code>. Ricompiliamo:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Compilazione completata.
    Errori: 0
</code></pre></div></div>

<p>Zero errori e <strong>zero warning</strong>. Nessun <code class="language-plaintext highlighter-rouge">MCP9005</code>, nessun <code class="language-plaintext highlighter-rouge">CS</code>. Niente ti dice che qualcosa è cambiato.</p>

<h3 id="passo-2-eseguilo">Passo 2: eseguilo</h3>

<p>Il server parte, il client si connette, <code class="language-plaintext highlighter-rouge">tools/list</code> risponde. E poi:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  [tools] close_ticket
  [risultato] An error occurred invoking 'close_ticket'.
</code></pre></div></div>

<p>Nei log del server:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>fail: ModelContextProtocol.Server.McpServer
      "close_ticket" threw an unhandled exception.
      System.InvalidOperationException: Elicitation is not supported in stateless mode.
         at McpServer.ThrowIfElicitationUnsupported(ElicitRequestParams request)
         at McpServer.ElicitAsync(ElicitRequestParams requestParams, CancellationToken ct)
</code></pre></div></div>

<div class="warning-box p-4 my-6 rounded-r border-l-4 border-red-500 bg-red-50 dark:bg-gray-800 dark:border-red-400">
  <div class="flex items-start">
    <span class="text-red-600 dark:text-red-400 text-lg mr-3">⚠️</span>
    <div class="text-sm text-red-800 dark:text-gray-100">
      <span class="font-bold">Attenzione:</span> l'handshake, la lista dei tool e i tool che non chiedono niente continuano a funzionare. Se il tuo smoke test di deploy si limita a elencare i tool, passa. Si rompe solo il tool che aveva bisogno di parlare con l'utente, e solo quando qualcuno lo chiama davvero.
    </div>
  </div>
</div>

<h3 id="perché-succede">Perché succede</h3>

<p>Perché è cambiato un default. Nella 1.x il trasporto HTTP era stateful se non dicevi niente; dalla 2.x è <strong>stateless</strong> se non dici niente. Il tuo codice non nominava <code class="language-plaintext highlighter-rouge">Stateless</code> — non serviva, era già quello che volevi — quindi il significato di quella riga mancante si è ribaltato sotto i piedi. E in modalità stateless il server non ha nessun canale su cui raggiungere il client.</p>

<h3 id="le-due-strade">Le due strade</h3>

<p>La scorciatoia è rimettere le sessioni, e il server riparte:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">.</span><span class="nf">WithHttpTransport</span><span class="p">(</span><span class="n">options</span> <span class="p">=&gt;</span> <span class="p">{</span> <span class="n">options</span><span class="p">.</span><span class="n">Stateless</span> <span class="p">=</span> <span class="k">false</span><span class="p">;</span> <span class="p">})</span>
</code></pre></div></div>

<p>Funziona davvero, l’ho verificato. Ma hai alzato il pacchetto e tenuto tutto quello che rendeva il modello vecchio poco scalabile. Come passo intermedio per sbloccare un deploy va benissimo; come destinazione, no.</p>

<p>La migrazione vera è riscrivere il tool nella forma vista sopra. Il confronto con la versione 1.x dice tre cose:</p>

<ul>
  <li><strong>Il metodo non è più <code class="language-plaintext highlighter-rouge">async</code>.</strong> Non aspetta più niente: chiede e termina. Sparisce anche il <code class="language-plaintext highlighter-rouge">CancellationToken</code>, che serviva a interrompere l’attesa.</li>
  <li><strong>Lo schema dell’elicitation non cambia.</strong> È lo stesso <code class="language-plaintext highlighter-rouge">ElicitRequestParams</code>, solo consegnato in un altro modo: prima come argomento di <code class="language-plaintext highlighter-rouge">ElicitAsync</code>, ora dentro <code class="language-plaintext highlighter-rouge">InputRequest.ForElicitation</code>. L’elicitation non è deprecata, è cambiato il veicolo.</li>
  <li><strong>È comparso <code class="language-plaintext highlighter-rouge">IsMrtrSupported</code>.</strong> Serve a non lasciare in un vicolo cieco i client che non ce la fanno.</li>
</ul>

<h3 id="i-warning-che-invece-esistono">I warning che invece esistono</h3>

<p>Il caso che abbiamo visto non ne produce nessuno, ma ci sono API che l’SDK segnala: <strong><code class="language-plaintext highlighter-rouge">MCP9004</code></strong> per <code class="language-plaintext highlighter-rouge">EnableLegacySse</code>, <strong><code class="language-plaintext highlighter-rouge">MCP9005</code></strong> per Roots, Sampling e Logging (funzionano ancora per almeno dodici mesi, ma le nuove implementazioni non dovrebbero adottarle), <strong><code class="language-plaintext highlighter-rouge">MCP9006</code></strong> per le opzioni che hanno senso solo con le sessioni come <code class="language-plaintext highlighter-rouge">EventStreamStore</code> e <code class="language-plaintext highlighter-rouge">IdleTimeout</code>, <strong><code class="language-plaintext highlighter-rouge">MCP9007</code></strong> per <code class="language-plaintext highlighter-rouge">AuthorizationRedirectDelegate</code>.</p>

<p>Se nel tuo <code class="language-plaintext highlighter-rouge">.csproj</code> c’è un <code class="language-plaintext highlighter-rouge">NoWarn</code> che elenca questi codici, quella lista è la tua to-do list di migrazione: si cancella una voce alla volta finché non resta niente.</p>

<h2 id="tre-cose-a-cui-fare-attenzione">Tre cose a cui fare attenzione</h2>

<p><strong>Gli effetti collaterali vanno dopo la domanda.</strong> Il round 1 e il round 2 sono due chiamate allo stesso tool con gli stessi argomenti: tutto quello che il tool fa prima di sollevare <code class="language-plaintext highlighter-rouge">InputRequiredException</code> viene eseguito due volte. Nel codice sopra il metodo <code class="language-plaintext highlighter-rouge">Close</code> sta dopo, ed è deliberato. Nella versione 1.x il codice prima dell’<code class="language-plaintext highlighter-rouge">await</code> girava una volta sola, quindi la migrazione può trasformare in doppioni cose che prima erano innocue.</p>

<p><strong>Il <code class="language-plaintext highlighter-rouge">requestState</code> è input non fidato.</strong> Torna dal client, e il client può averlo modificato. Nell’esempio è un ID di ticket e il danno è limitato, ma se ci metti qualcosa che decide un’autorizzazione, firmalo.</p>

<p><strong>Controlla <code class="language-plaintext highlighter-rouge">IsMrtrSupported</code>.</strong> Un client vecchio che parla con un server stateless non può essere interrogato: non c’è nessun canale su cui raggiungerlo. Il ramo di fallback non deve fare miracoli, basta che dica cosa manca.</p>

<div class="hint-box p-4 my-6 rounded-r border-l-4 border-blue-500">
  <div class="flex items-start">
    <p class="text-sm text-blue-700 dark:text-blue-300">
      👉 Per capire al volo come sta girando un server: <code>curl -o /dev/null -w "%{http_code}" http://localhost:5250/</code>. Un <code>405</code> vuol dire stateless, un <code>400</code> vuol dire che le sessioni sono ancora attive.
    </p>
  </div>
</div>

<h2 id="conclusioni">Conclusioni</h2>

<p>Il server nuovo è più semplice del vecchio, non più complicato: stateless è il default, il trasporto non si configura, e il tool non deve tenersi stretto niente fra un round e l’altro.</p>

<p>La migrazione, invece, è insidiosa proprio perché non fa rumore. Non è quella che non compila: è questa, che compila pulita e si rompe solo sul percorso che a nessuno viene in mente di testare. Se hai un server 1.x con un <code class="language-plaintext highlighter-rouge">ElicitAsync</code> dentro, cercalo prima di alzare il pacchetto — e prova davvero il tool che fa la domanda, non solo <code class="language-plaintext highlighter-rouge">tools/list</code>.</p>

<h2 id="risorse-utili">Risorse utili</h2>

<ul>
  <li><a href="https://github.com/amengoli9/tutorial-devpills/tree/main/AI_projects/MCP">Codice completo su GitHub</a></li>
  <li><a href="https://devblogs.microsoft.com/dotnet/announcing-v20-of-the-official-mcp-csharp-sdk/">Annuncio dell’SDK C# v2.0</a></li>
  <li><a href="https://blog.modelcontextprotocol.io/posts/2026-07-28/">La revisione 2026-07-28</a></li>
</ul>]]></content><author><name>Alessandro Mengoli</name></author><category term=".NET" /><category term="AI" /><category term="dotnet" /><category term="mcp" /><category term="ai" /><category term="llm" /><category term="protocols" /><summary type="html"><![CDATA[Un MCP server stateless in .NET con l'SDK 2.2.0, con un tool che a metà chiamata deve chiedere qualcosa all'utente. E poi la parte scomoda: migrare un server 1.x, che compila senza un warning e si rompe a runtime.]]></summary></entry><entry xml:lang="it"><title type="html">MCP versione 2026-07-28: cosa cambia e perché</title><link href="https://www.devpills.net/mcp-2026-07-28/" rel="alternate" type="text/html" title="MCP versione 2026-07-28: cosa cambia e perché" /><published>2026-08-20T00:00:00+02:00</published><updated>2026-08-20T00:00:00+02:00</updated><id>https://www.devpills.net/mcp-2026-07-28</id><content type="html" xml:base="https://www.devpills.net/mcp-2026-07-28/"><![CDATA[<p>Dalla sua introduzione a fine 2024 il Model Context Protocol ha avuto un’adozione molto rapida e continua. Come naturale evoluzione, e per rispondere alle tante richieste arrivate nel tempo, è stata rilasciata la versione <strong>2026-07-28</strong>, che introduce grandi cambiamenti.</p>

<p>Nei precedenti articoli della serie abbiamo visto <a href="/mcp-intro/">cos’è MCP</a> e <a href="/mcp-server-dotnet/">come costruire un server in .NET</a>. Qui vediamo cosa è cambiato e perché.</p>

<h2 id="il-cambio-da-stateful-a-stateless">Il cambio da stateful a stateless</h2>

<p>Il più grande cambiamento è il passaggio da stateful a stateless. Tutti gli altri cambiamenti, grandi e piccoli, discendono da lì.</p>

<p>Quando il protocollo è nato, il suo scopo era principalmente quello di collegare processi locali attraverso una pipe, dove client e server possono scriversi quando vogliono. Quella simmetria su HTTP non esiste: parla il client, risponde il server. Per rendere comunque possibile la comunicazione server → client, erano stati introdotti dei meccanismi appositi: una sessione tenuta aperta, identificata da un <code class="language-plaintext highlighter-rouge">Mcp-Session-Id</code>, su cui il server poteva scrivere quando ne aveva bisogno.</p>

<p>Questo ha due costi. Il primo è che bisogna mantenere lo stato. Il secondo, più fastidioso, è che lega la richiesta del client all’istanza esatta del server che ha aperto quella sessione, e questo non permette di scalare: serve che il load balancer mandi sempre lo stesso client alla stessa istanza, e se quella cade, la sessione muore con lei.</p>

<p>La nuova versione interrompe questo flusso per sposarsi al meglio con HTTP e soprattutto per garantire scalabilità e affidabilità.</p>

<h2 id="mrtr-multi-round-trip-requests">MRTR: Multi Round-Trip Requests</h2>

<p>Se il server ha bisogno di informazioni dal client, semplicemente risponde: nella risposta chiede quello che gli serve e chiude la comunicazione. Il client, una volta ottenute le informazioni necessarie, richiama il server con la stessa chiamata arricchita della risposta.</p>

<p><img src="/assets/images/posts/mcp-mrtr-vs-sessione.svg" alt="A sinistra una sola chiamata HTTP che resta aperta mentre il server interroga l'utente, a destra due chiamate indipendenti legate da requestState" /></p>

<p>Facciamo un esempio concreto. Il tool è <code class="language-plaintext highlighter-rouge">close_ticket</code>: chiude un ticket di assistenza, ma prima vuole farsi confermare il motivo. È il caso minimo che richiede un’interazione a metà chiamata, ed è esattamente il punto in cui le due revisioni divergono.</p>

<p>Nel <strong>round 1</strong> il client chiama <code class="language-plaintext highlighter-rouge">tools/call</code>. Il server non ha il motivo di chiusura, quindi non arriva in fondo al lavoro: risponde con <code class="language-plaintext highlighter-rouge">resultType: input_required</code>, la domanda da girare all’utente e un <code class="language-plaintext highlighter-rouge">requestState</code>. <strong>La chiamata HTTP finisce qui</strong>: il server lascia andare la connessione, l’handler termina, in memoria non resta niente. Il <code class="language-plaintext highlighter-rouge">requestState</code> è la sola continuità fra i due round, lo produce il server e il client lo rimanda indietro così com’è.</p>

<p>Nel <strong>round 2</strong> il client ripete la stessa <code class="language-plaintext highlighter-rouge">tools/call</code>, aggiungendo il <code class="language-plaintext highlighter-rouge">requestState</code> ricevuto e la risposta dell’utente dentro <code class="language-plaintext highlighter-rouge">inputResponses</code>. Questa volta il server ha tutto quello che gli serve e chiude con <code class="language-plaintext highlighter-rouge">resultType: complete</code>.</p>

<p>Il campo da guardare è proprio <code class="language-plaintext highlighter-rouge">resultType</code>: <code class="language-plaintext highlighter-rouge">input_required</code> significa “mi manca qualcosa, richiamami”, <code class="language-plaintext highlighter-rouge">complete</code> che il giro è finito.</p>

<p>La differenza fra i due mondi si legge a colpo d’occhio nelle tracce dello stesso identico tool, eseguito prima con una sessione e poi in MRTR.</p>

<p><img src="/assets/images/posts/mcp-wf-stateful.jpg" alt="Traccia del vecchio modello con sessione: initialize, canale GET aperto, elicitation annidata e DELETE finale" /></p>

<p>Con la sessione: <strong>26 span, profondità 8</strong>. Si vedono l’<code class="language-plaintext highlighter-rouge">initialize</code>, l’<code class="language-plaintext highlighter-rouge">elicitation/create</code> annidata dentro il <code class="language-plaintext highlighter-rouge">tools/call</code> e il <code class="language-plaintext highlighter-rouge">DELETE</code> finale che smonta la sessione. In mezzo c’è un <code class="language-plaintext highlighter-rouge">GET /</code> che da solo dura 0,15s su una traccia di 0,37s: è il canale server → client, aperto per tutta la sessione.</p>

<p><img src="/assets/images/posts/mcp-wf-mrtr.jpg" alt="Traccia MRTR: server/discover, tools/list e due tools/call fratelli, senza GET e senza DELETE" /></p>

<p>Con MRTR: <strong>19 span, profondità 4</strong>. I due <code class="language-plaintext highlighter-rouge">tools/call</code> sono fratelli, non uno dentro l’altro. Nessuna barra <code class="language-plaintext highlighter-rouge">GET</code>, nessun <code class="language-plaintext highlighter-rouge">DELETE</code>.</p>

<p>La differenza di profondità è il cambio di filosofia reso visibile: prima una chiamata conteneva una conversazione, ora sono due chiamate indipendenti.</p>

<h2 id="addio-handshake">Addio handshake</h2>

<p>Sempre nell’ottica dello stateless, è stato rimosso anche il meccanismo di inizializzazione: l’exchange <code class="language-plaintext highlighter-rouge">initialize</code>/<code class="language-plaintext highlighter-rouge">notifications/initialized</code> e l’<code class="language-plaintext highlighter-rouge">Mcp-Session-Id</code> lasciano il posto a richieste standalone.</p>

<p>L’handshake faceva due lavori, e i due lavori hanno preso strade opposte.</p>

<p><strong>La parte client</strong> — versione del protocollo, capability e identità — serviva al server per sapere con chi stava parlando. Ora viaggia in ogni richiesta dentro <code class="language-plaintext highlighter-rouge">_meta</code>, sotto tre chiavi:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>io.modelcontextprotocol/protocolVersion
io.modelcontextprotocol/clientCapabilities
io.modelcontextprotocol/clientInfo
</code></pre></div></div>

<p>Sono, letteralmente, il contenuto dell’<code class="language-plaintext highlighter-rouge">initialize</code> spalmato su ogni richiesta. E non sono facoltative: se ne manca una, il server rifiuta la richiesta.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{"error":{"code":-32602,"message":
  "Requests using protocol version '2026-07-28' must include
   '_meta/io.modelcontextprotocol/protocolVersion'."}}
</code></pre></div></div>

<p><strong>La parte server</strong> — <code class="language-plaintext highlighter-rouge">serverInfo</code> e <code class="language-plaintext highlighter-rouge">serverCapabilities</code> — permetteva al client di sapere che cosa era in grado di fare il server. Ora è diventata una richiesta ordinaria, <code class="language-plaintext highlighter-rouge">server/discover</code>, ed è facoltativa: un client che sa già quale tool vuole chiamare può saltarla e andare dritto al <code class="language-plaintext highlighter-rouge">tools/call</code>.</p>

<p>Si passa quindi da una negoziazione, con due parti che si mettono d’accordo e ricordano l’accordo, a una dichiarazione ripetuta a ogni richiesta.</p>

<div class="hint-box p-4 my-6 rounded-r border-l-4 border-blue-500">
  <div class="flex items-start">
    <p class="text-sm text-blue-700 dark:text-blue-300">
      👉 Un trucco pratico: con l'SDK .NET, una GET sull'endpoint di un server stateless risponde <code>405 Method Not Allowed</code>, perché in quella modalità i verbi GET e DELETE non vengono nemmeno registrati. Non è garantito dalla specifica, ma è un modo rapido per capire da fuori come sta girando un server.
    </p>
  </div>
</div>

<h2 id="le-altre-modifiche">Le altre modifiche</h2>

<ul>
  <li><strong>Routing basato su header.</strong> Sono stati introdotti <code class="language-plaintext highlighter-rouge">Mcp-Method</code> e <code class="language-plaintext highlighter-rouge">Mcp-Name</code>. Dato che è tutto basato su POST verso lo stesso endpoint, un proxy che avesse voluto instradare le richieste avrebbe dovuto leggere e analizzare il body JSON: ora gli bastano due header.</li>
  <li><strong>Liste cacheabili.</strong> Le risposte di <code class="language-plaintext highlighter-rouge">tools/list</code>, <code class="language-plaintext highlighter-rouge">prompts/list</code>, <code class="language-plaintext highlighter-rouge">resources/list</code> e <code class="language-plaintext highlighter-rouge">resources/read</code> portano ora <code class="language-plaintext highlighter-rouge">ttlMs</code> e <code class="language-plaintext highlighter-rouge">cacheScope</code>: il client sa per quanto tenerle e con che ambito. Senza una sessione da cui dipendere, la cache diventa possibile.</li>
  <li><strong>Roots e Sampling deprecate.</strong> Erano richieste che partivano dal server verso il client. Continuano a funzionare per almeno dodici mesi, ma le nuove implementazioni non dovrebbero adottarle.</li>
  <li><strong>Logging deprecato.</strong> Era una notifica e non una domanda in attesa di risposta, quindi non potrebbe nemmeno avere un veicolo MRTR. La soluzione diventa, giustamente, OpenTelemetry — e se vuoi approfondire, c’è <a href="/serie/">la serie dedicata</a> su questo blog.</li>
  <li><strong>Elicitation viva.</strong> Non è deprecata: cambia solo il veicolo, e diventa il caso d’uso principale di MRTR.</li>
  <li><strong>Autorizzazione.</strong> Anche qui ci sono novità, ma sono abbastanza corpose da meritare un articolo a parte.</li>
</ul>

<h2 id="pro-contro-e-a-cosa-fare-attenzione">Pro, contro e a cosa fare attenzione</h2>

<p>Come in tutte le cose, ci sono pro e contro, e punti su cui bisogna prestare attenzione.</p>

<p>A fronte di una maggiore scalabilità del sistema, abbiamo più round-trip, quindi più latenza: un’interazione che prima stava dentro una chiamata ora ne richiede due. Su una rete locale si parla di millisecondi, su una connessione lenta di molto di più.</p>

<p>E soprattutto dobbiamo prestare attenzione a come scriviamo il server, perché da ora in avanti va gestita l’idempotenza.</p>

<div class="warning-box p-4 my-6 rounded-r border-l-4 border-red-500 bg-red-50 dark:bg-gray-800 dark:border-red-400">
  <div class="flex items-start">
    <span class="text-red-600 dark:text-red-400 text-lg mr-3">⚠️</span>
    <div class="text-sm text-red-800 dark:text-gray-100">
      <span class="font-bold">Attenzione:</span> il round 1 e il round 2 sono due chiamate allo stesso tool con gli stessi argomenti. Tutto quello che il tool fa <em>prima</em> di chiedere l'informazione che gli manca viene quindi eseguito due volte. Gli effetti collaterali vanno spostati dopo il punto in cui il tool ha tutto quello che gli serve, oppure protetti con una chiave di deduplicazione.
    </div>
  </div>
</div>

<h2 id="conclusioni">Conclusioni</h2>

<p>MCP è passato da “il server può richiamare il client nel mezzo di una chiamata” a “il server risponde e si dimentica di te”. La fine delle sessioni, gli header nuovi, le liste cacheabili e le feature deprecate discendono tutti da questa scelta.</p>

<p>Il prezzo è qualche round-trip in più e la responsabilità dell’idempotenza. Quello che si ottiene è un server che si mette dietro un load balancer qualsiasi senza configurazioni particolari, e che può essere riavviato senza effetti drastici sulle conversazioni in corso.</p>

<p>Nel prossimo articolo passiamo alla pratica: <a href="/mcp-dotnet-2026-07-28/">come scrivere in .NET un server con la nuova revisione e come migrare quelli vecchi</a> — perché alzare il pacchetto compila senza un warning e poi si rompe a runtime.</p>

<h2 id="risorse-utili">Risorse utili</h2>

<ul>
  <li><a href="https://blog.modelcontextprotocol.io/posts/2026-07-28/">Annuncio ufficiale della revisione 2026-07-28</a></li>
  <li><a href="https://modelcontextprotocol.io">Documentazione ufficiale MCP</a></li>
</ul>]]></content><author><name>Alessandro Mengoli</name></author><category term="AI" /><category term="Protocols" /><category term="mcp" /><category term="ai" /><category term="llm" /><category term="protocols" /><category term="http" /><summary type="html"><![CDATA[La revisione 2026-07-28 di MCP (Model Context Protocol) segna il passaggio da stateful a stateless. Vediamo cosa cambia: MRTR, la fine dell'handshake initialize e cosa significa per chi deve scalare un server.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://www.devpills.net/assets/images/posts/mcp-mrtr-vs-sessione.svg" /><media:content medium="image" url="https://www.devpills.net/assets/images/posts/mcp-mrtr-vs-sessione.svg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry xml:lang="it"><title type="html">Keyed Services in .NET: pattern e pitfall</title><link href="https://www.devpills.net/di-keyed-services/" rel="alternate" type="text/html" title="Keyed Services in .NET: pattern e pitfall" /><published>2026-02-28T00:00:00+01:00</published><updated>2026-02-28T00:00:00+01:00</updated><id>https://www.devpills.net/di-keyed-services</id><content type="html" xml:base="https://www.devpills.net/di-keyed-services/"><![CDATA[<p>Nei primi tre articoli della serie abbiamo visto i <a href="/di-lifetime-regole/">lifetime</a>, gli <a href="/anti-pattern-lifetime-di-dotnet/">anti-pattern dei lifetime</a> e i <a href="/di-service-locator-factory-registrazioni/">pitfall su registrazione e risoluzione</a>. In questo ultimo articolo ci concentriamo sui <strong>Keyed Services</strong>, introdotti in .NET 8.</p>

<p>Prima dei Keyed Services, registrare più implementazioni della stessa interfaccia e scegliere quale iniettare richiedeva workaround: factory manuali, risoluzione tramite <code class="language-plaintext highlighter-rouge">IEnumerable&lt;T&gt;</code> con filtro LINQ, o container di terze parti come Autofac. Era un’operazione comune — pensate a scenari con più provider di cache, più strategy di notifica, o più implementazioni di un gateway di pagamento — ma il container nativo non la supportava direttamente.</p>

<p>I Keyed Services risolvono il problema permettendo di associare una chiave a ogni registrazione e di specificare quale implementazione iniettare tramite l’attributo <code class="language-plaintext highlighter-rouge">[FromKeyedServices]</code>. Non serve più filtrare a runtime o costruire factory ad hoc: la scelta dell’implementazione è dichiarativa e avviene al momento della risoluzione.</p>

<h2 id="registrazione-e-risoluzione">Registrazione e risoluzione</h2>

<p>La registrazione avviene con i metodi <code class="language-plaintext highlighter-rouge">AddKeyed{Lifetime}</code>, passando una chiave come primo parametro:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">services</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">ServiceCollection</span><span class="p">();</span>
<span class="n">services</span><span class="p">.</span><span class="n">AddKeyedSingleton</span><span class="p">&lt;</span><span class="n">ICache</span><span class="p">,</span> <span class="n">RedisCache</span><span class="p">&gt;(</span><span class="s">"redis"</span><span class="p">);</span>
<span class="n">services</span><span class="p">.</span><span class="n">AddKeyedSingleton</span><span class="p">&lt;</span><span class="n">ICache</span><span class="p">,</span> <span class="n">MemoryCache</span><span class="p">&gt;(</span><span class="s">"memory"</span><span class="p">);</span>
</code></pre></div></div>

<p>Per iniettare una specifica implementazione, si usa l’attributo <code class="language-plaintext highlighter-rouge">[FromKeyedServices]</code> nel costruttore del servizio che ne ha bisogno:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">class</span> <span class="nc">ProductService</span><span class="p">(</span>
    <span class="p">[</span><span class="nf">FromKeyedServices</span><span class="p">(</span><span class="s">"redis"</span><span class="p">)]</span> <span class="n">ICache</span> <span class="n">cache</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="kt">string</span> <span class="nf">GetProducts</span><span class="p">()</span> <span class="p">=&gt;</span> <span class="n">cache</span><span class="p">.</span><span class="nf">Get</span><span class="p">(</span><span class="s">"products"</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Il container sa quale <code class="language-plaintext highlighter-rouge">ICache</code> fornire perché la chiave <code class="language-plaintext highlighter-rouge">"redis"</code> nella richiesta corrisponde a quella usata nella registrazione. In Minimal API la stessa risoluzione avviene direttamente nei parametri dell’endpoint:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">app</span><span class="p">.</span><span class="nf">MapGet</span><span class="p">(</span><span class="s">"/products"</span><span class="p">,</span> <span class="p">([</span><span class="nf">FromKeyedServices</span><span class="p">(</span><span class="s">"memory"</span><span class="p">)]</span> <span class="n">ICache</span> <span class="n">cache</span><span class="p">)</span> <span class="p">=&gt;</span>
<span class="p">{</span>
    <span class="k">return</span> <span class="n">cache</span><span class="p">.</span><span class="nf">Get</span><span class="p">(</span><span class="s">"products"</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div></div>

<p>La risoluzione manuale dal service provider usa <code class="language-plaintext highlighter-rouge">GetRequiredKeyedService</code>:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">redis</span> <span class="p">=</span> <span class="n">provider</span><span class="p">.</span><span class="n">GetRequiredKeyedService</span><span class="p">&lt;</span><span class="n">ICache</span><span class="p">&gt;(</span><span class="s">"redis"</span><span class="p">);</span>
<span class="kt">var</span> <span class="n">memory</span> <span class="p">=</span> <span class="n">provider</span><span class="p">.</span><span class="n">GetRequiredKeyedService</span><span class="p">&lt;</span><span class="n">ICache</span><span class="p">&gt;(</span><span class="s">"memory"</span><span class="p">);</span>
</code></pre></div></div>

<p>Un dettaglio importante: la chiave non è limitata a <code class="language-plaintext highlighter-rouge">string</code>. Può essere qualsiasi <code class="language-plaintext highlighter-rouge">object</code> che implementa correttamente <code class="language-plaintext highlighter-rouge">Equals</code> — <code class="language-plaintext highlighter-rouge">int</code>, <code class="language-plaintext highlighter-rouge">enum</code>, <code class="language-plaintext highlighter-rouge">record</code>, o qualsiasi tipo custom. Questo apre possibilità interessanti che vedremo nella sezione sulle best practice.</p>

<h2 id="anykey-registrazione-fallback">AnyKey: registrazione fallback</h2>

<p><code class="language-plaintext highlighter-rouge">KeyedService.AnyKey</code> è un valore speciale che permette di registrare un’implementazione di default. Quando il container riceve una richiesta per una chiave che non ha una registrazione esplicita, usa la registrazione <code class="language-plaintext highlighter-rouge">AnyKey</code> come fallback:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">services</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">ServiceCollection</span><span class="p">();</span>
<span class="n">services</span><span class="p">.</span><span class="n">AddKeyedSingleton</span><span class="p">&lt;</span><span class="n">ICache</span><span class="p">,</span> <span class="n">PremiumCache</span><span class="p">&gt;(</span><span class="s">"premium"</span><span class="p">);</span>
<span class="n">services</span><span class="p">.</span><span class="n">AddKeyedSingleton</span><span class="p">&lt;</span><span class="n">ICache</span><span class="p">,</span> <span class="n">DefaultCache</span><span class="p">&gt;(</span><span class="n">KeyedService</span><span class="p">.</span><span class="n">AnyKey</span><span class="p">);</span>

<span class="kt">var</span> <span class="n">provider</span> <span class="p">=</span> <span class="n">services</span><span class="p">.</span><span class="nf">BuildServiceProvider</span><span class="p">();</span>

<span class="kt">var</span> <span class="n">premium</span> <span class="p">=</span> <span class="n">provider</span><span class="p">.</span><span class="n">GetRequiredKeyedService</span><span class="p">&lt;</span><span class="n">ICache</span><span class="p">&gt;(</span><span class="s">"premium"</span><span class="p">);</span>
<span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="n">premium</span><span class="p">.</span><span class="nf">GetType</span><span class="p">().</span><span class="n">Name</span><span class="p">);</span>  <span class="c1">// "PremiumCache"</span>

<span class="kt">var</span> <span class="n">basic</span> <span class="p">=</span> <span class="n">provider</span><span class="p">.</span><span class="n">GetRequiredKeyedService</span><span class="p">&lt;</span><span class="n">ICache</span><span class="p">&gt;(</span><span class="s">"basic"</span><span class="p">);</span>
<span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="n">basic</span><span class="p">.</span><span class="nf">GetType</span><span class="p">().</span><span class="n">Name</span><span class="p">);</span>    <span class="c1">// "DefaultCache" (fallback)</span>

<span class="kt">var</span> <span class="n">other</span> <span class="p">=</span> <span class="n">provider</span><span class="p">.</span><span class="n">GetRequiredKeyedService</span><span class="p">&lt;</span><span class="n">ICache</span><span class="p">&gt;(</span><span class="s">"anything"</span><span class="p">);</span>
<span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="n">other</span><span class="p">.</span><span class="nf">GetType</span><span class="p">().</span><span class="n">Name</span><span class="p">);</span>    <span class="c1">// "DefaultCache" (fallback)</span>
</code></pre></div></div>

<p>La chiave <code class="language-plaintext highlighter-rouge">"premium"</code> ha una registrazione dedicata, quindi il container restituisce <code class="language-plaintext highlighter-rouge">PremiumCache</code>. Le chiavi <code class="language-plaintext highlighter-rouge">"basic"</code> e <code class="language-plaintext highlighter-rouge">"anything"</code> non hanno registrazioni esplicite, quindi il container usa il fallback <code class="language-plaintext highlighter-rouge">AnyKey</code> e restituisce <code class="language-plaintext highlighter-rouge">DefaultCache</code>.</p>

<p>La registrazione con <code class="language-plaintext highlighter-rouge">AnyKey</code> supporta anche una factory che riceve la chiave richiesta come secondo parametro. Questo permette di creare istanze personalizzate in base alla chiave, senza dover registrare ogni variante singolarmente:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">services</span><span class="p">.</span><span class="n">AddKeyedSingleton</span><span class="p">&lt;</span><span class="n">ICache</span><span class="p">&gt;(</span><span class="n">KeyedService</span><span class="p">.</span><span class="n">AnyKey</span><span class="p">,</span> <span class="p">(</span><span class="n">sp</span><span class="p">,</span> <span class="n">key</span><span class="p">)</span> <span class="p">=&gt;</span>
<span class="p">{</span>
    <span class="k">return</span> <span class="k">new</span> <span class="nf">DefaultCache</span><span class="p">(</span><span class="n">key</span><span class="p">?.</span><span class="nf">ToString</span><span class="p">()</span> <span class="p">??</span> <span class="s">"unknown"</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div></div>

<h2 id="pitfall-keyed-e-non-keyed-sono-registrazioni-separate">Pitfall: Keyed e Non-Keyed sono registrazioni separate</h2>

<p>Un aspetto che sorprende molti sviluppatori è che le registrazioni keyed e non-keyed per la stessa interfaccia vivono in due mondi completamente separati. Registrare una versione keyed non ha alcun effetto sulla risoluzione non-keyed, e viceversa. Non si sovrascrivono e non interagiscono in alcun modo:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">services</span><span class="p">.</span><span class="n">AddSingleton</span><span class="p">&lt;</span><span class="n">ICache</span><span class="p">,</span> <span class="n">GlobalCache</span><span class="p">&gt;();</span>              <span class="c1">// non-keyed</span>
<span class="n">services</span><span class="p">.</span><span class="n">AddKeyedSingleton</span><span class="p">&lt;</span><span class="n">ICache</span><span class="p">,</span> <span class="n">RedisCache</span><span class="p">&gt;(</span><span class="s">"redis"</span><span class="p">);</span>   <span class="c1">// keyed</span>

<span class="kt">var</span> <span class="n">nonKeyed</span> <span class="p">=</span> <span class="n">provider</span><span class="p">.</span><span class="n">GetRequiredService</span><span class="p">&lt;</span><span class="n">ICache</span><span class="p">&gt;();</span>
<span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="n">nonKeyed</span><span class="p">.</span><span class="nf">GetType</span><span class="p">().</span><span class="n">Name</span><span class="p">);</span>                <span class="c1">// "GlobalCache"</span>

<span class="kt">var</span> <span class="n">keyed</span> <span class="p">=</span> <span class="n">provider</span><span class="p">.</span><span class="n">GetRequiredKeyedService</span><span class="p">&lt;</span><span class="n">ICache</span><span class="p">&gt;(</span><span class="s">"redis"</span><span class="p">);</span>
<span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="n">keyed</span><span class="p">.</span><span class="nf">GetType</span><span class="p">().</span><span class="n">Name</span><span class="p">);</span>                   <span class="c1">// "RedisCache"</span>
</code></pre></div></div>

<p>Questo comportamento è by design, ma va tenuto presente quando si progetta la struttura delle registrazioni. Se un servizio non specifica <code class="language-plaintext highlighter-rouge">[FromKeyedServices]</code>, riceve sempre la registrazione non-keyed — anche se esistono registrazioni keyed per la stessa interfaccia.</p>

<h2 id="pitfall-typo-nella-chiave-con-anykey-fallback">Pitfall: typo nella chiave con AnyKey fallback</h2>

<p><code class="language-plaintext highlighter-rouge">AnyKey</code> è comodo ma introduce un rischio che vale la pena conoscere: se si sbaglia a scrivere una chiave, il fallback restituisce l’implementazione di default senza segnalare l’errore. Il risultato è un bug silenzioso.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">services</span><span class="p">.</span><span class="n">AddKeyedSingleton</span><span class="p">&lt;</span><span class="n">ICache</span><span class="p">,</span> <span class="n">PremiumCache</span><span class="p">&gt;(</span><span class="s">"premium"</span><span class="p">);</span>
<span class="n">services</span><span class="p">.</span><span class="n">AddKeyedSingleton</span><span class="p">&lt;</span><span class="n">ICache</span><span class="p">,</span> <span class="n">DefaultCache</span><span class="p">&gt;(</span><span class="n">KeyedService</span><span class="p">.</span><span class="n">AnyKey</span><span class="p">);</span>

<span class="c1">// Typo: "premiun" invece di "premium"</span>
<span class="kt">var</span> <span class="n">cache</span> <span class="p">=</span> <span class="n">provider</span><span class="p">.</span><span class="n">GetRequiredKeyedService</span><span class="p">&lt;</span><span class="n">ICache</span><span class="p">&gt;(</span><span class="s">"premiun"</span><span class="p">);</span>
<span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="n">cache</span><span class="p">.</span><span class="nf">GetType</span><span class="p">().</span><span class="n">Name</span><span class="p">);</span> <span class="c1">// "DefaultCache" — non "PremiumCache"</span>
</code></pre></div></div>

<p>Il codice funziona, non lancia eccezioni, e restituisce un servizio. Ma è il servizio sbagliato. Senza <code class="language-plaintext highlighter-rouge">AnyKey</code>, lo stesso typo avrebbe lanciato un’<code class="language-plaintext highlighter-rouge">InvalidOperationException</code> — il che è preferibile, perché il problema è visibile e viene intercettato subito.</p>

<h3 id="come-mitigare">Come mitigare</h3>

<p>La soluzione è usare costanti o enum per le chiavi, evitando stringhe sparse nel codice:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">static</span> <span class="k">class</span> <span class="nc">CacheKeys</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">const</span> <span class="kt">string</span> <span class="n">Redis</span> <span class="p">=</span> <span class="s">"redis"</span><span class="p">;</span>
    <span class="k">public</span> <span class="k">const</span> <span class="kt">string</span> <span class="n">Memory</span> <span class="p">=</span> <span class="s">"memory"</span><span class="p">;</span>
    <span class="k">public</span> <span class="k">const</span> <span class="kt">string</span> <span class="n">Premium</span> <span class="p">=</span> <span class="s">"premium"</span><span class="p">;</span>
<span class="p">}</span>

<span class="n">services</span><span class="p">.</span><span class="n">AddKeyedSingleton</span><span class="p">&lt;</span><span class="n">ICache</span><span class="p">,</span> <span class="n">RedisCache</span><span class="p">&gt;(</span><span class="n">CacheKeys</span><span class="p">.</span><span class="n">Redis</span><span class="p">);</span>
<span class="n">services</span><span class="p">.</span><span class="n">AddKeyedSingleton</span><span class="p">&lt;</span><span class="n">ICache</span><span class="p">,</span> <span class="n">MemoryCache</span><span class="p">&gt;(</span><span class="n">CacheKeys</span><span class="p">.</span><span class="n">Memory</span><span class="p">);</span>
<span class="n">services</span><span class="p">.</span><span class="n">AddKeyedSingleton</span><span class="p">&lt;</span><span class="n">ICache</span><span class="p">,</span> <span class="n">PremiumCache</span><span class="p">&gt;(</span><span class="n">CacheKeys</span><span class="p">.</span><span class="n">Premium</span><span class="p">);</span>

<span class="k">public</span> <span class="k">class</span> <span class="nc">ProductService</span><span class="p">(</span>
    <span class="p">[</span><span class="nf">FromKeyedServices</span><span class="p">(</span><span class="n">CacheKeys</span><span class="p">.</span><span class="n">Redis</span><span class="p">)]</span> <span class="n">ICache</span> <span class="n">cache</span><span class="p">)</span>
<span class="p">{</span> <span class="p">}</span>
</code></pre></div></div>

<p>In alternativa, sfruttando il fatto che le chiavi possono essere qualsiasi tipo con <code class="language-plaintext highlighter-rouge">Equals</code>, si può usare un <code class="language-plaintext highlighter-rouge">enum</code>:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">enum</span> <span class="n">CacheType</span> <span class="p">{</span> <span class="n">Redis</span><span class="p">,</span> <span class="n">Memory</span><span class="p">,</span> <span class="n">Premium</span> <span class="p">}</span>

<span class="n">services</span><span class="p">.</span><span class="n">AddKeyedSingleton</span><span class="p">&lt;</span><span class="n">ICache</span><span class="p">,</span> <span class="n">RedisCache</span><span class="p">&gt;(</span><span class="n">CacheType</span><span class="p">.</span><span class="n">Redis</span><span class="p">);</span>
<span class="n">services</span><span class="p">.</span><span class="n">AddKeyedSingleton</span><span class="p">&lt;</span><span class="n">ICache</span><span class="p">,</span> <span class="n">MemoryCache</span><span class="p">&gt;(</span><span class="n">CacheType</span><span class="p">.</span><span class="n">Memory</span><span class="p">);</span>
</code></pre></div></div>

<p>In entrambi i casi, un typo diventa un errore di compilazione — eliminando il problema alla radice.</p>

<h2 id="breaking-change-in-net-10-getkeyedservice-con-anykey">Breaking change in .NET 10: GetKeyedService con AnyKey</h2>

<p>In .NET 10 il comportamento di <code class="language-plaintext highlighter-rouge">GetKeyedService()</code> e <code class="language-plaintext highlighter-rouge">GetKeyedServices()</code> con <code class="language-plaintext highlighter-rouge">KeyedService.AnyKey</code> è stato modificato per correggere un’inconsistenza semantica che esisteva fin dall’introduzione dei Keyed Services.</p>

<h3 id="il-problema-concettuale">Il problema concettuale</h3>

<p><code class="language-plaintext highlighter-rouge">AnyKey</code> è concepito come un <strong>wildcard per la registrazione</strong> — un meccanismo di fallback che dice al container “usa questa implementazione per qualsiasi chiave non registrata esplicitamente”. Non è una chiave di risoluzione. Ma in .NET 8 e 9, il container lo trattava anche come tale, il che portava a comportamenti confusi: <code class="language-plaintext highlighter-rouge">GetKeyedService(KeyedService.AnyKey)</code> restituiva il servizio registrato con <code class="language-plaintext highlighter-rouge">AnyKey</code> come se fosse una chiave qualsiasi, e <code class="language-plaintext highlighter-rouge">GetKeyedServices(KeyedService.AnyKey)</code> restituiva tutte le registrazioni, incluse quelle <code class="language-plaintext highlighter-rouge">AnyKey</code>.</p>

<h3 id="il-cambiamento">Il cambiamento</h3>

<p>In .NET 10, <code class="language-plaintext highlighter-rouge">GetKeyedService()</code> (singolare) con <code class="language-plaintext highlighter-rouge">AnyKey</code> lancia un’<code class="language-plaintext highlighter-rouge">InvalidOperationException</code>:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">services</span><span class="p">.</span><span class="n">AddKeyedSingleton</span><span class="p">&lt;</span><span class="n">ICache</span><span class="p">,</span> <span class="n">DefaultCache</span><span class="p">&gt;(</span><span class="n">KeyedService</span><span class="p">.</span><span class="n">AnyKey</span><span class="p">);</span>
<span class="n">services</span><span class="p">.</span><span class="n">AddKeyedSingleton</span><span class="p">&lt;</span><span class="n">ICache</span><span class="p">,</span> <span class="n">PremiumCache</span><span class="p">&gt;(</span><span class="s">"premium"</span><span class="p">);</span>

<span class="kt">var</span> <span class="n">provider</span> <span class="p">=</span> <span class="n">services</span><span class="p">.</span><span class="nf">BuildServiceProvider</span><span class="p">();</span>

<span class="c1">// .NET 8/9: restituiva DefaultCache</span>
<span class="c1">// .NET 10: lancia InvalidOperationException</span>
<span class="kt">var</span> <span class="n">service</span> <span class="p">=</span> <span class="n">provider</span><span class="p">.</span><span class="n">GetKeyedService</span><span class="p">&lt;</span><span class="n">ICache</span><span class="p">&gt;(</span><span class="n">KeyedService</span><span class="p">.</span><span class="n">AnyKey</span><span class="p">);</span>
<span class="c1">// "Cannot resolve a single service using AnyKey."</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">GetKeyedServices()</code> (plurale) con <code class="language-plaintext highlighter-rouge">AnyKey</code> non restituisce più le registrazioni <code class="language-plaintext highlighter-rouge">AnyKey</code> — restituisce solo i servizi registrati con chiavi specifiche:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// .NET 8/9</span>
<span class="kt">var</span> <span class="n">all</span> <span class="p">=</span> <span class="n">provider</span><span class="p">.</span><span class="n">GetKeyedServices</span><span class="p">&lt;</span><span class="n">ICache</span><span class="p">&gt;(</span><span class="n">KeyedService</span><span class="p">.</span><span class="n">AnyKey</span><span class="p">);</span>
<span class="c1">// [DefaultCache, PremiumCache] — tutte le registrazioni</span>

<span class="c1">// .NET 10</span>
<span class="kt">var</span> <span class="n">all</span> <span class="p">=</span> <span class="n">provider</span><span class="p">.</span><span class="n">GetKeyedServices</span><span class="p">&lt;</span><span class="n">ICache</span><span class="p">&gt;(</span><span class="n">KeyedService</span><span class="p">.</span><span class="n">AnyKey</span><span class="p">);</span>
<span class="c1">// [PremiumCache] — solo le chiavi specifiche, AnyKey esclusa</span>
</code></pre></div></div>

<p>Il razionale dietro il cambiamento è chiaro: <code class="language-plaintext highlighter-rouge">AnyKey</code> è un meccanismo di registrazione, non di risoluzione. Usarlo per risolvere un singolo servizio non aveva senso semanticamente e creava ambiguità.</p>

<h3 id="come-aggiornare-il-codice">Come aggiornare il codice</h3>

<p>Se il codice usa <code class="language-plaintext highlighter-rouge">GetKeyedService(KeyedService.AnyKey)</code> per ottenere il fallback, la migrazione è semplice. Si può registrare il default con una chiave esplicita:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">services</span><span class="p">.</span><span class="n">AddKeyedSingleton</span><span class="p">&lt;</span><span class="n">ICache</span><span class="p">,</span> <span class="n">DefaultCache</span><span class="p">&gt;(</span><span class="s">"default"</span><span class="p">);</span>
<span class="n">services</span><span class="p">.</span><span class="n">AddKeyedSingleton</span><span class="p">&lt;</span><span class="n">ICache</span><span class="p">,</span> <span class="n">PremiumCache</span><span class="p">&gt;(</span><span class="s">"premium"</span><span class="p">);</span>

<span class="kt">var</span> <span class="n">fallback</span> <span class="p">=</span> <span class="n">provider</span><span class="p">.</span><span class="n">GetKeyedService</span><span class="p">&lt;</span><span class="n">ICache</span><span class="p">&gt;(</span><span class="s">"default"</span><span class="p">);</span>
</code></pre></div></div>

<p>Oppure si può sfruttare la separazione tra keyed e non-keyed, registrando il default come servizio non-keyed:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">services</span><span class="p">.</span><span class="n">AddSingleton</span><span class="p">&lt;</span><span class="n">ICache</span><span class="p">,</span> <span class="n">DefaultCache</span><span class="p">&gt;();</span>                  <span class="c1">// non-keyed</span>
<span class="n">services</span><span class="p">.</span><span class="n">AddKeyedSingleton</span><span class="p">&lt;</span><span class="n">ICache</span><span class="p">,</span> <span class="n">PremiumCache</span><span class="p">&gt;(</span><span class="s">"premium"</span><span class="p">);</span>    <span class="c1">// keyed</span>

<span class="kt">var</span> <span class="n">fallback</span> <span class="p">=</span> <span class="n">provider</span><span class="p">.</span><span class="n">GetService</span><span class="p">&lt;</span><span class="n">ICache</span><span class="p">&gt;();</span>                    <span class="c1">// DefaultCache</span>
<span class="kt">var</span> <span class="n">premium</span> <span class="p">=</span> <span class="n">provider</span><span class="p">.</span><span class="n">GetKeyedService</span><span class="p">&lt;</span><span class="n">ICache</span><span class="p">&gt;(</span><span class="s">"premium"</span><span class="p">);</span>       <span class="c1">// PremiumCache</span>
</code></pre></div></div>

<p>La seconda opzione è particolarmente pulita: il default è il servizio “normale”, le varianti specifiche sono keyed. Qualsiasi classe che non ha bisogno di una variante specifica riceve il default tramite la classica constructor injection, senza attributi.</p>

<h2 id="conclusione">Conclusione</h2>

<p>I Keyed Services sono un’aggiunta al container DI nativo di .NET che copre uno scenario prima gestibile solo con workaround. I punti da tenere a mente: usare costanti o enum per le chiavi per avere la sicurezza del compilatore, valutare i rischi del fallback <code class="language-plaintext highlighter-rouge">AnyKey</code> — in particolare i bug silenziosi da typo — e ricordare che keyed e non-keyed sono registrazioni indipendenti che non si influenzano a vicenda. Per chi sta migrando a .NET 10, verificare che il codice non usi <code class="language-plaintext highlighter-rouge">GetKeyedService(KeyedService.AnyKey)</code> per risolvere servizi, perché in .NET 10 lancia un’eccezione.</p>

<p>Con questo articolo chiudiamo la serie sulla Dependency Injection in .NET. I temi che abbiamo coperto — lifetime e le loro regole, anti-pattern dei lifetime, pitfall su registrazione e risoluzione, e Keyed Services — sono le basi per usare la DI in modo consapevole ed evitare i problemi più comuni.</p>

<p>Gli esempi di codice di questa serie sono disponibili nel <a href="https://github.com/amengoli9/talks/tree/main/20260227_sharpcoding_rome">repository GitHub del talk</a> presentato a Sharp Coding Rome.</p>

<blockquote>
  <p><strong>Fonti:</strong></p>
  <ul>
    <li><a href="https://learn.microsoft.com/dotnet/core/extensions/dependency-injection#keyed-services">Microsoft Learn — Keyed services</a></li>
    <li><a href="https://learn.microsoft.com/dotnet/core/compatibility/extensions/10.0/getkeyedservice-anykey">Microsoft Learn — Breaking change: GetKeyedService with AnyKey in .NET 10</a></li>
    <li><a href="https://learn.microsoft.com/aspnet/core/fundamentals/dependency-injection#keyed-services">Microsoft Learn — Dependency injection in ASP.NET Core: Keyed services</a></li>
  </ul>
</blockquote>]]></content><author><name>Alessandro Mengoli</name></author><category term=".NET" /><category term="Best Practices" /><category term="dotnet" /><category term="dependency-injection" /><category term="keyed-services" /><category term="best-practices" /><summary type="html"><![CDATA[Come funzionano i Keyed Services introdotti in .NET 8, il meccanismo di fallback con AnyKey, i pitfall da evitare e la breaking change in .NET 10.]]></summary></entry><entry xml:lang="it"><title type="html">Service Locator, factory asincrone e registrazioni multiple in .NET</title><link href="https://www.devpills.net/di-service-locator-factory-registrazioni/" rel="alternate" type="text/html" title="Service Locator, factory asincrone e registrazioni multiple in .NET" /><published>2026-02-28T00:00:00+01:00</published><updated>2026-02-28T00:00:00+01:00</updated><id>https://www.devpills.net/di-service-locator-factory-registrazioni</id><content type="html" xml:base="https://www.devpills.net/di-service-locator-factory-registrazioni/"><![CDATA[<p>Nei primi due articoli della serie abbiamo visto i <a href="/di-lifetime-regole/">lifetime della DI</a> e i <a href="/anti-pattern-lifetime-di-dotnet/">relativi anti-pattern</a>. In questo articolo ci spostiamo su tre problemi che riguardano la registrazione e la risoluzione dei servizi: dipendenze nascoste, factory che bloccano il thread e override silenziosi.</p>

<p>Sono pitfall meno discussi rispetto alla Captive Dependency, ma altrettanto frequenti — soprattutto nelle applicazioni che crescono, dove le registrazioni si distribuiscono tra <code class="language-plaintext highlighter-rouge">Program.cs</code>, extension method di librerie interne e pacchetti NuGet.</p>

<h2 id="service-locator">Service Locator</h2>

<p>Il Service Locator è un pattern in cui una classe riceve <code class="language-plaintext highlighter-rouge">IServiceProvider</code> nel costruttore e lo usa per risolvere le proprie dipendenze internamente, invece di dichiararle esplicitamente.</p>

<h3 id="il-problema">Il problema</h3>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">class</span> <span class="nc">NotificationService</span><span class="p">(</span><span class="n">IServiceProvider</span> <span class="n">provider</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">void</span> <span class="nf">SendNotification</span><span class="p">(</span><span class="kt">string</span> <span class="n">userId</span><span class="p">,</span> <span class="kt">string</span> <span class="n">message</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">emailSender</span> <span class="p">=</span> <span class="n">provider</span><span class="p">.</span><span class="n">GetRequiredService</span><span class="p">&lt;</span><span class="n">IEmailSender</span><span class="p">&gt;();</span>
        <span class="kt">var</span> <span class="n">userRepo</span> <span class="p">=</span> <span class="n">provider</span><span class="p">.</span><span class="n">GetRequiredService</span><span class="p">&lt;</span><span class="n">IUserRepository</span><span class="p">&gt;();</span>

        <span class="kt">var</span> <span class="n">user</span> <span class="p">=</span> <span class="n">userRepo</span><span class="p">.</span><span class="nf">GetById</span><span class="p">(</span><span class="n">userId</span><span class="p">);</span>
        <span class="n">emailSender</span><span class="p">.</span><span class="nf">Send</span><span class="p">(</span><span class="n">user</span><span class="p">.</span><span class="n">Email</span><span class="p">,</span> <span class="n">message</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Guardando il costruttore, l’unica dipendenza visibile è <code class="language-plaintext highlighter-rouge">IServiceProvider</code>. Per sapere che <code class="language-plaintext highlighter-rouge">NotificationService</code> ha bisogno di <code class="language-plaintext highlighter-rouge">IEmailSender</code> e <code class="language-plaintext highlighter-rouge">IUserRepository</code>, bisogna leggere l’implementazione di ogni metodo. In una classe con più metodi, il numero di dipendenze nascoste può crescere senza controllo.</p>

<p>Anche i test diventano più complicati: invece di passare direttamente i mock delle dipendenze, bisogna costruire e configurare un intero <code class="language-plaintext highlighter-rouge">IServiceProvider</code>.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Test con Service Locator: serve mockare IServiceProvider</span>
<span class="kt">var</span> <span class="n">mockProvider</span> <span class="p">=</span> <span class="k">new</span> <span class="n">Mock</span><span class="p">&lt;</span><span class="n">IServiceProvider</span><span class="p">&gt;();</span>
<span class="n">mockProvider</span><span class="p">.</span><span class="nf">Setup</span><span class="p">(</span><span class="n">p</span> <span class="p">=&gt;</span> <span class="n">p</span><span class="p">.</span><span class="nf">GetService</span><span class="p">(</span><span class="k">typeof</span><span class="p">(</span><span class="n">IEmailSender</span><span class="p">)))</span>
            <span class="p">.</span><span class="nf">Returns</span><span class="p">(</span><span class="k">new</span> <span class="n">Mock</span><span class="p">&lt;</span><span class="n">IEmailSender</span><span class="p">&gt;().</span><span class="n">Object</span><span class="p">);</span>
<span class="n">mockProvider</span><span class="p">.</span><span class="nf">Setup</span><span class="p">(</span><span class="n">p</span> <span class="p">=&gt;</span> <span class="n">p</span><span class="p">.</span><span class="nf">GetService</span><span class="p">(</span><span class="k">typeof</span><span class="p">(</span><span class="n">IUserRepository</span><span class="p">)))</span>
            <span class="p">.</span><span class="nf">Returns</span><span class="p">(</span><span class="k">new</span> <span class="n">Mock</span><span class="p">&lt;</span><span class="n">IUserRepository</span><span class="p">&gt;().</span><span class="n">Object</span><span class="p">);</span>
<span class="c1">// ... e così via per ogni dipendenza nascosta</span>
</code></pre></div></div>

<p>Le linee guida Microsoft sono esplicite: <em>“Avoid using the service locator pattern. For example, don’t invoke GetService to obtain a service instance when you can use DI instead.”</em></p>

<h3 id="la-soluzione">La soluzione</h3>

<p>Dichiarare le dipendenze nel costruttore:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">class</span> <span class="nc">NotificationService</span><span class="p">(</span>
    <span class="n">IEmailSender</span> <span class="n">emailSender</span><span class="p">,</span>
    <span class="n">IUserRepository</span> <span class="n">userRepo</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">void</span> <span class="nf">SendNotification</span><span class="p">(</span><span class="kt">string</span> <span class="n">userId</span><span class="p">,</span> <span class="kt">string</span> <span class="n">message</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">user</span> <span class="p">=</span> <span class="n">userRepo</span><span class="p">.</span><span class="nf">GetById</span><span class="p">(</span><span class="n">userId</span><span class="p">);</span>
        <span class="n">emailSender</span><span class="p">.</span><span class="nf">Send</span><span class="p">(</span><span class="n">user</span><span class="p">.</span><span class="n">Email</span><span class="p">,</span> <span class="n">message</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Le dipendenze sono visibili, il costruttore documenta ciò che serve alla classe, e i test sono diretti:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">mockEmail</span> <span class="p">=</span> <span class="k">new</span> <span class="n">Mock</span><span class="p">&lt;</span><span class="n">IEmailSender</span><span class="p">&gt;();</span>
<span class="kt">var</span> <span class="n">sut</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">NotificationService</span><span class="p">(</span>
    <span class="n">mockEmail</span><span class="p">.</span><span class="n">Object</span><span class="p">,</span>
    <span class="n">Mock</span><span class="p">.</span><span class="n">Of</span><span class="p">&lt;</span><span class="n">IUserRepository</span><span class="p">&gt;());</span>

<span class="n">sut</span><span class="p">.</span><span class="nf">SendNotification</span><span class="p">(</span><span class="s">"user1"</span><span class="p">,</span> <span class="s">"Hello"</span><span class="p">);</span>

<span class="n">mockEmail</span><span class="p">.</span><span class="nf">Verify</span><span class="p">(</span><span class="n">e</span> <span class="p">=&gt;</span> <span class="n">e</span><span class="p">.</span><span class="nf">Send</span><span class="p">(</span><span class="n">It</span><span class="p">.</span><span class="n">IsAny</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">&gt;(),</span> <span class="s">"Hello"</span><span class="p">),</span> <span class="n">Times</span><span class="p">.</span><span class="n">Once</span><span class="p">);</span>
</code></pre></div></div>

<div class="hint-box p-4 my-6 rounded-r border-l-4 border-blue-500">
  <div class="flex items-start">
    <p class="text-sm text-blue-700 dark:text-blue-300">
      Ci sono casi in cui iniettare <code>IServiceProvider</code> è legittimo — ad esempio nei middleware di ASP.NET Core, in scenari di plugin/estensione dinamici, o quando si usa <code>IServiceScopeFactory</code> per creare scope in servizi Singleton (come visto nell'articolo precedente). La differenza è tra usarlo come meccanismo infrastrutturale e usarlo come sostituto della constructor injection.
    </p>
  </div>
</div>

<h2 id="factory-asincrone-e-deadlock">Factory asincrone e deadlock</h2>

<p>Il container DI di .NET risolve i servizi in modo sincrono. Non esiste un <code class="language-plaintext highlighter-rouge">GetRequiredServiceAsync</code>. Questo significa che le factory passate a <code class="language-plaintext highlighter-rouge">AddSingleton</code>, <code class="language-plaintext highlighter-rouge">AddScoped</code> e <code class="language-plaintext highlighter-rouge">AddTransient</code> devono essere sincrone.</p>

<h3 id="il-problema-1">Il problema</h3>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">services</span><span class="p">.</span><span class="n">AddSingleton</span><span class="p">&lt;</span><span class="n">IConnection</span><span class="p">&gt;(</span><span class="n">sp</span> <span class="p">=&gt;</span>
<span class="p">{</span>
    <span class="k">return</span> <span class="nf">CreateConnectionAsync</span><span class="p">(</span><span class="n">sp</span><span class="p">).</span><span class="n">Result</span><span class="p">;</span> <span class="c1">// blocca il thread</span>
<span class="p">});</span>

<span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="n">IConnection</span><span class="p">&gt;</span> <span class="nf">CreateConnectionAsync</span><span class="p">(</span><span class="n">IServiceProvider</span> <span class="n">sp</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">config</span> <span class="p">=</span> <span class="n">sp</span><span class="p">.</span><span class="n">GetRequiredService</span><span class="p">&lt;</span><span class="n">IConfiguration</span><span class="p">&gt;();</span>
    <span class="kt">var</span> <span class="n">connection</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">SqlConnection</span><span class="p">(</span><span class="n">config</span><span class="p">.</span><span class="nf">GetConnectionString</span><span class="p">(</span><span class="s">"Default"</span><span class="p">));</span>
    <span class="k">await</span> <span class="n">connection</span><span class="p">.</span><span class="nf">OpenAsync</span><span class="p">();</span>
    <span class="k">return</span> <span class="n">connection</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Chiamare <code class="language-plaintext highlighter-rouge">.Result</code> su un <code class="language-plaintext highlighter-rouge">Task</code> all’interno di una factory DI blocca il thread corrente fino al completamento dell’operazione asincrona. Questo può causare un deadlock per due motivi:</p>

<ul>
  <li>Se esiste un <code class="language-plaintext highlighter-rouge">SynchronizationContext</code> (es. in WPF, WinForms, o Blazor Server), il <code class="language-plaintext highlighter-rouge">Task</code> potrebbe aver bisogno di riprendere sullo stesso thread che è bloccato in attesa.</li>
  <li>Se il metodo asincrono chiama a sua volta <code class="language-plaintext highlighter-rouge">GetRequiredService</code> sul container, questo può entrare in conflitto con la risoluzione già in corso, che detiene un lock interno.</li>
</ul>

<p>La documentazione Microsoft classifica questo come un anti-pattern esplicito: <em>“If the factory is asynchronous, and you use Task&lt;TResult&gt;.Result, it will cause a deadlock.”</em> La raccomandazione è: <em>“Keep DI factories fast and synchronous.”</em></p>

<h3 id="le-soluzioni">Le soluzioni</h3>

<p><strong>1) Lazy async initialization:</strong></p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">class</span> <span class="nc">ConnectionWrapper</span><span class="p">(</span><span class="n">IConfiguration</span> <span class="n">config</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">private</span> <span class="k">readonly</span> <span class="n">Lazy</span><span class="p">&lt;</span><span class="n">Task</span><span class="p">&lt;</span><span class="n">IConnection</span><span class="p">&gt;&gt;</span> <span class="n">_connection</span> <span class="p">=</span> <span class="k">new</span><span class="p">(</span><span class="k">async</span> <span class="p">()</span> <span class="p">=&gt;</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">conn</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">SqlConnection</span><span class="p">(</span><span class="n">config</span><span class="p">.</span><span class="nf">GetConnectionString</span><span class="p">(</span><span class="s">"Default"</span><span class="p">));</span>
        <span class="k">await</span> <span class="n">conn</span><span class="p">.</span><span class="nf">OpenAsync</span><span class="p">();</span>
        <span class="k">return</span> <span class="n">conn</span><span class="p">;</span>
    <span class="p">});</span>

    <span class="k">public</span> <span class="n">Task</span><span class="p">&lt;</span><span class="n">IConnection</span><span class="p">&gt;</span> <span class="nf">GetConnectionAsync</span><span class="p">()</span> <span class="p">=&gt;</span> <span class="n">_connection</span><span class="p">.</span><span class="n">Value</span><span class="p">;</span>
<span class="p">}</span>

<span class="n">services</span><span class="p">.</span><span class="n">AddSingleton</span><span class="p">&lt;</span><span class="n">ConnectionWrapper</span><span class="p">&gt;();</span>
</code></pre></div></div>

<p>La factory registrata nel container è sincrona (crea un <code class="language-plaintext highlighter-rouge">ConnectionWrapper</code>). L’inizializzazione asincrona avviene alla prima chiamata di <code class="language-plaintext highlighter-rouge">GetConnectionAsync()</code>, fuori dal contesto di risoluzione del container.</p>

<p><strong>2) Inizializzazione prima della build:</strong></p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">builder</span> <span class="p">=</span> <span class="n">Host</span><span class="p">.</span><span class="nf">CreateApplicationBuilder</span><span class="p">(</span><span class="n">args</span><span class="p">);</span>

<span class="kt">var</span> <span class="n">connection</span> <span class="p">=</span> <span class="k">await</span> <span class="nf">CreateConnectionAsync</span><span class="p">(</span><span class="n">builder</span><span class="p">.</span><span class="n">Configuration</span><span class="p">);</span>
<span class="n">builder</span><span class="p">.</span><span class="n">Services</span><span class="p">.</span><span class="n">AddSingleton</span><span class="p">&lt;</span><span class="n">IConnection</span><span class="p">&gt;(</span><span class="n">connection</span><span class="p">);</span>
</code></pre></div></div>

<p>L’istanza viene creata in modo asincrono prima di essere registrata. Non c’è nessuna factory — si registra direttamente l’oggetto già inizializzato.</p>

<p><strong>3) <code class="language-plaintext highlighter-rouge">IHostedService</code> per il warm-up:</strong></p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">class</span> <span class="nc">ConnectionInitializer</span><span class="p">(</span><span class="n">ConnectionWrapper</span> <span class="n">wrapper</span><span class="p">)</span> <span class="p">:</span> <span class="n">IHostedService</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">async</span> <span class="n">Task</span> <span class="nf">StartAsync</span><span class="p">(</span><span class="n">CancellationToken</span> <span class="n">ct</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="k">await</span> <span class="n">wrapper</span><span class="p">.</span><span class="nf">GetConnectionAsync</span><span class="p">();</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="n">Task</span> <span class="nf">StopAsync</span><span class="p">(</span><span class="n">CancellationToken</span> <span class="n">ct</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="n">Task</span><span class="p">.</span><span class="n">CompletedTask</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Combinato con la soluzione 1, l’<code class="language-plaintext highlighter-rouge">IHostedService</code> forza l’inizializzazione della connessione all’avvio dell’applicazione, prima che arrivi la prima richiesta.</p>

<h2 id="registrazioni-multiple-lultima-vince">Registrazioni multiple: l’ultima vince</h2>

<p>Quando si registra la stessa interfaccia più volte nel container, il comportamento è diverso a seconda di come si risolve il servizio.</p>

<h3 id="il-problema-2">Il problema</h3>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">services</span><span class="p">.</span><span class="n">AddSingleton</span><span class="p">&lt;</span><span class="n">IMessageWriter</span><span class="p">,</span> <span class="n">ConsoleMessageWriter</span><span class="p">&gt;();</span>
<span class="n">services</span><span class="p">.</span><span class="n">AddSingleton</span><span class="p">&lt;</span><span class="n">IMessageWriter</span><span class="p">,</span> <span class="n">LoggingMessageWriter</span><span class="p">&gt;();</span>
<span class="n">services</span><span class="p">.</span><span class="n">AddSingleton</span><span class="p">&lt;</span><span class="n">IMessageWriter</span><span class="p">,</span> <span class="n">FileMessageWriter</span><span class="p">&gt;();</span>

<span class="kt">var</span> <span class="n">provider</span> <span class="p">=</span> <span class="n">services</span><span class="p">.</span><span class="nf">BuildServiceProvider</span><span class="p">();</span>

<span class="c1">// Risoluzione singola: l'ultimo registrato</span>
<span class="kt">var</span> <span class="n">writer</span> <span class="p">=</span> <span class="n">provider</span><span class="p">.</span><span class="n">GetRequiredService</span><span class="p">&lt;</span><span class="n">IMessageWriter</span><span class="p">&gt;();</span>
<span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="n">writer</span><span class="p">.</span><span class="nf">GetType</span><span class="p">().</span><span class="n">Name</span><span class="p">);</span> <span class="c1">// "FileMessageWriter"</span>

<span class="c1">// Risoluzione multipla: tutti, in ordine di registrazione</span>
<span class="kt">var</span> <span class="n">all</span> <span class="p">=</span> <span class="n">provider</span><span class="p">.</span><span class="n">GetRequiredService</span><span class="p">&lt;</span><span class="n">IEnumerable</span><span class="p">&lt;</span><span class="n">IMessageWriter</span><span class="p">&gt;&gt;();</span>
<span class="c1">// [ConsoleMessageWriter, LoggingMessageWriter, FileMessageWriter]</span>
</code></pre></div></div>

<p>Se si risolve un singolo <code class="language-plaintext highlighter-rouge">IMessageWriter</code>, il container restituisce l’ultima implementazione registrata. Le registrazioni precedenti non vengono sovrascritte — restano disponibili tramite <code class="language-plaintext highlighter-rouge">IEnumerable&lt;IMessageWriter&gt;</code> — ma per la risoluzione singola, l’ultima vince.</p>

<p>Questo è il comportamento documentato: <em>“The second call to AddSingleton overrides the previous one when resolved as IMyDependency and adds to the previous one when multiple services are resolved via IEnumerable&lt;IMyDependency&gt;. Services appear in the order they were registered when resolved via IEnumerable&lt;{SERVICE}&gt;.”</em></p>

<p>Il container non lancia alcun avviso. Se una extension method di una libreria registra la propria implementazione di <code class="language-plaintext highlighter-rouge">IMessageWriter</code> dopo la nostra, il nostro servizio viene sostituito senza alcuna segnalazione. Questo succede facilmente quando le registrazioni sono distribuite tra più file o pacchetti.</p>

<h3 id="le-soluzioni-1">Le soluzioni</h3>

<p><strong><code class="language-plaintext highlighter-rouge">TryAdd</code> — registra solo se il service type non è già presente:</strong></p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">Microsoft.Extensions.DependencyInjection.Extensions</span><span class="p">;</span>

<span class="n">services</span><span class="p">.</span><span class="n">AddSingleton</span><span class="p">&lt;</span><span class="n">IMessageWriter</span><span class="p">,</span> <span class="n">ConsoleMessageWriter</span><span class="p">&gt;();</span>
<span class="n">services</span><span class="p">.</span><span class="n">TryAddSingleton</span><span class="p">&lt;</span><span class="n">IMessageWriter</span><span class="p">,</span> <span class="n">LoggingMessageWriter</span><span class="p">&gt;();</span>
<span class="c1">// LoggingMessageWriter viene ignorato: IMessageWriter è già registrato</span>

<span class="kt">var</span> <span class="n">writer</span> <span class="p">=</span> <span class="n">provider</span><span class="p">.</span><span class="n">GetRequiredService</span><span class="p">&lt;</span><span class="n">IMessageWriter</span><span class="p">&gt;();</span>
<span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="n">writer</span><span class="p">.</span><span class="nf">GetType</span><span class="p">().</span><span class="n">Name</span><span class="p">);</span> <span class="c1">// "ConsoleMessageWriter"</span>
</code></pre></div></div>

<p>Con <code class="language-plaintext highlighter-rouge">TryAdd</code>, la prima registrazione vince. Le successive per lo stesso service type vengono ignorate.</p>

<p><strong><code class="language-plaintext highlighter-rouge">TryAddEnumerable</code> — evita duplicati nelle registrazioni multiple:</strong></p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">services</span><span class="p">.</span><span class="nf">TryAddEnumerable</span><span class="p">(</span>
    <span class="n">ServiceDescriptor</span><span class="p">.</span><span class="n">Singleton</span><span class="p">&lt;</span><span class="n">IMessageWriter</span><span class="p">,</span> <span class="n">ConsoleMessageWriter</span><span class="p">&gt;());</span>
<span class="n">services</span><span class="p">.</span><span class="nf">TryAddEnumerable</span><span class="p">(</span>
    <span class="n">ServiceDescriptor</span><span class="p">.</span><span class="n">Singleton</span><span class="p">&lt;</span><span class="n">IMessageWriter</span><span class="p">,</span> <span class="n">LoggingMessageWriter</span><span class="p">&gt;());</span>
<span class="n">services</span><span class="p">.</span><span class="nf">TryAddEnumerable</span><span class="p">(</span>
    <span class="n">ServiceDescriptor</span><span class="p">.</span><span class="n">Singleton</span><span class="p">&lt;</span><span class="n">IMessageWriter</span><span class="p">,</span> <span class="n">ConsoleMessageWriter</span><span class="p">&gt;());</span>
<span class="c1">// ^ Ignorata: la coppia (IMessageWriter, ConsoleMessageWriter) è già presente</span>

<span class="kt">var</span> <span class="n">all</span> <span class="p">=</span> <span class="n">provider</span><span class="p">.</span><span class="n">GetRequiredService</span><span class="p">&lt;</span><span class="n">IEnumerable</span><span class="p">&lt;</span><span class="n">IMessageWriter</span><span class="p">&gt;&gt;();</span>
<span class="c1">// [ConsoleMessageWriter, LoggingMessageWriter] — senza duplicati</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">TryAddEnumerable</code> controlla sia il service type che l’implementation type: aggiunge solo se quella specifica coppia non è già registrata.</p>

<p>Riepilogo:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Add&lt;T, Impl&gt;()        → aggiunge sempre (l'ultimo vince per singolo, tutti per IEnumerable)
TryAdd&lt;T, Impl&gt;()     → aggiunge solo se T non è già registrato
TryAddEnumerable(...)  → aggiunge solo se la coppia (T, Impl) non è già presente
</code></pre></div></div>

<div class="hint-box p-4 my-6 rounded-r border-l-4 border-blue-500">
  <div class="flex items-start">
    <p class="text-sm text-blue-700 dark:text-blue-300">
      Se sviluppate una libreria che espone una extension method tipo <code>AddMyLibrary()</code>, usate <code>TryAdd</code> nelle registrazioni. In questo modo chi consuma la libreria può fare override registrando la propria implementazione <strong>prima</strong> di chiamare <code>AddMyLibrary()</code>, senza conflitti.
    </p>
  </div>
</div>

<h2 id="conclusione">Conclusione</h2>

<p>I tre problemi di questo articolo hanno in comune il fatto di manifestarsi in silenzio. Il Service Locator nasconde le dipendenze e non produce errori. Una factory asincrona con <code class="language-plaintext highlighter-rouge">.Result</code> può funzionare in assenza di <code class="language-plaintext highlighter-rouge">SynchronizationContext</code> e poi bloccarsi in un contesto diverso. E una registrazione sovrascritta passa inosservata finché il servizio non si comporta in modo inatteso.</p>

<p>Le contromisure: constructor injection esplicita, factory sincrone (con lazy initialization o <code class="language-plaintext highlighter-rouge">IHostedService</code> per i casi asincroni), e <code class="language-plaintext highlighter-rouge">TryAdd</code>/<code class="language-plaintext highlighter-rouge">TryAddEnumerable</code> per registrazioni sicure.</p>

<p>Nel <a href="/di-keyed-services/">prossimo articolo</a> chiudiamo la serie con i Keyed Services: registrazione con chiave, <code class="language-plaintext highlighter-rouge">AnyKey</code>, e la breaking change in .NET 10.</p>

<blockquote>
  <p><strong>Fonti:</strong></p>
  <ul>
    <li><a href="https://learn.microsoft.com/dotnet/core/extensions/dependency-injection-guidelines">Microsoft Learn — Dependency injection guidelines</a></li>
    <li><a href="https://learn.microsoft.com/dotnet/core/extensions/dependency-injection-guidelines#async-di-factories-can-cause-deadlocks">Microsoft Learn — Async DI factories can cause deadlocks</a></li>
    <li><a href="https://learn.microsoft.com/dotnet/core/extensions/dependency-injection#service-registration-methods">Microsoft Learn — Service registration methods</a></li>
  </ul>
</blockquote>]]></content><author><name>Alessandro Mengoli</name></author><category term=".NET" /><category term="Best Practices" /><category term="dotnet" /><category term="dependency-injection" /><category term="antipattern" /><category term="best-practices" /><summary type="html"><![CDATA[Tre pitfall della DI in .NET che non riguardano i lifetime: il Service Locator pattern, il deadlock nelle factory asincrone e il comportamento delle registrazioni multiple.]]></summary></entry><entry xml:lang="it"><title type="html">Anti-pattern dei lifetime nella DI in .NET</title><link href="https://www.devpills.net/anti-pattern-lifetime-di-dotnet/" rel="alternate" type="text/html" title="Anti-pattern dei lifetime nella DI in .NET" /><published>2026-02-27T00:00:00+01:00</published><updated>2026-02-27T00:00:00+01:00</updated><id>https://www.devpills.net/anti-pattern-lifetime-di-dotnet</id><content type="html" xml:base="https://www.devpills.net/anti-pattern-lifetime-di-dotnet/"><![CDATA[<p>Nel <a href="/di-lifetime-regole/">precedente articolo</a> abbiamo visto i tre lifetime della DI in .NET e la regola fondamentale: un servizio non deve dipendere da un altro con lifetime più breve del proprio. In questo articolo vediamo cosa succede quando quella regola viene violata.</p>

<p>I problemi legati ai lifetime sono tra i più insidiosi nella Dependency Injection, perché spesso non si manifestano durante lo sviluppo o nei test. Un <code class="language-plaintext highlighter-rouge">DbContext</code> catturato in un Singleton, ad esempio, può funzionare per ore prima di mostrare comportamenti anomali. Un memory leak da Transient Disposable cresce lentamente e diventa visibile solo sotto carico. E un servizio Scoped che diventa Singleton passa inosservato finché due request non si trovano a condividere stato che dovrebbe essere isolato.</p>

<p>Vediamo questi tre scenari nel dettaglio.</p>

<h2 id="captive-dependency">Captive Dependency</h2>

<p>Il termine <strong>Captive Dependency</strong> è stato coniato da <a href="https://blog.ploeh.dk/2014/06/02/captive-dependency">Mark Seemann</a> e indica una situazione in cui un servizio con lifetime lungo (es. Singleton) cattura un servizio con lifetime più breve (es. Scoped). Il servizio catturato sopravvive oltre il suo ciclo di vita previsto.</p>

<h3 id="il-problema">Il problema</h3>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">services</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">ServiceCollection</span><span class="p">();</span>
<span class="n">services</span><span class="p">.</span><span class="n">AddSingleton</span><span class="p">&lt;</span><span class="n">OrderService</span><span class="p">&gt;();</span>
<span class="n">services</span><span class="p">.</span><span class="n">AddScoped</span><span class="p">&lt;</span><span class="n">FakeDbContext</span><span class="p">&gt;();</span>

<span class="k">public</span> <span class="k">class</span> <span class="nc">OrderService</span><span class="p">(</span><span class="n">FakeDbContext</span> <span class="n">db</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">void</span> <span class="nf">CreateOrder</span><span class="p">(</span><span class="n">Order</span> <span class="n">order</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">db</span><span class="p">.</span><span class="n">Orders</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="n">order</span><span class="p">);</span>
        <span class="n">db</span><span class="p">.</span><span class="nf">SaveChanges</span><span class="p">();</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">OrderService</code> è Singleton, quindi viene creato una sola volta. Il <code class="language-plaintext highlighter-rouge">FakeDbContext</code> iniettato al momento della creazione resta lo stesso per tutta la vita dell’applicazione, anche se era registrato come Scoped. Le request successive continuano a usare la stessa istanza di <code class="language-plaintext highlighter-rouge">DbContext</code> — con tutto lo stato accumulato dalle operazioni precedenti.</p>

<p>Il risultato: tracking di entità inconsistente, dati stale, possibili eccezioni.</p>

<h3 id="le-soluzioni">Le soluzioni</h3>

<p><strong>1) Usare <code class="language-plaintext highlighter-rouge">IServiceScopeFactory</code> per creare scope on-demand:</strong></p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">class</span> <span class="nc">OrderService</span><span class="p">(</span><span class="n">IServiceScopeFactory</span> <span class="n">scopeFactory</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">void</span> <span class="nf">CreateOrder</span><span class="p">(</span><span class="n">Order</span> <span class="n">order</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="k">using</span> <span class="nn">var</span> <span class="n">scope</span> <span class="p">=</span> <span class="n">scopeFactory</span><span class="p">.</span><span class="nf">CreateScope</span><span class="p">();</span>
        <span class="kt">var</span> <span class="n">db</span> <span class="p">=</span> <span class="n">scope</span><span class="p">.</span><span class="n">ServiceProvider</span><span class="p">.</span><span class="n">GetRequiredService</span><span class="p">&lt;</span><span class="n">FakeDbContext</span><span class="p">&gt;();</span>
        <span class="n">db</span><span class="p">.</span><span class="n">Orders</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="n">order</span><span class="p">);</span>
        <span class="n">db</span><span class="p">.</span><span class="nf">SaveChanges</span><span class="p">();</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Ogni chiamata a <code class="language-plaintext highlighter-rouge">CreateOrder</code> crea un nuovo scope con la propria istanza di <code class="language-plaintext highlighter-rouge">DbContext</code>, che viene correttamente disposta alla fine del blocco <code class="language-plaintext highlighter-rouge">using</code>. <code class="language-plaintext highlighter-rouge">IServiceScopeFactory</code> è sempre registrato come Singleton dal container, quindi può essere iniettato senza problemi in un servizio Singleton.</p>

<p><strong>2) Allineare i lifetime:</strong></p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">services</span><span class="p">.</span><span class="n">AddScoped</span><span class="p">&lt;</span><span class="n">OrderService</span><span class="p">&gt;();</span>
<span class="n">services</span><span class="p">.</span><span class="n">AddScoped</span><span class="p">&lt;</span><span class="n">FakeDbContext</span><span class="p">&gt;();</span>
</code></pre></div></div>

<p>Se <code class="language-plaintext highlighter-rouge">OrderService</code> non ha motivo di essere Singleton, la soluzione più semplice è registrarlo come Scoped. Entrambi i servizi vivranno per la durata della request.</p>

<p><strong>3) Abilitare <code class="language-plaintext highlighter-rouge">ValidateScopes</code>:</strong></p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">provider</span> <span class="p">=</span> <span class="n">services</span><span class="p">.</span><span class="nf">BuildServiceProvider</span><span class="p">(</span><span class="k">new</span> <span class="n">ServiceProviderOptions</span>
<span class="p">{</span>
    <span class="n">ValidateScopes</span> <span class="p">=</span> <span class="k">true</span><span class="p">,</span>
    <span class="n">ValidateOnBuild</span> <span class="p">=</span> <span class="k">true</span>
<span class="p">});</span>
</code></pre></div></div>

<p>Con <code class="language-plaintext highlighter-rouge">ValidateOnBuild = true</code>, il problema viene rilevato al momento della build del provider. Con <code class="language-plaintext highlighter-rouge">ValidateScopes = true</code> senza <code class="language-plaintext highlighter-rouge">ValidateOnBuild</code>, l’eccezione viene lanciata alla prima risoluzione di <code class="language-plaintext highlighter-rouge">OrderService</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Cannot consume scoped service 'FakeDbContext' from singleton 'OrderService'.
</code></pre></div></div>

<h2 id="transient-disposable-catturati-dal-container">Transient Disposable catturati dal container</h2>

<p>Quando un servizio Transient implementa <code class="language-plaintext highlighter-rouge">IDisposable</code>, il container DI mantiene un riferimento a ogni istanza creata per poterla disporre quando lo scope (o il container stesso) viene distrutto. Se queste istanze vengono risolte dal root provider (senza scope), si accumulano in memoria fino allo shutdown dell’applicazione.</p>

<h3 id="il-problema-1">Il problema</h3>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">class</span> <span class="nc">ExpensiveResource</span> <span class="p">:</span> <span class="n">IDisposable</span>
<span class="p">{</span>
    <span class="k">private</span> <span class="k">readonly</span> <span class="kt">byte</span><span class="p">[]</span> <span class="n">_buffer</span> <span class="p">=</span> <span class="k">new</span> <span class="kt">byte</span><span class="p">[</span><span class="m">1024</span> <span class="p">*</span> <span class="m">1024</span><span class="p">];</span> <span class="c1">// 1MB</span>

    <span class="k">public</span> <span class="k">void</span> <span class="nf">DoWork</span><span class="p">()</span> <span class="p">=&gt;</span> <span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="s">"Working..."</span><span class="p">);</span>

    <span class="k">public</span> <span class="k">void</span> <span class="nf">Dispose</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="s">$"</span><span class="p">{</span><span class="k">nameof</span><span class="p">(</span><span class="n">ExpensiveResource</span><span class="p">)}</span><span class="s"> disposed"</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="n">services</span><span class="p">.</span><span class="n">AddTransient</span><span class="p">&lt;</span><span class="n">ExpensiveResource</span><span class="p">&gt;();</span>

<span class="kt">var</span> <span class="n">provider</span> <span class="p">=</span> <span class="n">services</span><span class="p">.</span><span class="nf">BuildServiceProvider</span><span class="p">();</span>

<span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="p">=</span> <span class="m">0</span><span class="p">;</span> <span class="n">i</span> <span class="p">&lt;</span> <span class="m">1000</span><span class="p">;</span> <span class="n">i</span><span class="p">++)</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">resource</span> <span class="p">=</span> <span class="n">provider</span><span class="p">.</span><span class="n">GetRequiredService</span><span class="p">&lt;</span><span class="n">ExpensiveResource</span><span class="p">&gt;();</span>
    <span class="n">resource</span><span class="p">.</span><span class="nf">DoWork</span><span class="p">();</span>
    <span class="c1">// Ogni istanza viene trattenuta dal container</span>
<span class="p">}</span>
<span class="c1">// 1000 istanze × 1MB = ~1GB di memoria non rilasciata</span>
<span class="c1">// Il Dispose avviene solo quando il provider viene disposto (shutdown dell'app)</span>
</code></pre></div></div>

<p>Il container tiene traccia di tutte le istanze Transient <code class="language-plaintext highlighter-rouge">IDisposable</code> che crea, perché è sua responsabilità chiamare <code class="language-plaintext highlighter-rouge">Dispose()</code> su di esse. Se non c’è uno scope che delimita il ciclo di vita, le istanze restano in memoria fino alla fine dell’applicazione.</p>

<h3 id="le-soluzioni-1">Le soluzioni</h3>

<p><strong>1) Usare uno scope per ogni unità di lavoro:</strong></p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="p">=</span> <span class="m">0</span><span class="p">;</span> <span class="n">i</span> <span class="p">&lt;</span> <span class="m">1000</span><span class="p">;</span> <span class="n">i</span><span class="p">++)</span>
<span class="p">{</span>
    <span class="k">using</span> <span class="nn">var</span> <span class="n">scope</span> <span class="p">=</span> <span class="n">provider</span><span class="p">.</span><span class="nf">CreateScope</span><span class="p">();</span>
    <span class="kt">var</span> <span class="n">resource</span> <span class="p">=</span> <span class="n">scope</span><span class="p">.</span><span class="n">ServiceProvider</span><span class="p">.</span><span class="n">GetRequiredService</span><span class="p">&lt;</span><span class="n">ExpensiveResource</span><span class="p">&gt;();</span>
    <span class="n">resource</span><span class="p">.</span><span class="nf">DoWork</span><span class="p">();</span>
<span class="p">}</span> <span class="c1">// Alla fine di ogni scope, le istanze transient vengono disposte</span>
</code></pre></div></div>

<p><strong>2) Factory pattern per la gestione manuale del ciclo di vita:</strong></p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">services</span><span class="p">.</span><span class="n">AddSingleton</span><span class="p">&lt;</span><span class="n">Func</span><span class="p">&lt;</span><span class="n">ExpensiveResource</span><span class="p">&gt;&gt;(()</span> <span class="p">=&gt;</span> <span class="k">new</span> <span class="nf">ExpensiveResource</span><span class="p">());</span>

<span class="k">public</span> <span class="k">class</span> <span class="nc">MyService</span><span class="p">(</span><span class="n">Func</span><span class="p">&lt;</span><span class="n">ExpensiveResource</span><span class="p">&gt;</span> <span class="n">resourceFactory</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">void</span> <span class="nf">Process</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="k">using</span> <span class="nn">var</span> <span class="n">resource</span> <span class="p">=</span> <span class="nf">resourceFactory</span><span class="p">();</span>
        <span class="n">resource</span><span class="p">.</span><span class="nf">DoWork</span><span class="p">();</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Con il factory pattern, l’istanza non è gestita dal container — siamo noi a crearla e a chiamare <code class="language-plaintext highlighter-rouge">Dispose()</code>. Il container non trattiene alcun riferimento.</p>

<div class="warning-box p-4 my-6 rounded-r border-l-4 border-red-500 bg-red-50 dark:bg-gray-800 dark:border-red-400">
  <div class="flex items-start">
    <span class="text-red-600 dark:text-red-400 text-lg mr-3">⚠️</span>
    <div class="text-sm text-red-800 dark:text-gray-100">
      <span class="font-bold">Attenzione:</span> il consumer di una dipendenza <code>IDisposable</code> non deve mai chiamare <code>Dispose()</code> direttamente su di essa. È il container (o lo scope) che si occupa del cleanup. L'eccezione è il factory pattern, dove l'istanza è creata fuori dal container.
    </div>
  </div>
</div>

<p>Le linee guida ufficiali di Microsoft su questo punto sono chiare: evitare di registrare servizi <code class="language-plaintext highlighter-rouge">IDisposable</code> come Transient. Se necessario, usare il factory pattern.</p>

<h2 id="servizio-scoped-risolto-come-singleton">Servizio Scoped risolto come Singleton</h2>

<p>Se un servizio Scoped viene risolto dal root <code class="language-plaintext highlighter-rouge">IServiceProvider</code> — cioè senza creare uno scope — il suo lifetime viene di fatto promosso a Singleton. Il servizio viene creato una sola volta e riutilizzato per tutte le richieste successive.</p>

<h3 id="il-problema-2">Il problema</h3>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">class</span> <span class="nc">RequestContext</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="n">Guid</span> <span class="n">RequestId</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="p">}</span> <span class="p">=</span> <span class="n">Guid</span><span class="p">.</span><span class="nf">NewGuid</span><span class="p">();</span>
    <span class="k">public</span> <span class="kt">string</span><span class="p">?</span> <span class="n">UserName</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">private</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span>
    <span class="k">public</span> <span class="k">void</span> <span class="nf">SetUser</span><span class="p">(</span><span class="kt">string</span> <span class="n">userName</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="n">UserName</span> <span class="p">=</span> <span class="n">userName</span><span class="p">;</span>
<span class="p">}</span>

<span class="n">services</span><span class="p">.</span><span class="n">AddScoped</span><span class="p">&lt;</span><span class="n">RequestContext</span><span class="p">&gt;();</span>

<span class="kt">var</span> <span class="n">provider</span> <span class="p">=</span> <span class="n">services</span><span class="p">.</span><span class="nf">BuildServiceProvider</span><span class="p">();</span>

<span class="c1">// Risoluzione dal root provider, senza scope</span>
<span class="kt">var</span> <span class="n">request1</span> <span class="p">=</span> <span class="n">provider</span><span class="p">.</span><span class="n">GetRequiredService</span><span class="p">&lt;</span><span class="n">RequestContext</span><span class="p">&gt;();</span>
<span class="n">request1</span><span class="p">.</span><span class="nf">SetUser</span><span class="p">(</span><span class="s">"alice"</span><span class="p">);</span>

<span class="kt">var</span> <span class="n">request2</span> <span class="p">=</span> <span class="n">provider</span><span class="p">.</span><span class="n">GetRequiredService</span><span class="p">&lt;</span><span class="n">RequestContext</span><span class="p">&gt;();</span>
<span class="n">request2</span><span class="p">.</span><span class="nf">SetUser</span><span class="p">(</span><span class="s">"bob"</span><span class="p">);</span>

<span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="nf">ReferenceEquals</span><span class="p">(</span><span class="n">request1</span><span class="p">,</span> <span class="n">request2</span><span class="p">));</span> <span class="c1">// true</span>
<span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="n">request1</span><span class="p">.</span><span class="n">UserName</span><span class="p">);</span> <span class="c1">// bob</span>
<span class="c1">// request1 e request2 sono la stessa istanza:</span>
<span class="c1">// stato condiviso accidentalmente tra "request" diverse</span>
</code></pre></div></div>

<p>In ASP.NET Core questo problema non si presenta per le request HTTP, perché il framework crea automaticamente uno scope per ogni request. Il rischio esiste nei <strong>background service</strong>, nelle <strong>console app</strong> e in qualsiasi contesto dove lo scope non viene creato automaticamente.</p>

<h3 id="la-soluzione">La soluzione</h3>

<p>Creare sempre uno scope esplicito quando si risolvono servizi Scoped fuori da ASP.NET Core:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">provider</span> <span class="p">=</span> <span class="n">services</span><span class="p">.</span><span class="nf">BuildServiceProvider</span><span class="p">();</span>

<span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="p">=</span> <span class="m">1</span><span class="p">;</span> <span class="n">i</span> <span class="p">&lt;=</span> <span class="m">3</span><span class="p">;</span> <span class="n">i</span><span class="p">++)</span>
<span class="p">{</span>
    <span class="k">using</span> <span class="nn">var</span> <span class="n">scope</span> <span class="p">=</span> <span class="n">provider</span><span class="p">.</span><span class="nf">CreateScope</span><span class="p">();</span>
    <span class="kt">var</span> <span class="n">request</span> <span class="p">=</span> <span class="n">scope</span><span class="p">.</span><span class="n">ServiceProvider</span><span class="p">.</span><span class="n">GetRequiredService</span><span class="p">&lt;</span><span class="n">RequestContext</span><span class="p">&gt;();</span>
    <span class="n">request</span><span class="p">.</span><span class="nf">SetUser</span><span class="p">(</span><span class="s">$"user-</span><span class="p">{</span><span class="n">i</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>
    <span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="s">$"Scope </span><span class="p">{</span><span class="n">i</span><span class="p">}</span><span class="s">: </span><span class="p">{</span><span class="n">request</span><span class="p">.</span><span class="n">RequestId</span><span class="p">}</span><span class="s"> - </span><span class="p">{</span><span class="n">request</span><span class="p">.</span><span class="n">UserName</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>
<span class="p">}</span>
<span class="c1">// Ogni scope ha la propria istanza di RequestContext</span>
</code></pre></div></div>

<p>Anche in questo caso, <code class="language-plaintext highlighter-rouge">ValidateScopes</code> rileva il problema. Se abilitato, risolvere un servizio Scoped dal root provider lancia un’eccezione:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Cannot resolve scoped service 'RequestContext' from root provider.
</code></pre></div></div>

<h2 id="conclusione">Conclusione</h2>

<p>I tre anti-pattern di questo articolo hanno un denominatore comune: un disallineamento tra il lifetime dichiarato e quello effettivo del servizio. La Captive Dependency cattura un servizio Scoped in un Singleton, i Transient Disposable si accumulano senza essere rilasciati, e un servizio Scoped risolto dal root provider diventa un Singleton accidentale.</p>

<p>Le contromisure sono semplici: usare <code class="language-plaintext highlighter-rouge">IServiceScopeFactory</code> quando un Singleton ha bisogno di servizi Scoped, gestire esplicitamente gli scope per i Transient Disposable, e abilitare <code class="language-plaintext highlighter-rouge">ValidateScopes</code> e <code class="language-plaintext highlighter-rouge">ValidateOnBuild</code> per intercettare questi problemi prima che raggiungano la produzione.</p>

<p>Nel <a href="/di-service-locator-factory-registrazioni/">prossimo articolo</a> vedremo il Service Locator pattern, i deadlock nelle factory asincrone e il comportamento delle registrazioni multiple.</p>

<blockquote>
  <p><strong>Fonti:</strong></p>
  <ul>
    <li><a href="https://learn.microsoft.com/dotnet/core/extensions/dependency-injection-guidelines#example-anti-patterns">Microsoft Learn — Dependency injection guidelines: anti-patterns</a></li>
    <li><a href="https://learn.microsoft.com/dotnet/core/extensions/dependency-injection-guidelines#idisposable-guidance-for-transient-and-shared-instances">Microsoft Learn — IDisposable guidance for transient and shared instances</a></li>
    <li><a href="https://blog.ploeh.dk/2014/06/02/captive-dependency">Mark Seemann — Captive Dependency</a></li>
  </ul>
</blockquote>]]></content><author><name>Alessandro Mengoli</name></author><category term=".NET" /><category term="Best Practices" /><category term="dotnet" /><category term="dependency-injection" /><category term="lifetime" /><category term="best-practices" /><category term="antipattern" /><summary type="html"><![CDATA[Captive Dependency, Transient Disposable catturati dal container e servizi Scoped che diventano Singleton: tre anti-pattern legati ai lifetime e come evitarli.]]></summary></entry></feed>