<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Isaac Ojeda</title>
    <description>The latest articles on DEV Community by Isaac Ojeda (@isaacojeda).</description>
    <link>https://dev.to/isaacojeda</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F566499%2F40a29082-f0cc-45c9-8411-de7b1e987f47.png</url>
      <title>DEV Community: Isaac Ojeda</title>
      <link>https://dev.to/isaacojeda</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/isaacojeda"/>
    <language>en</language>
    <item>
      <title>EF Core: La explosión cartesiana — Hiciste todo bien y aun así el query es un desastre</title>
      <dc:creator>Isaac Ojeda</dc:creator>
      <pubDate>Wed, 13 May 2026 00:16:55 +0000</pubDate>
      <link>https://dev.to/isaacojeda/ef-core-la-explosion-cartesiana-hiciste-todo-bien-y-aun-asi-el-query-es-un-desastre-22do</link>
      <guid>https://dev.to/isaacojeda/ef-core-la-explosion-cartesiana-hiciste-todo-bien-y-aun-asi-el-query-es-un-desastre-22do</guid>
      <description>&lt;p&gt;En el &lt;a href="https://dev.to/isaacojeda/ef-core-tu-query-funciona-tus-pruebas-pasan-y-estas-leyendo-50000-filas-para-devolver-3-1c9e"&gt;artículo anterior&lt;/a&gt; vimos el problema N+1: queries dentro de loops que se multiplican con los datos. La solución que aprendiste fue usar &lt;code&gt;Include&lt;/code&gt; para cargar las relaciones en una sola query.&lt;/p&gt;

&lt;p&gt;Eso es correcto. Hasta que tienes más de una colección relacionada en el mismo nivel.&lt;/p&gt;




&lt;h2&gt;
  
  
  El escenario: un Include razonable que se vuelve un problema
&lt;/h2&gt;

&lt;p&gt;Tienes pedidos. Cada pedido tiene un cliente, una lista de productos y una lista de pagos. Quieres cargar todo en una sola operación para evitar el N+1:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;pedidos&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pedidos&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Include&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cliente&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Include&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Productos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Include&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pagos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FechaCreacion&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;hace30Dias&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tres &lt;code&gt;Include&lt;/code&gt;. Se ve limpio, se ve correcto. EF Core lo acepta sin quejarse.&lt;/p&gt;

&lt;p&gt;Pero el SQL que genera no es lo que imaginas.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lo que EF Core hace por debajo
&lt;/h2&gt;

&lt;p&gt;El problema aparece específicamente cuando incluyes múltiples colecciones “hermanas” en el mismo nivel del grafo de navegación. Es importante distinguirlo de &lt;code&gt;ThenInclude&lt;/code&gt;, que normalmente no genera este problema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Caso problemático: dos colecciones en el mismo nivel&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Include&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Productos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Include&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pagos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Normalmente no problemático: navegación en profundidad&lt;/span&gt;
&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Include&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Productos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ThenInclude&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pr&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Categoria&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cuando EF Core genera un &lt;code&gt;JOIN&lt;/code&gt; para cada colección hermana, el resultado no produce una fila por pedido — produce una fila por cada &lt;strong&gt;combinación posible&lt;/strong&gt; entre los registros relacionados.&lt;/p&gt;

&lt;p&gt;Si un pedido tiene 5 productos y 3 pagos, el resultado del &lt;code&gt;JOIN&lt;/code&gt; tiene &lt;strong&gt;15 filas&lt;/strong&gt; para ese pedido. EF Core las lee todas y reconstruye el objeto en memoria, pero la base de datos procesó y transfirió 15 filas donde conceptualmente había 1.&lt;/p&gt;

&lt;p&gt;El crecimiento es cartesiano: cada colección multiplica las filas del resultado. Con 100 pedidos, cada uno con 10 productos y 5 pagos, el resultado no son 100 filas — son &lt;strong&gt;5,000 filas&lt;/strong&gt; que viajan de la base de datos a tu servidor para que EF Core las reduzca de vuelta a 100 objetos.&lt;/p&gt;

&lt;p&gt;Eso es la explosión cartesiana.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cómo detectarlo
&lt;/h2&gt;

&lt;h3&gt;
  
  
  El warning de EF Core
&lt;/h3&gt;

&lt;p&gt;Cuando EF Core detecta este patrón, emite un warning en los logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Compiling a query which loads related collections for more than
one collection navigation, either via 'Include' or through
projection. Please review the generated SQL and inspect whether
the cartesian explosion might negatively impact performance.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Si ves este mensaje en tus logs y lo ignoraste, es probable que ya tengas este problema en alguna query.&lt;/p&gt;

&lt;h3&gt;
  
  
  Los tiempos que no tienen proporción
&lt;/h3&gt;

&lt;p&gt;Con los logs habilitados:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddDbContext&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AppDbContext&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseSqlServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connectionString&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LogLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Information&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verás algo así:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;Executed&lt;/span&gt; &lt;span class="n"&gt;DbCommand&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;847&lt;/span&gt;&lt;span class="n"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;Parameters&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p0&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'2025-02-14'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;CommandType&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'Text'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CommandTimeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'30'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;Pedidos&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;Clientes&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;PedidoProductos&lt;/span&gt; &lt;span class="n"&gt;pr&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;Pagos&lt;/span&gt; &lt;span class="n"&gt;pa&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FechaCreacion&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Una sola query, pero 847ms. Con pocos datos en dev tal vez son 12ms y nadie lo cuestiona. Con datos reales de producción el tiempo empieza a crecer de forma que no tiene proporción con el número de registros que devuelve el endpoint.&lt;/p&gt;

&lt;p&gt;A diferencia del N+1, aquí solo hay &lt;strong&gt;una query&lt;/strong&gt;. Si solo cuentas queries, todo parece correcto. Lo que tienes que mirar es cuántas filas devuelve esa query.&lt;/p&gt;




&lt;h2&gt;
  
  
  La solución: AsSplitQuery
&lt;/h2&gt;

&lt;p&gt;EF Core 5 introdujo &lt;code&gt;AsSplitQuery&lt;/code&gt; precisamente para este caso. En lugar de un solo &lt;code&gt;JOIN&lt;/code&gt; que produce el producto cartesiano, EF Core ejecuta una query separada por cada &lt;code&gt;Include&lt;/code&gt; y ensambla los resultados en memoria:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;pedidos&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pedidos&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Include&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cliente&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Include&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Productos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Include&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pagos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FechaCreacion&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;hace30Dias&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsSplitQuery&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Las queries que se ejecutan ahora:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Query 1: los pedidos con el cliente&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FechaCreacion&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Nombre&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;Pedidos&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;Clientes&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClienteId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FechaCreacion&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2025-02-14'&lt;/span&gt;

&lt;span class="c1"&gt;-- Query 2: los productos de esos pedidos&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Nombre&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Precio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PedidoId&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;PedidoProductos&lt;/span&gt; &lt;span class="n"&gt;pr&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PedidoId&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt;

&lt;span class="c1"&gt;-- Query 3: los pagos de esos pedidos&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Monto&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FechaPago&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PedidoId&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;Pagos&lt;/span&gt; &lt;span class="n"&gt;pa&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PedidoId&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tres queries en lugar de una, pero cada una devuelve exactamente las filas que necesita. Sin producto cartesiano, sin filas duplicadas.&lt;/p&gt;




&lt;h2&gt;
  
  
  AsSplitQuery no es siempre la respuesta
&lt;/h2&gt;

&lt;p&gt;Vale la pena entender cuándo usarlo y cuándo no:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Úsalo cuando:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tienes múltiples &lt;code&gt;Include&lt;/code&gt; de colecciones hermanas&lt;/li&gt;
&lt;li&gt;Los tiempos de query son desproporcionados respecto al número de registros que devuelves&lt;/li&gt;
&lt;li&gt;El warning de EF Core aparece en tus logs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;No lo uses cuando:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Solo tienes un &lt;code&gt;Include&lt;/code&gt; — el producto cartesiano no ocurre con una sola colección&lt;/li&gt;
&lt;li&gt;Necesitas consistencia transaccional estricta — las queries de &lt;code&gt;AsSplitQuery&lt;/code&gt; se ejecutan por separado y en teoría otro proceso podría modificar datos entre una y otra&lt;/li&gt;
&lt;li&gt;El conjunto de datos es pequeño — el overhead de múltiples queries puede ser mayor que el beneficio&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Una advertencia sobre paginación:&lt;/strong&gt; si usas &lt;code&gt;AsSplitQuery&lt;/code&gt; junto con &lt;code&gt;Skip&lt;/code&gt;/&lt;code&gt;Take&lt;/code&gt;, asegúrate de tener un &lt;code&gt;OrderBy&lt;/code&gt; estable y con un campo único. Sin eso, los resultados entre las queries separadas pueden ser inconsistentes.&lt;/p&gt;




&lt;h2&gt;
  
  
  El Include que no hace nada
&lt;/h2&gt;

&lt;p&gt;Antes de cerrar, vale la pena mencionar un hábito relacionado que ocurre con frecuencia.&lt;/p&gt;

&lt;p&gt;Muchos developers agregan &lt;code&gt;Include&lt;/code&gt; de forma defensiva — para asegurarse de que las propiedades de navegación no sean null. Tiene sentido cuando materializas la entidad completa. Pero cuando proyectas a un DTO con &lt;code&gt;Select&lt;/code&gt;, EF Core ignora completamente los &lt;code&gt;Include&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Los Include son ignorados — EF Core no materializa Pedido&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;pedidos&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pedidos&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Include&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cliente&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;// ignorado&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Include&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Productos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="c1"&gt;// ignorado&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Include&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pagos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="c1"&gt;// ignorado&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FechaCreacion&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;hace30Dias&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;PedidoDetalleDto&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Cliente&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cliente&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Nombre&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Productos&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Productos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pr&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Nombre&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ToList&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;TotalPagado&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pagos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pa&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Monto&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;EF Core resuelve los JOINs necesarios directamente desde la proyección del &lt;code&gt;Select&lt;/code&gt;. Los &lt;code&gt;Include&lt;/code&gt; no aportan nada — ni errores, ni beneficios, ni SQL adicional. Lo mismo aplica para &lt;code&gt;AsSplitQuery&lt;/code&gt;: si proyectas a un DTO, no hay entidades que materializar, así que tampoco tiene efecto.&lt;/p&gt;

&lt;p&gt;El código funciona igual con o sin ellos. El problema es que quien lo lee después asume que son necesarios, y esa confusión se acumula.&lt;/p&gt;




&lt;h2&gt;
  
  
  La proyección con Select como alternativa
&lt;/h2&gt;

&lt;p&gt;Cuando no necesitas materializar la entidad completa, la proyección con &lt;code&gt;Select&lt;/code&gt; puede ser más eficiente que &lt;code&gt;AsSplitQuery&lt;/code&gt;. En muchos casos permite a EF Core generar SQL mucho más eficiente y evitar la materialización completa de relaciones:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✅ Sin Include, sin AsSplitQuery&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;pedidos&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pedidos&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FechaCreacion&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;hace30Dias&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;PedidoDetalleDto&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Cliente&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cliente&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Nombre&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Productos&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Productos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pr&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Nombre&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ToList&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;TotalPagado&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pagos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pa&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;pa&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Monto&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La regla general: usa &lt;code&gt;Include&lt;/code&gt; cuando materialices la entidad. Usa &lt;code&gt;Select&lt;/code&gt; cuando trabajes con DTOs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Resumen
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Problema&lt;/th&gt;
&lt;th&gt;Síntoma&lt;/th&gt;
&lt;th&gt;Solución&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;ToList()&lt;/code&gt; prematuro&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;SELECT *&lt;/code&gt; sin &lt;code&gt;WHERE&lt;/code&gt;, todo en memoria&lt;/td&gt;
&lt;td&gt;Mantener &lt;code&gt;IQueryable&lt;/code&gt; hasta el final&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;SELECT *&lt;/code&gt; silencioso&lt;/td&gt;
&lt;td&gt;Proyección ignorada, columnas de más&lt;/td&gt;
&lt;td&gt;Expresiones traducibles en &lt;code&gt;Select&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;N+1&lt;/td&gt;
&lt;td&gt;Una query por cada registro del loop&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Include&lt;/code&gt; o proyección con &lt;code&gt;Select&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Explosión cartesiana&lt;/td&gt;
&lt;td&gt;Una query lenta con filas multiplicadas&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;AsSplitQuery&lt;/code&gt; o proyección con &lt;code&gt;Select&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Si no ves el SQL que EF Core genera, no sabes lo que está pasando. Los logs son la herramienta más simple y más ignorada para detectar estos problemas antes de que lleguen a producción.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;¿Has tenido que resolver una explosión cartesiana en producción? ¿Cómo lo detectaste? Cuéntame en los comentarios.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  ¿Qué sigue?
&lt;/h2&gt;

&lt;p&gt;En el próximo artículo vamos a hablar de algo que EF Core hace en todas tus consultas sin que lo hayas pedido: rastrear cada entidad que lees para detectar cambios. En pantallas de solo lectura estás pagando ese costo en memoria y CPU sin obtener nada a cambio — y con suficientes datos, se nota.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>efcore</category>
    </item>
    <item>
      <title>EF Core: N+1 — Una query en dev, mil queries en producción</title>
      <dc:creator>Isaac Ojeda</dc:creator>
      <pubDate>Fri, 08 May 2026 18:33:09 +0000</pubDate>
      <link>https://dev.to/isaacojeda/ef-core-n1-una-query-en-dev-mil-queries-en-produccion-2kg4</link>
      <guid>https://dev.to/isaacojeda/ef-core-n1-una-query-en-dev-mil-queries-en-produccion-2kg4</guid>
      <description>&lt;p&gt;En el &lt;a href="https://dev.to/isaacojeda/ef-core-tu-query-funciona-tus-pruebas-pasan-y-estas-leyendo-50000-filas-para-devolver-3-1c9e"&gt;artículo anterior&lt;/a&gt; vimos cómo EF Core puede traer más datos de los necesarios sin avisar: un &lt;code&gt;ToList()&lt;/code&gt; prematuro que consulta toda la tabla, o un &lt;code&gt;Select&lt;/code&gt; con expresiones no traducibles que termina haciendo &lt;code&gt;SELECT *&lt;/code&gt; en silencio.&lt;/p&gt;

&lt;p&gt;Este problema es diferente. No es que una query traiga demasiado, es que una operación que debería ser una sola query termina convirtiéndose en cientos o miles.&lt;/p&gt;

&lt;p&gt;Y lo más difícil: en desarrollo nadie lo nota.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Por qué es el más traicionero de los tres&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;El &lt;code&gt;ToList()&lt;/code&gt; prematuro y el &lt;code&gt;SELECT *&lt;/code&gt; silencioso tienen algo en común: el daño es proporcional al tamaño de la tabla. Con pocos datos, el impacto es menor, pero el problema ya existe desde el primer día.&lt;/p&gt;

&lt;p&gt;El N+1 es diferente.&lt;/p&gt;

&lt;p&gt;Con 10 pedidos en la base de datos, hace 11 queries. Nadie lo siente. Con 10,000 pedidos, hace 10,001 queries por cada carga de pantalla. La base de datos empieza a ahogarse, los tiempos se disparan y el equipo no entiende qué cambió — porque el código lleva meses igual.&lt;/p&gt;

&lt;p&gt;Es un problema que crece silenciosamente junto con el negocio.&lt;/p&gt;

&lt;p&gt;Y el verdadero costo no es solo “más SQL”. Cada query implica:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;un roundtrip a la base de datos&lt;/li&gt;
&lt;li&gt;parsing y compilación del comando&lt;/li&gt;
&lt;li&gt;ejecución&lt;/li&gt;
&lt;li&gt;transferencia de datos&lt;/li&gt;
&lt;li&gt;sincronización de conexiones&lt;/li&gt;
&lt;li&gt;presión sobre el connection pool&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Mil queries pequeñas suelen ser muchísimo más costosas que una sola query grande.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Cómo ocurre en EF Core&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Si vienes de EF6 o de otros ORMs con lazy loading habilitado por default, el N+1 clásico era acceder a una propiedad de navegación dentro de un loop y que el ORM disparara una query automáticamente por debajo, sin que el código lo hiciera explícito.&lt;/p&gt;

&lt;p&gt;En EF Core el lazy loading está deshabilitado por default. Si intentas acceder a una propiedad de navegación que no fue cargada, obtienes &lt;code&gt;null&lt;/code&gt;, no una query silenciosa.&lt;/p&gt;

&lt;p&gt;Eso es mejor, pero el problema sigue ocurriendo — ahora de forma explícita.&lt;/p&gt;

&lt;p&gt;El N+1 en EF Core se ve así:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;pedidos&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pedidos&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FechaCreacion&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;hace30Dias&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Query 1: trae los pedidos&lt;/span&gt;

&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;pedido&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;pedidos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Query 2..N: una por cada pedido&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;cliente&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Clientes&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FirstOrDefaultAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;pedido&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClienteId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cliente&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Nombre&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; - &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pedido&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Total&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El SQL que se ejecuta:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Query 1&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClienteId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FechaCreacion&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;Pedidos&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FechaCreacion&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2025-02-14'&lt;/span&gt;

&lt;span class="c1"&gt;-- Query 2&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;TOP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;Clientes&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

&lt;span class="c1"&gt;-- Query 3&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;TOP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;Clientes&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;

&lt;span class="c1"&gt;-- ... y así por cada pedido en el resultado&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Con 500 pedidos en el rango de fechas: 501 queries.&lt;/p&gt;

&lt;p&gt;Con 5,000 pedidos: 5,001.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;La variante que se escapa en code review&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;El ejemplo anterior es fácil de detectar porque la query adicional es visible dentro del loop.&lt;/p&gt;

&lt;p&gt;Pero en proyectos reales el problema suele esconderse detrás de servicios aparentemente inocentes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;pedidos&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pedidos&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FechaCreacion&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;hace30Dias&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;pedido&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;pedidos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Parece una llamada inocente a un servicio&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;dto&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_pedidoService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EnriquecerAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pedido&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;resultado&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dto&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Y dentro del servicio:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PedidoDto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;EnriquecerAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Pedido&lt;/span&gt; &lt;span class="n"&gt;pedido&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;cliente&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Clientes&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FirstOrDefaultAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;pedido&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClienteId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;ultimaFactura&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Facturas&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClienteId&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;pedido&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClienteId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OrderByDescending&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FechaEmision&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FirstOrDefaultAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;PedidoDto&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Cliente&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cliente&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Nombre&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pedido&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;UltimaFactura&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ultimaFactura&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Folio&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Aquí el loop ejecuta &lt;strong&gt;2 queries por pedido&lt;/strong&gt; en lugar de 1.&lt;/p&gt;

&lt;p&gt;Con 500 pedidos: 1,001 queries.&lt;/p&gt;

&lt;p&gt;Y el problema es mucho más difícil de detectar porque el &lt;code&gt;foreach&lt;/code&gt; se ve completamente razonable. El costo real está escondido en otro archivo, detrás de capas de abstracción.&lt;/p&gt;

&lt;p&gt;Este tipo de N+1 suele sobrevivir code reviews durante meses.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Cómo detectarlo&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;La misma herramienta del artículo anterior: los logs de EF Core.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddDbContext&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AppDbContext&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseSqlServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connectionString&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LogLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Information&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Con el N+1 activo, verás algo así en la consola:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Executed DbCommand (2ms) SELECT ... FROM Pedidos WHERE ...
Executed DbCommand (1ms) SELECT TOP(1) ... FROM Clientes WHERE Id = 1
Executed DbCommand (1ms) SELECT TOP(1) ... FROM Clientes WHERE Id = 2
Executed DbCommand (1ms) SELECT TOP(1) ... FROM Clientes WHERE Id = 3
Executed DbCommand (1ms) SELECT TOP(1) ... FROM Clientes WHERE Id = 4
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Si ves el mismo patrón de query repitiéndose con diferentes parámetros, tienes un N+1.&lt;/p&gt;

&lt;p&gt;Una señal todavía más rápida:&lt;/p&gt;

&lt;p&gt;si el número de queries crece proporcionalmente al número de registros que devuelve la primera query, casi siempre es N+1.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Las soluciones&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;&lt;code&gt;Include&lt;/code&gt;&lt;/strong&gt; &lt;strong&gt;— cuando necesitas la entidad completa&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;pedidos&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pedidos&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Include&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cliente&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FechaCreacion&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;hace30Dias&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;pedido&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;pedidos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Cliente ya está cargado, sin queries adicionales&lt;/span&gt;
    &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pedido&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cliente&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Nombre&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; - &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pedido&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Total&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;EF Core genera un &lt;code&gt;JOIN&lt;/code&gt; y trae todo en una sola query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FechaCreacion&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Nombre&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RFC&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;Pedidos&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;Clientes&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClienteId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FechaCreacion&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2025-02-14'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Esto resuelve el N+1 porque elimina las queries repetidas.&lt;/p&gt;

&lt;p&gt;Funciona bien cuando realmente necesitas la entidad &lt;code&gt;Cliente&lt;/code&gt; completa. Si solo necesitas el nombre, estás trayendo columnas de más.&lt;/p&gt;

&lt;p&gt;Y cuidado: usar múltiples &lt;code&gt;Include&lt;/code&gt; sobre colecciones puede llevar a otro problema distinto — explosión cartesiana. Lo veremos en el siguiente artículo.&lt;/p&gt;




&lt;h3&gt;
  
  
  &lt;strong&gt;Proyección con&lt;/strong&gt; &lt;strong&gt;&lt;code&gt;Select&lt;/code&gt;&lt;/strong&gt; &lt;strong&gt;— la más eficiente para DTOs&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;pedidos&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pedidos&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FechaCreacion&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;hace30Dias&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;PedidoResumenDto&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Cliente&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cliente&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Nombre&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Total&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;EF Core resuelve el &lt;code&gt;JOIN&lt;/code&gt; automáticamente a partir de la proyección, sin necesidad de &lt;code&gt;Include&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;El SQL resultante solo trae las columnas necesarias:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Nombre&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Total&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;Pedidos&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;Clientes&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClienteId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FechaCreacion&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2025-02-14'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Esta suele ser la mejor solución cuando trabajas con DTOs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;una sola query&lt;/li&gt;
&lt;li&gt;solo las columnas necesarias&lt;/li&gt;
&lt;li&gt;sin entidades adicionales en memoria&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  &lt;strong&gt;Cuando el enriquecimiento es inevitable&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;A veces el servicio que enriquece los datos tiene lógica compleja que no puedes mover fácilmente a una proyección SQL.&lt;/p&gt;

&lt;p&gt;En ese caso, la alternativa es cargar todo lo necesario antes del loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;pedidos&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pedidos&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FechaCreacion&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;hace30Dias&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;clienteIds&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pedidos&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClienteId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Distinct&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Una sola query para todos los clientes necesarios&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;clientes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Clientes&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;clienteIds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToDictionaryAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;pedido&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;pedidos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;cliente&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;clientes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pedido&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClienteId&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cliente&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Nombre&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; - &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pedido&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Total&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ahora el número de queries es constante:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1 query para pedidos&lt;/li&gt;
&lt;li&gt;1 query para clientes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No importa si tienes 10 pedidos o 100,000.&lt;/p&gt;

&lt;p&gt;En lugar de N queries de un registro cada una, ahora son 2 queries en total.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Resumen&lt;/strong&gt;
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Situación&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Queries ejecutadas&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Solución&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Query en loop, entidad relacionada&lt;/td&gt;
&lt;td&gt;1 + N&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Include&lt;/code&gt; o proyección con &lt;code&gt;Select&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Servicio con queries dentro del loop&lt;/td&gt;
&lt;td&gt;1 + (N × M)&lt;/td&gt;
&lt;td&gt;Cargar datos necesarios antes del loop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Proyección con &lt;code&gt;Select&lt;/code&gt; y JOIN&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;✅ Ya es correcto&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;La regla general:&lt;/p&gt;

&lt;p&gt;Todo lo que necesitas para procesar una lista debería estar cargado antes de iterar sobre ella.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;¿Qué sigue?&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Hasta ahora vimos tres problemas distintos:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;traer más registros de los necesarios&lt;/li&gt;
&lt;li&gt;traer más columnas de las necesarias&lt;/li&gt;
&lt;li&gt;multiplicar queries accidentalmente&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;El siguiente problema es el opuesto al N+1:&lt;/p&gt;

&lt;p&gt;hacer una sola query… pero generar muchas más filas de las esperadas.&lt;/p&gt;

&lt;p&gt;Cuando abusas de &lt;code&gt;Include&lt;/code&gt;, EF Core puede terminar produciendo joins gigantescos que duplican datos masivamente — la explosión cartesiana.&lt;/p&gt;

&lt;p&gt;Y sí, EF Core incluso tiene herramientas como &lt;code&gt;AsSplitQuery()&lt;/code&gt; para intentar balancear ambos extremos.&lt;/p&gt;

&lt;p&gt;Lo veremos en el siguiente artículo.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;¿Has encontrado un N+1 en producción? ¿Cómo lo detectaste? Cuéntame en los comentarios.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>efcore</category>
      <category>dotnet</category>
      <category>csharp</category>
      <category>performance</category>
    </item>
    <item>
      <title>Ports &amp; Adapters: cómo aislar tu núcleo de todo lo que puede cambiar</title>
      <dc:creator>Isaac Ojeda</dc:creator>
      <pubDate>Wed, 29 Apr 2026 14:35:11 +0000</pubDate>
      <link>https://dev.to/isaacojeda/ports-adapters-como-aislar-tu-nucleo-de-todo-lo-que-puede-cambiar-51jl</link>
      <guid>https://dev.to/isaacojeda/ports-adapters-como-aislar-tu-nucleo-de-todo-lo-que-puede-cambiar-51jl</guid>
      <description>&lt;h1&gt;
  
  
  Ports &amp;amp; Adapters: cómo aislar tu núcleo de todo lo que puede cambiar
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"El núcleo de tu aplicación no debería saber si el correo lo manda SendGrid, Mailgun o un servidor SMTP de Docker levantado a las 2am."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Introducción
&lt;/h2&gt;

&lt;p&gt;Cuando empezamos un proyecto nuevo, el instinto natural es conectar todo rápido: llamamos a SendGrid directamente desde el servicio de negocio, usamos &lt;code&gt;HttpClient&lt;/code&gt; con la URL hardcodeada, leemos la configuración en el lugar menos pensado. Funciona. Por un rato.&lt;/p&gt;

&lt;p&gt;El problema aparece cuando necesitas correr las pruebas sin mandar correos reales, o cuando el cliente cambia de proveedor de email a mitad del proyecto, o cuando en desarrollo no tienes acceso al servidor SMTP de producción. De repente, lo que "funcionaba" se convierte en un obstáculo.&lt;/p&gt;

&lt;p&gt;El patrón &lt;strong&gt;Ports &amp;amp; Adapters&lt;/strong&gt;, introducido por Alistair Cockburn bajo el nombre de &lt;em&gt;Arquitectura Hexagonal&lt;/em&gt;, propone una idea elegante y poderosa: &lt;strong&gt;el núcleo de tu aplicación define contratos (puertos), y las implementaciones concretas (adaptadores) se enchufan desde afuera&lt;/strong&gt;. El core no sabe —ni le importa— quién está del otro lado.&lt;/p&gt;

&lt;p&gt;Este artículo nace de mi experiencia trabajando con este patrón en proyectos reales con .NET. No como ejercicio académico, sino como herramienta de trabajo que ha simplificado enormemente el mantenimiento, las pruebas y los despliegues en múltiples ambientes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Desarrollo
&lt;/h2&gt;

&lt;h3&gt;
  
  
  El problema concreto: el correo electrónico
&lt;/h3&gt;

&lt;p&gt;Tomemos uno de los casos más comunes: &lt;strong&gt;enviar correos electrónicos&lt;/strong&gt;. En un proyecto típico manejamos al menos tres ambientes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Ambiente&lt;/th&gt;
&lt;th&gt;Proveedor de email&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Desarrollo&lt;/td&gt;
&lt;td&gt;Servidor SMTP local en Docker (ej. Mailpit)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;QA / Staging&lt;/td&gt;
&lt;td&gt;Servidor SMTP interno de la empresa&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Producción&lt;/td&gt;
&lt;td&gt;SaaS como SendGrid, con tracking y analytics&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Si el código de negocio sabe con quién está hablando, tienes un problema: cambiar de proveedor implica tocar el núcleo. Eso va en contra de uno de los principios más valiosos en diseño de software: &lt;strong&gt;aislar lo que cambia de lo que no cambia&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  El port: define el contrato, no la implementación
&lt;/h3&gt;

&lt;p&gt;Un &lt;strong&gt;port&lt;/strong&gt; es simplemente una interfaz. Dice &lt;em&gt;qué&lt;/em&gt; se puede hacer, sin decir &lt;em&gt;cómo&lt;/em&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IEmailSender&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;SendAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EmailMessage&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;EmailMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;To&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;HtmlBody&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;PlainTextBody&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tu lógica de negocio depende únicamente de &lt;code&gt;IEmailSender&lt;/code&gt;. No sabe nada de SMTP, ni de SendGrid, ni de credenciales. Solo conoce el contrato.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserRegistrationService&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;IEmailSender&lt;/span&gt; &lt;span class="n"&gt;_emailSender&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;UserRegistrationService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IEmailSender&lt;/span&gt; &lt;span class="n"&gt;emailSender&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_emailSender&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;emailSender&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;RegisterAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RegisterUserCommand&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// ... lógica de registro ...&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_emailSender&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SendAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;EmailMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;To&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"¡Bienvenido!"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;HtmlBody&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$"&amp;lt;h1&amp;gt;Hola, &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;/h1&amp;gt;&amp;lt;p&amp;gt;Tu cuenta ha sido creada.&amp;lt;/p&amp;gt;"&lt;/span&gt;
        &lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El núcleo queda limpio. Nada de dependencias externas, nada que cambie cuando cambias de proveedor.&lt;/p&gt;

&lt;h3&gt;
  
  
  Los adapters: implementaciones intercambiables
&lt;/h3&gt;

&lt;p&gt;Cada ambiente tiene su propio &lt;strong&gt;adapter&lt;/strong&gt;, que implementa el mismo port.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Adapter SMTP&lt;/strong&gt; (para desarrollo con Docker):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SmtpEmailSender&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IEmailSender&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;SmtpOptions&lt;/span&gt; &lt;span class="n"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;SmtpEmailSender&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SmtpOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_options&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;SendAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EmailMessage&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SmtpClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ConnectAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SecureSocketOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;mail&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;MimeMessage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;mail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;To&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MailboxAddress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;To&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="n"&gt;mail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Subject&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Subject&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;mail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;TextPart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TextFormat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HtmlBody&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SendAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DisconnectAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Adapter SendGrid&lt;/strong&gt; (para producción):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SendGridEmailSender&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IEmailSender&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;SendGridClient&lt;/span&gt; &lt;span class="n"&gt;_client&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;SendGridOptions&lt;/span&gt; &lt;span class="n"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;SendGridEmailSender&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SendGridOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_options&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;_client&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SendGridClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ApiKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;SendAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EmailMessage&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MailHelper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateSingleEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;EmailAddress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FromEmail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FromName&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;EmailAddress&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;To&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;plainTextContent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PlainTextBody&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;htmlContent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HtmlBody&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SendEmailAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cada adapter es independiente. Si mañana migras de SendGrid a Mailgun, creas un nuevo adaptador y listo. El núcleo no se toca.&lt;/p&gt;

&lt;h3&gt;
  
  
  El Factory: construir el adapter correcto según la configuración
&lt;/h3&gt;

&lt;p&gt;Aquí es donde el patrón cobra su mayor fuerza en la práctica. En lugar de registrar manualmente el adaptador en &lt;code&gt;Program.cs&lt;/code&gt; y cambiarlo por ambiente, uso un &lt;strong&gt;factory&lt;/strong&gt; que lee la configuración y devuelve la implementación adecuada.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Configuración en &lt;code&gt;appsettings.json&lt;/code&gt;:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Provider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SendGrid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"SendGrid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"ApiKey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SG.xxx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"FromEmail"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"noreply@miapp.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"FromName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Mi Aplicación"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Smtp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Host"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"localhost"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Port"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1025&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Factory para el registro:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EmailServiceExtensions&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;IServiceCollection&lt;/span&gt; &lt;span class="nf"&gt;AddEmailSender&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="n"&gt;IServiceCollection&lt;/span&gt; &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;IConfiguration&lt;/span&gt; &lt;span class="n"&gt;configuration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;configuration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Email:Provider"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="s"&gt;"Smtp"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddTransient&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IEmailSender&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;sp&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;provider&lt;/span&gt; &lt;span class="k"&gt;switch&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="s"&gt;"SendGrid"&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SendGridEmailSender&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;sp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetRequiredService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SendGridOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;()),&lt;/span&gt;

                &lt;span class="s"&gt;"Smtp"&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SmtpEmailSender&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;sp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetRequiredService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SmtpOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;()),&lt;/span&gt;

                &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"Proveedor de email no soportado: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;

        &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configure&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SendGridOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetSection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Email:SendGrid"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configure&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SmtpOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetSection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Email:Smtp"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Y en &lt;code&gt;Program.cs&lt;/code&gt;, limpio y simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddEmailSender&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cambiar de proveedor es ahora una línea en el &lt;code&gt;appsettings&lt;/code&gt; del ambiente correspondiente. Sin recompilar, sin tocar el núcleo.&lt;/p&gt;

&lt;h3&gt;
  
  
  El decorador: comportamiento sin contaminar los adaptadores
&lt;/h3&gt;

&lt;p&gt;Un patrón que combina perfectamente con esto es el &lt;strong&gt;Decorator&lt;/strong&gt;. Imagina que quieres agregar logging a todos los envíos de email, independientemente del proveedor:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 Ya he hablado sobre este patrón &lt;a href="https://dev.to/isaacojeda/aspnet-core-y-el-patron-decorador-ampliando-la-funcionalidad-de-tus-apis-5jk"&gt;en este artículo&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LoggingEmailSenderDecorator&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IEmailSender&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;IEmailSender&lt;/span&gt; &lt;span class="n"&gt;_inner&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;LoggingEmailSenderDecorator&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_logger&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;LoggingEmailSenderDecorator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;IEmailSender&lt;/span&gt; &lt;span class="n"&gt;inner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;LoggingEmailSenderDecorator&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_inner&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;inner&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;_logger&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;SendAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EmailMessage&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Enviando correo a {To} con asunto '{Subject}'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;To&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Subject&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;try&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_inner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SendAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;_logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Correo enviado correctamente a {To}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;To&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;_logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Error al enviar correo a {To}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;To&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Para enchufar el decorador, puedes usar &lt;strong&gt;Scrutor&lt;/strong&gt; (librería NuGet de Dependency Injection avanzado) o hacerlo manualmente:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Con Scrutor&lt;/span&gt;
&lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Decorate&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IEmailSender&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LoggingEmailSenderDecorator&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// O manualmente&lt;/span&gt;
&lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddTransient&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IEmailSender&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;sp&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;inner&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="cm"&gt;/* tu factory aquí */&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetRequiredService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;LoggingEmailSenderDecorator&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;LoggingEmailSenderDecorator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 También utilizar Keyed Services del DI es útil cuando queremos tener varios decoradores, en mi artículo mencionado anteriormente hablo sobre ello.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;El logging aplica a cualquier adaptador. Si mañana agregas un adaptador nuevo, el decorador lo cubre automáticamente. Sin duplicar código.&lt;/p&gt;

&lt;h3&gt;
  
  
  El adaptador &lt;code&gt;Null&lt;/code&gt;: tu mejor aliado en pruebas
&lt;/h3&gt;

&lt;p&gt;Para pruebas unitarias o escenarios donde no quieres efectos secundarios reales, el &lt;strong&gt;Null Object Pattern&lt;/strong&gt; aplicado como adaptador es invaluable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NullEmailSender&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IEmailSender&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;SendAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EmailMessage&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CompletedTask&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// No hace nada. Y está bien.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;También puedes tener una versión que almacena los mensajes en memoria para hacer aserciones en las pruebas:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;InMemoryEmailSender&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IEmailSender&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;EmailMessage&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;SentMessages&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;SendAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EmailMessage&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;SentMessages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CompletedTask&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// En tus pruebas&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;emailSender&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InMemoryEmailSender&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;UserRegistrationService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;emailSender&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RegisterAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;RegisterUserCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"test@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Isaac"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Single&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;emailSender&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SentMessages&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"test@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;emailSender&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SentMessages&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;To&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pruebas rápidas, sin dependencias externas, sin configuración extra.&lt;/p&gt;

&lt;h3&gt;
  
  
  Este patrón escala a todo
&lt;/h3&gt;

&lt;p&gt;Este mismo enfoque aplica a cualquier dependencia de infraestructura:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Almacenamiento de archivos&lt;/strong&gt;: &lt;code&gt;IFileStorage&lt;/code&gt; con adaptadores para &lt;code&gt;AzureBlobStorage&lt;/code&gt;, &lt;code&gt;LocalFileSystem&lt;/code&gt;, &lt;code&gt;S3&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Notificaciones push&lt;/strong&gt;: &lt;code&gt;IPushNotificationSender&lt;/code&gt; con adaptadores para Firebase, APNS, etc.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SMS&lt;/strong&gt;: &lt;code&gt;ISmsSender&lt;/code&gt; con Twilio en producción, log en desarrollo&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Caché&lt;/strong&gt;: &lt;code&gt;ICacheProvider&lt;/code&gt; sobre &lt;code&gt;IMemoryCache&lt;/code&gt; o &lt;code&gt;IDistributedCache&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pagos&lt;/strong&gt;: &lt;code&gt;IPaymentGateway&lt;/code&gt; con Stripe, Conekta, o un mock para pruebas&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;El patrón es el mismo: &lt;strong&gt;define el contrato en el núcleo, implementa fuera, conecta con un factory&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusión
&lt;/h2&gt;

&lt;p&gt;Ports &amp;amp; Adapters no es un patrón complicado. Su fuerza está en una idea simple: &lt;strong&gt;el núcleo de tu aplicación debería poder existir sin importarle nada del mundo exterior&lt;/strong&gt;. No le importa si el correo va por SMTP o SendGrid. No le importa si los archivos están en Azure Blob o en disco local. No le importa si estás en desarrollo, QA o producción.&lt;/p&gt;

&lt;p&gt;Lo que me ha resultado más valioso en la práctica es la combinación Factory + Decorador:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;El &lt;strong&gt;Factory&lt;/strong&gt; decide qué adaptador construir según la configuración del ambiente.&lt;/li&gt;
&lt;li&gt;El &lt;strong&gt;Decorador&lt;/strong&gt; agrega comportamiento transversal (logging, reintentos, métricas) sin contaminar ningún adaptador.&lt;/li&gt;
&lt;li&gt;El &lt;strong&gt;Null/InMemory adapter&lt;/strong&gt; hace que las pruebas sean rápidas y deterministas.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cuando alguien en el equipo dice "hay que cambiar de proveedor de email", la respuesta es: &lt;em&gt;creamos un adaptador nuevo, actualizamos el appsetting, listo&lt;/em&gt;. Sin miedo, sin regresiones inesperadas, sin refactors dolorosos.&lt;/p&gt;

&lt;p&gt;Ese es el tipo de arquitectura que te deja dormir tranquilo en producción.&lt;/p&gt;

&lt;h2&gt;
  
  
  Referencias y recursos para aprender más
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Alistair Cockburn — Hexagonal Architecture&lt;/strong&gt; (artículo original, 2005)&lt;br&gt;&lt;br&gt;
&lt;a href="https://alistair.cockburn.us/hexagonal-architecture/" rel="noopener noreferrer"&gt;https://alistair.cockburn.us/hexagonal-architecture/&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Microsoft Docs — Dependency Injection en ASP.NET Core&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection" rel="noopener noreferrer"&gt;https://learn.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Scrutor — Decorator support for Microsoft DI&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://github.com/khellang/Scrutor" rel="noopener noreferrer"&gt;https://github.com/khellang/Scrutor&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;MailKit — Cliente SMTP para .NET&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://github.com/jstedfast/MailKit" rel="noopener noreferrer"&gt;https://github.com/jstedfast/MailKit&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Mailpit — Servidor SMTP para desarrollo con Docker&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://mailpit.axllent.org/" rel="noopener noreferrer"&gt;https://mailpit.axllent.org/&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Mark Seemann — Dependency Injection in .NET&lt;/strong&gt; &lt;em&gt;(libro recomendado)&lt;/em&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://www.manning.com/books/dependency-injection-principles-practices-patterns" rel="noopener noreferrer"&gt;https://www.manning.com/books/dependency-injection-principles-practices-patterns&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Clean Architecture — Robert C. Martin&lt;/strong&gt; &lt;em&gt;(el contexto más amplio de este patrón)&lt;/em&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html" rel="noopener noreferrer"&gt;https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;¿Usas este patrón en tus proyectos? ¿Con qué combinaciones de adaptadores has trabajado? Déjame tu experiencia en los comentarios.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>dotnet</category>
      <category>patterns</category>
    </item>
    <item>
      <title>Cómo manejar fallos transitorios en .NET con Polly y ResiliencePipelineBuilder</title>
      <dc:creator>Isaac Ojeda</dc:creator>
      <pubDate>Mon, 27 Apr 2026 13:30:06 +0000</pubDate>
      <link>https://dev.to/isaacojeda/como-manejar-fallos-transitorios-en-net-con-polly-y-resiliencepipelinebuilder-4fk4</link>
      <guid>https://dev.to/isaacojeda/como-manejar-fallos-transitorios-en-net-con-polly-y-resiliencepipelinebuilder-4fk4</guid>
      <description>&lt;h2&gt;
  
  
  Introducción
&lt;/h2&gt;

&lt;p&gt;Hay una idea que tarde o temprano aparece en cualquier sistema distribuido: no todo error significa que algo esté roto de verdad.&lt;/p&gt;

&lt;p&gt;A veces una API tarda más de lo normal. A veces un servicio está reiniciándose. A veces la red falla durante un instante. Y a veces simplemente pegaste justo en el peor momento posible.&lt;/p&gt;

&lt;p&gt;Ese tipo de errores existen, pasan seguido y, lo más importante, muchas veces &lt;strong&gt;se resuelven solos si reintentas correctamente&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Ahí entra Polly.&lt;/p&gt;

&lt;p&gt;Con Polly v8 cambió bastante la forma de definir resiliencia en .NET. La API ahora gira alrededor de &lt;code&gt;ResiliencePipeline&lt;/code&gt; y &lt;code&gt;ResiliencePipelineBuilder&lt;/code&gt;, que permiten componer estrategias como retry, timeout o circuit breaker de una forma mucho más clara que en versiones anteriores.&lt;/p&gt;

&lt;p&gt;Si ya vienes usando &lt;code&gt;Microsoft.Extensions.Http.Resilience&lt;/code&gt; con &lt;code&gt;HttpClient&lt;/code&gt;, probablemente ya viste una parte de esta idea. Pero Polly no sirve solo para HTTP. También aplica para operaciones como:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;subir archivos a Blob Storage&lt;/li&gt;
&lt;li&gt;consultar una base de datos&lt;/li&gt;
&lt;li&gt;invocar un servicio gRPC&lt;/li&gt;
&lt;li&gt;procesar mensajes de una cola&lt;/li&gt;
&lt;li&gt;ejecutar cualquier operación async que pueda fallar por causas temporales&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;En este artículo vamos a usar un ejemplo simple para mostrar cómo aplicar &lt;code&gt;ResiliencePipelineBuilder&lt;/code&gt;, pero también quiero aprovechar para cubrir mejor el concepto detrás de todo esto: &lt;strong&gt;Retry Pattern&lt;/strong&gt;, &lt;strong&gt;transient fault handling&lt;/strong&gt; y algunos criterios prácticos para no caer en reintentos ciegos.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema real
&lt;/h2&gt;

&lt;p&gt;Cuando estamos empezando, es común tratar todos los errores igual: si falló, falló. Se lanza la excepción, se loguea algo y listo.&lt;/p&gt;

&lt;p&gt;El problema es que en producción eso suele ser demasiado ingenuo.&lt;/p&gt;

&lt;p&gt;Imagina este flujo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Usuario -&amp;gt; Tu app -&amp;gt; Servicio externo -&amp;gt; falla momentáneamente -&amp;gt; error
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ahora imagina este otro:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Usuario -&amp;gt; Tu app -&amp;gt; Servicio externo -&amp;gt; falla momentáneamente -&amp;gt; retry -&amp;gt; éxito
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La diferencia entre ambos no es menor. En el primer caso devuelves un error por algo que tal vez duró 300 milisegundos. En el segundo, absorbiste una falla esperable del entorno y la operación terminó bien.&lt;/p&gt;

&lt;p&gt;Eso es resiliencia: no asumir que el mundo es estable, sino diseñar para que el sistema siga funcionando razonablemente bien cuando aparecen fallos normales.&lt;/p&gt;

&lt;h2&gt;
  
  
  ¿Qué es transient fault handling?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Transient fault handling&lt;/code&gt; es la práctica de detectar errores temporales y responder de forma inteligente, en lugar de tratar esos errores como fallos definitivos.&lt;/p&gt;

&lt;p&gt;No significa "reintentar todo".&lt;/p&gt;

&lt;p&gt;Significa:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;identificar qué errores son temporales&lt;/li&gt;
&lt;li&gt;decidir si vale la pena reintentar&lt;/li&gt;
&lt;li&gt;espaciar esos intentos de forma razonable&lt;/li&gt;
&lt;li&gt;cortar cuando ya no tiene sentido seguir intentando&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Ejemplos comunes de fallos transitorios:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;TimeoutException&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;errores de transporte representados por &lt;code&gt;HttpRequestException&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;respuestas &lt;code&gt;429 Too Many Requests&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;respuestas &lt;code&gt;408 Request Timeout&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;errores &lt;code&gt;5xx&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;problemas breves de conectividad&lt;/li&gt;
&lt;li&gt;servicios que están arrancando o recuperándose&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ejemplos de fallos que normalmente &lt;strong&gt;no&lt;/strong&gt; conviene reintentar:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;401 Unauthorized&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;403 Forbidden&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;404 Not Found&lt;/code&gt; cuando el recurso realmente no existe&lt;/li&gt;
&lt;li&gt;validaciones de negocio fallidas&lt;/li&gt;
&lt;li&gt;errores de formato o datos inválidos&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Esta distinción es la base del Retry Pattern. Si no la haces bien, el retry deja de ser una estrategia de resiliencia y pasa a ser una forma elegante de insistir inútilmente.&lt;/p&gt;

&lt;h2&gt;
  
  
  En HTTP no basta con capturar &lt;code&gt;HttpRequestException&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Aquí hay un matiz importante. En operaciones HTTP, no siempre decides reintentar por la excepción que recibes. Muy a menudo lo que realmente importa es el &lt;code&gt;status code&lt;/code&gt; de la respuesta.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;HttpRequestException&lt;/code&gt; suele representar errores de transporte o conectividad:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fallo DNS&lt;/li&gt;
&lt;li&gt;conexión rechazada&lt;/li&gt;
&lt;li&gt;corte de red&lt;/li&gt;
&lt;li&gt;handshake TLS fallido&lt;/li&gt;
&lt;li&gt;socket cerrado de forma inesperada&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Eso sí suele ser un buen candidato para retry.&lt;/p&gt;

&lt;p&gt;Pero en HTTP también existe otro caso: la petición llega al servidor, el servidor responde, y aun así la respuesta indica que conviene reintentar. Ahí el criterio ya no está en la excepción, sino en el código de estado.&lt;/p&gt;

&lt;p&gt;En otras palabras:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;si no hay respuesta, normalmente evalúas la excepción&lt;/li&gt;
&lt;li&gt;si sí hay respuesta, normalmente evalúas el &lt;code&gt;status code&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Además, con &lt;code&gt;HttpClient&lt;/code&gt; una respuesta &lt;code&gt;404&lt;/code&gt;, &lt;code&gt;429&lt;/code&gt; o &lt;code&gt;503&lt;/code&gt; no lanza excepción por sí sola. Solo obtienes una excepción si tú llamas a &lt;code&gt;EnsureSuccessStatusCode()&lt;/code&gt; o si el fallo ocurre antes de recibir la respuesta. Por eso, cuando haces retry en HTTP, muchas veces necesitas manejar &lt;strong&gt;ambas cosas&lt;/strong&gt;: excepciones de transporte y resultados HTTP no exitosos.&lt;/p&gt;

&lt;h2&gt;
  
  
  ¿Cuándo tiene sentido reintentar según el status code?
&lt;/h2&gt;

&lt;p&gt;Como regla general, tiene sentido considerar retry en estos códigos:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;408 Request Timeout&lt;/code&gt;: el servidor no completó la petición a tiempo&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;429 Too Many Requests&lt;/code&gt;: te están limitando; idealmente debes respetar &lt;code&gt;Retry-After&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;500 Internal Server Error&lt;/code&gt;: puede ser transitorio, aunque depende del sistema&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;502 Bad Gateway&lt;/code&gt;: fallo temporal de gateway o upstream&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;503 Service Unavailable&lt;/code&gt;: servicio saturado, caído o en mantenimiento&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;504 Gateway Timeout&lt;/code&gt;: el gateway no recibió respuesta a tiempo del upstream&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Hay otros códigos que &lt;strong&gt;a veces&lt;/strong&gt; pueden ser reintentables, pero dependen mucho del dominio:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;409 Conflict&lt;/code&gt;: puede tener sentido si el conflicto es temporal o si hay concurrencia optimista&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;423 Locked&lt;/code&gt;: puede ser temporal si el recurso está bloqueado brevemente&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;425 Too Early&lt;/code&gt;: en algunos escenarios conviene reintentar más tarde&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;En cambio, normalmente &lt;strong&gt;no&lt;/strong&gt; tiene sentido reintentar estos códigos:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;400 Bad Request&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;401 Unauthorized&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;403 Forbidden&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;404 Not Found&lt;/code&gt; cuando el recurso realmente no existe&lt;/li&gt;
&lt;li&gt;&lt;code&gt;405 Method Not Allowed&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;422 Unprocessable Entity&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;La idea de fondo es simple: si el problema está en tu request, reintentar no arregla nada. Si el problema está en el servidor, en la red o en una condición temporal del entorno, entonces retry sí puede tener sentido.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retry Pattern
&lt;/h2&gt;

&lt;p&gt;El Retry Pattern consiste en volver a ejecutar una operación que falló, asumiendo que el error pudo haber sido temporal.&lt;/p&gt;

&lt;p&gt;La clave no está en el hecho de reintentar. La clave está en &lt;strong&gt;cómo&lt;/strong&gt; reintentas.&lt;/p&gt;

&lt;p&gt;Un retry bien hecho:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;solo reintenta errores transitorios&lt;/li&gt;
&lt;li&gt;pone un límite claro de intentos&lt;/li&gt;
&lt;li&gt;espera entre intentos&lt;/li&gt;
&lt;li&gt;idealmente usa backoff y jitter&lt;/li&gt;
&lt;li&gt;deja trazabilidad con logs o métricas&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Un retry mal hecho:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;reintenta cualquier excepción&lt;/li&gt;
&lt;li&gt;dispara intentos de forma inmediata&lt;/li&gt;
&lt;li&gt;multiplica la carga sobre un servicio ya degradado&lt;/li&gt;
&lt;li&gt;empeora la latencia total&lt;/li&gt;
&lt;li&gt;puede generar operaciones duplicadas si no hay idempotencia&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Si te quedas con una sola idea del artículo, que sea esta: &lt;strong&gt;retry no es un parche; es una decisión de diseño&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un punto importante: idempotencia
&lt;/h2&gt;

&lt;p&gt;Antes de agregar reintentos a cualquier operación, hay una pregunta que conviene hacer siempre:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Si esta operación se ejecuta dos veces, ¿el resultado sigue siendo correcto?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Eso es idempotencia.&lt;/p&gt;

&lt;p&gt;Por ejemplo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;consultar datos suele ser seguro para reintentar&lt;/li&gt;
&lt;li&gt;actualizar un estado a un valor fijo puede ser seguro&lt;/li&gt;
&lt;li&gt;cobrar una tarjeta dos veces claramente no es seguro&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Si una operación no es idempotente, agregar retry sin más puede introducir errores peores que el fallo original.&lt;/p&gt;

&lt;p&gt;No quiere decir que no puedas reintentar nunca, pero sí que probablemente necesites una estrategia adicional: claves idempotentes, deduplicación, control transaccional o algún mecanismo parecido.&lt;/p&gt;

&lt;h2&gt;
  
  
  Instalación
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package Polly
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  El servicio inestable del ejemplo
&lt;/h2&gt;

&lt;p&gt;Para mostrar el comportamiento de Polly, el proyecto usa un servicio que falla de forma aleatoria. No es sofisticado, pero alcanza para representar bastante bien lo que pasa cuando dependes de infraestructura o servicios externos.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UnreliableService&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;Random&lt;/span&gt; &lt;span class="n"&gt;_random&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;_attemptCount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;FailureRate&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0.7&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ProcessDataAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_attemptCount&lt;/span&gt;&lt;span class="p"&gt;++;&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;currentAttempt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_attemptCount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"  [Intento &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;currentAttempt&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;] Procesando: \"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;\"..."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Delay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromMilliseconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;500&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NextDouble&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;FailureRate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;exception&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GetRandomException&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"  [Intento &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;currentAttempt&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;] ❌ Falló con: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetType&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"  [Intento &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;currentAttempt&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;] ✅ Éxito!"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;$"Procesado: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (intento #&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;currentAttempt&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Las excepciones simuladas son justamente de las que suelen entrar en la categoría de fallos transitorios:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;HttpRequestException&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TimeoutException&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;IOException&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;InvalidOperationException&lt;/code&gt; con mensaje temporal&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;En este ejemplo usamos excepciones porque no estamos simulando respuestas HTTP reales. Pero si esta misma idea se aplicara a &lt;code&gt;HttpClient&lt;/code&gt;, además de las excepciones convendría evaluar el &lt;code&gt;status code&lt;/code&gt; devuelto por el servidor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creando el pipeline de retry
&lt;/h2&gt;

&lt;p&gt;Aquí es donde Polly v8 muestra su enfoque. En este caso el ejemplo es genérico y por eso decide reintentar según excepciones:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Polly&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Polly.Retry&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;retryPipeline&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;ResiliencePipelineBuilder&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddRetry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;RetryStrategyOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;MaxRetryAttempts&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;BackoffType&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DelayBackoffType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Exponential&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Delay&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;UseJitter&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ShouldHandle&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;PredicateBuilder&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;HttpRequestException&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TimeoutException&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IOException&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
                &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"temporarily unavailable"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
        &lt;span class="n"&gt;OnRetry&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"  ⏳ Reintento #&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AttemptNumber&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; en &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RetryDelay&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TotalSeconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;F1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;s"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ValueTask&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CompletedTask&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Qué hace cada opción
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Propiedad&lt;/th&gt;
&lt;th&gt;Qué significa en la práctica&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MaxRetryAttempts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cuántas veces más vas a intentar después del primer fallo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BackoffType&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cómo crece la espera entre reintentos&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Delay&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;El delay base&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;UseJitter&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Agrega un pequeño factor aleatorio para que todos los clientes no reintenten al mismo tiempo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ShouldHandle&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Define exactamente qué errores o resultados sí merecen retry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;OnRetry&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Te permite registrar eventos, medir, agregar observabilidad o contexto&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  ¿Por qué usar backoff exponencial?
&lt;/h2&gt;

&lt;p&gt;Porque reintentar demasiado rápido suele ser una mala idea.&lt;/p&gt;

&lt;p&gt;Si un servicio está saturado o recién se está recuperando, pegarle tres veces seguidas en 50 milisegundos no ayuda. Lo más probable es que solo agraves el problema.&lt;/p&gt;

&lt;p&gt;Con backoff exponencial, el intervalo entre intentos crece progresivamente:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Exponential (base 1s): 1s -&amp;gt; 2s -&amp;gt; 4s -&amp;gt; 8s
Linear (base 1s):      1s -&amp;gt; 2s -&amp;gt; 3s -&amp;gt; 4s
Constant (base 1s):    1s -&amp;gt; 1s -&amp;gt; 1s -&amp;gt; 1s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;En la mayoría de integraciones con servicios externos, exponencial suele ser el punto de partida razonable.&lt;/p&gt;

&lt;h2&gt;
  
  
  ¿Y el jitter para qué sirve?
&lt;/h2&gt;

&lt;p&gt;Supongamos que tienes diez instancias de tu app. Todas llaman al mismo servicio. Todas fallan al mismo tiempo. Todas reintentan exactamente un segundo después.&lt;/p&gt;

&lt;p&gt;Acabas de crear una mini estampida coordinada.&lt;/p&gt;

&lt;p&gt;Eso se conoce como &lt;code&gt;thundering herd&lt;/code&gt;. El &lt;code&gt;jitter&lt;/code&gt; rompe ese patrón agregando una pequeña variación aleatoria en el delay. No parece gran cosa, pero en escenarios con volumen real hace bastante diferencia.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ejecutando la operación con resiliencia
&lt;/h2&gt;

&lt;p&gt;Una vez construido el pipeline, ejecutas la operación dentro de él:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;retryPipeline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExecuteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;unreliableService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ProcessDataAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Datos importantes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;CancellationToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;None&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"Resultado: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Una salida típica puede verse así:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Intento 1] Procesando: "Datos importantes"...
[Intento 1] ❌ Falló con: HttpRequestException
⏳ Reintento #1 en 1.2s
[Intento 2] Procesando: "Datos importantes"...
[Intento 2] ❌ Falló con: TimeoutException
⏳ Reintento #2 en 2.1s
[Intento 3] Procesando: "Datos importantes"...
[Intento 3] ✅ Éxito!

Resultado: Procesado: Datos importantes (intento #3)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lo interesante no es solo que eventualmente funcione. Lo importante es que el código de negocio queda limpio, y la política de resiliencia queda definida en un lugar explícito.&lt;/p&gt;

&lt;h2&gt;
  
  
  Centralizando pipelines reutilizables
&lt;/h2&gt;

&lt;p&gt;Si vas a usar la misma idea en varios lugares, tiene sentido encapsular la configuración en un factory. El proyecto ya hace algo de eso en &lt;code&gt;ResiliencePipelines&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ResiliencePipelines&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;ResiliencePipeline&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;CreateRetryPipeline&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;maxRetries&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;ResiliencePipelineBuilder&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddRetry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;RetryStrategyOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;MaxRetryAttempts&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;maxRetries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;BackoffType&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DelayBackoffType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Exponential&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;Delay&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;UseJitter&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;ShouldHandle&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;PredicateBuilder&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
                    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;HttpRequestException&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
                    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TimeoutException&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
                    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IOException&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
            &lt;span class="p"&gt;})&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Esto tiene varias ventajas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;evitas repetir configuración&lt;/li&gt;
&lt;li&gt;mantienes consistencia entre operaciones similares&lt;/li&gt;
&lt;li&gt;puedes cambiar la estrategia en un solo punto&lt;/li&gt;
&lt;li&gt;es más simple testear y evolucionar la política con el tiempo&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  También existe el pipeline sin tipo genérico
&lt;/h2&gt;

&lt;p&gt;Si tu operación no devuelve valor, puedes usar &lt;code&gt;ResiliencePipelineBuilder&lt;/code&gt; sin &lt;code&gt;T&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;pipeline&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ResiliencePipelineBuilder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddRetry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;RetryStrategyOptions&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;MaxRetryAttempts&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;BackoffType&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DelayBackoffType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Exponential&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Delay&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExecuteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;SubirArchivoAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Retry no vive solo
&lt;/h2&gt;

&lt;p&gt;Aunque el foco de este artículo sea retry, la resiliencia real rara vez se resuelve con una sola estrategia.&lt;/p&gt;

&lt;p&gt;Por ejemplo, este tipo de composición suele tener bastante sentido:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;advancedPipeline&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;ResiliencePipelineBuilder&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddRetry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;RetryStrategyOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;MaxRetryAttempts&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;BackoffType&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DelayBackoffType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Exponential&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Delay&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Aquí hay una idea importante: el retry te protege de fallos transitorios, pero el timeout te protege de operaciones que quedan colgadas demasiado tiempo.&lt;/p&gt;

&lt;p&gt;Con el tiempo, es común sumar otras estrategias según el caso:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Circuit Breaker&lt;/code&gt; para dejar de insistir cuando un servicio claramente está caído&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Timeout&lt;/code&gt; para no esperar indefinidamente&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Rate Limiter&lt;/code&gt; para proteger recursos internos o externos&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Fallback&lt;/code&gt; para responder de forma degradada cuando no puedes completar la operación principal&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Cuándo NO usar retry
&lt;/h2&gt;

&lt;p&gt;Este punto muchas veces queda corto, pero es de los más importantes.&lt;/p&gt;

&lt;p&gt;No deberías aplicar retry de forma automática cuando:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;el error es permanente&lt;/li&gt;
&lt;li&gt;la operación no es segura para repetir (no es idempotente)&lt;/li&gt;
&lt;li&gt;cada intento adicional empeora la congestión&lt;/li&gt;
&lt;li&gt;el costo de esperar más supera el beneficio&lt;/li&gt;
&lt;li&gt;el usuario necesita una respuesta rápida y explícita, no varios segundos de insistencia&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;En HTTP, eso también significa que no deberías decir "voy a reintentar cualquier &lt;code&gt;HttpRequestException&lt;/code&gt; o cualquier respuesta no exitosa". Hay que distinguir entre errores de transporte y respuestas con significado funcional.&lt;/p&gt;

&lt;p&gt;En otras palabras: resiliencia no significa esconder todos los errores. Significa tratar mejor los errores que sí vale la pena absorber.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué conviene observar en producción
&lt;/h2&gt;

&lt;p&gt;Si agregas retry pero no mides nada, te pierdes la mitad del valor.&lt;/p&gt;

&lt;p&gt;Como mínimo, conviene tener visibilidad sobre esto:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;cuántos reintentos se están ejecutando&lt;/li&gt;
&lt;li&gt;qué excepciones están disparando esos retries&lt;/li&gt;
&lt;li&gt;cuánto aumenta la latencia total por los reintentos&lt;/li&gt;
&lt;li&gt;qué operaciones terminan fallando incluso después de agotar el pipeline&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Eso te ayuda a responder preguntas concretas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;¿estoy absorbiendo fallos ocasionales o tapando un problema más serio?&lt;/li&gt;
&lt;li&gt;¿mi política de retry está bien definida o está agregando demasiada espera?&lt;/li&gt;
&lt;li&gt;¿hay un servicio externo que se está degradando más de lo que pensábamos?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Polly te deja enganchar callbacks como &lt;code&gt;OnRetry&lt;/code&gt;, y desde ahí puedes registrar eventos, emitir métricas o enriquecer trazas distribuidas. En local alcanza con un &lt;code&gt;Console.WriteLine&lt;/code&gt;, pero en producción lo ideal es que esos eventos terminen en tu stack de observabilidad.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ejemplo más cercano a un caso real
&lt;/h2&gt;

&lt;p&gt;Si en vez de un servicio simulado estuvieras subiendo un archivo a Azure Blob Storage, la idea sería muy parecida:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ResilientBlobUploader&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;BlobContainerClient&lt;/span&gt; &lt;span class="n"&gt;_container&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;ResiliencePipeline&lt;/span&gt; &lt;span class="n"&gt;_uploadPipeline&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;ResilientBlobUploader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BlobContainerClient&lt;/span&gt; &lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_container&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;_uploadPipeline&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ResiliencePipelineBuilder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddRetry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;RetryStrategyOptions&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;MaxRetryAttempts&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;BackoffType&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DelayBackoffType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Exponential&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;Delay&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;ShouldHandle&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;PredicateBuilder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;RequestFailedException&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
                        &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="m"&gt;408&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="m"&gt;429&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="m"&gt;500&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="m"&gt;502&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="m"&gt;503&lt;/span&gt; &lt;span class="k"&gt;or&lt;/span&gt; &lt;span class="m"&gt;504&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IOException&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
            &lt;span class="p"&gt;})&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;UploadAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;blobName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Stream&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_uploadPipeline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExecuteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;blob&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetBlobClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blobName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UploadAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;overwrite&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Observa algo importante: no se manejan todos los códigos de error, solo los que realmente sugieren una condición temporal o recuperable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ejecutar el ejemplo
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El programa compara operaciones con y sin resiliencia, y además muestra un pipeline inline para que se vea la diferencia entre centralizar la configuración o definirla localmente.&lt;/p&gt;

&lt;p&gt;Conviene ejecutarlo varias veces, porque al haber fallos aleatorios vas a ver escenarios distintos en cada corrida.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusión
&lt;/h2&gt;

&lt;p&gt;Lo más valioso de Polly no es que "agrega retries". Lo valioso es que te obliga a pensar mejor cómo responde tu sistema frente a fallos normales del entorno.&lt;/p&gt;

&lt;p&gt;Si tuviera que resumir el mensaje del artículo, sería este:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;No todos los errores son iguales.&lt;/li&gt;
&lt;li&gt;Algunos errores son transitorios y merecen un retry.&lt;/li&gt;
&lt;li&gt;Un retry bien configurado necesita criterio, límites, backoff y observabilidad.&lt;/li&gt;
&lt;li&gt;Si la operación no es idempotente, reintentar puede ser peligroso.&lt;/li&gt;
&lt;li&gt;Retry es solo una pieza dentro de una estrategia de resiliencia más amplia.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Polly v8 hace que modelar todo esto sea bastante más cómodo que antes. Y eso está bueno, porque en aplicaciones reales la resiliencia no suele ser un lujo: suele ser parte de hacer software que se comporte bien fuera de tu máquina.&lt;/p&gt;

&lt;h2&gt;
  
  
  Referencias
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/App-vNext/Polly" rel="noopener noreferrer"&gt;Polly - repositorio oficial&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.pollydocs.org/" rel="noopener noreferrer"&gt;Polly v8 - documentación&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.pollydocs.org/strategies/retry" rel="noopener noreferrer"&gt;Retry strategy - Polly docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/dotnet/core/resilience/http-resilience" rel="noopener noreferrer"&gt;HTTP resilience en .NET&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/azure/architecture/patterns/retry" rel="noopener noreferrer"&gt;Retry pattern - Azure Architecture Center&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/dotnet/architecture/microservices/implement-resilient-applications/" rel="noopener noreferrer"&gt;Resilient applications y transient faults en .NET&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>csharp</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>EF Core: tu query funciona, tus pruebas pasan… y estás leyendo 50,000 filas para devolver 3</title>
      <dc:creator>Isaac Ojeda</dc:creator>
      <pubDate>Sat, 14 Mar 2026 22:44:46 +0000</pubDate>
      <link>https://dev.to/isaacojeda/ef-core-tu-query-funciona-tus-pruebas-pasan-y-estas-leyendo-50000-filas-para-devolver-3-1c9e</link>
      <guid>https://dev.to/isaacojeda/ef-core-tu-query-funciona-tus-pruebas-pasan-y-estas-leyendo-50000-filas-para-devolver-3-1c9e</guid>
      <description>&lt;p&gt;El código lleva meses en producción. Nadie se ha quejado. Todas las pruebas pasan.&lt;/p&gt;

&lt;p&gt;Luego llega un cliente grande, con años de historial acumulado, y esa pantalla que siempre cargó “bien” empieza a tardar varios segundos. A veces ni siquiera falla: simplemente se vuelve torpe, pesada, impredecible.&lt;/p&gt;

&lt;p&gt;Empiezas a revisar y descubres algo incómodo: el problema siempre estuvo ahí. Solo necesitaba suficientes datos para hacerse visible.&lt;/p&gt;

&lt;p&gt;Ese es uno de los patrones más traicioneros cuando trabajas con EF Core. No porque el código esté roto, sino porque &lt;strong&gt;funciona&lt;/strong&gt;. Devuelve los datos correctos. Pasa QA. Sobrevive en producción durante meses. Y aun así, por debajo, puede estar ejecutando consultas mucho más caras de lo que parecen.&lt;/p&gt;

&lt;p&gt;En este artículo quiero mostrar dos trampas comunes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;cuando materializas demasiado pronto con &lt;code&gt;ToList()&lt;/code&gt; y el filtro termina ocurriendo en memoria;&lt;/li&gt;
&lt;li&gt;cuando dejas lógica no traducible dentro de la proyección y la consulta deja de ser tan eficiente como parece.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;La idea no es memorizar reglas raras de EF Core. La idea es desarrollar un reflejo: &lt;strong&gt;siempre revisar el SQL real que se genera&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Por qué casi nadie lo detecta a tiempo
&lt;/h2&gt;

&lt;p&gt;Este problema rara vez aparece de golpe.&lt;/p&gt;

&lt;p&gt;En local, todo corre sobre una base pequeña, con datos de prueba y una máquina rápida. Las consultas tardan milisegundos y nadie siente la necesidad de inspeccionar el SQL.&lt;/p&gt;

&lt;p&gt;En QA o staging, la infraestructura puede parecerse a producción, pero el volumen de datos sigue sin representar el mundo real. El problema ya existe, solo que todavía no pesa.&lt;/p&gt;

&lt;p&gt;En producción, con clientes pequeños, los tiempos siguen siendo aceptables. No hay alertas. No hay tickets. No hay razón aparente para abrir el capó.&lt;/p&gt;

&lt;p&gt;Y entonces llega el cliente con años de historial, decenas de miles de registros y patrones reales de uso. Ahí, por primera vez, la consulta muestra su costo verdadero.&lt;/p&gt;

&lt;p&gt;Lo más difícil de este escenario no es corregirlo. Lo más difícil es que cuando por fin se vuelve visible, el código ya lleva tanto tiempo vivo que nadie recuerda con claridad por qué se escribió así.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;En EF Core, el peligro no siempre es que algo falle. A veces el peligro es que funcione… pero de la forma más cara posible.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Problema 1: el &lt;code&gt;ToList()&lt;/code&gt; que mueve el filtro fuera de SQL
&lt;/h2&gt;

&lt;h3&gt;
  
  
  El origen: una excepción legítima
&lt;/h3&gt;

&lt;p&gt;Todo empieza con algo bastante normal: quieres usar lógica de negocio propia dentro de un &lt;code&gt;Where&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;pedidos&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pedidos&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;EsDelUltimoMes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FechaCreacion&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;PedidoResumenDto&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Cliente&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cliente&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Nombre&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Total&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;EsDelUltimoMes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;fecha&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="n"&gt;fecha&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddMonths&lt;/span&gt;&lt;span class="p"&gt;(-&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ese código no es traducible tal como está. &lt;code&gt;EsDelUltimoMes&lt;/code&gt; es un método local de C#, y EF Core no sabe convertirlo a SQL. En EF Core moderno, eso normalmente termina en una excepción de traducción.&lt;/p&gt;

&lt;p&gt;Algo como esto:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;InvalidOperationException: The LINQ expression could not be translated.
Either rewrite the query in a form that can be translated, or switch to
client evaluation explicitly by inserting a call to 'AsEnumerable',
'AsAsyncEnumerable', 'ToList', or 'ToListAsync'.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La excepción no miente. Pero es muy fácil leerla de forma peligrosa.&lt;/p&gt;

&lt;h3&gt;
  
  
  La “solución” que parece arreglar todo
&lt;/h3&gt;

&lt;p&gt;Alguien ve que el mensaje menciona &lt;code&gt;ToList()&lt;/code&gt;, lo agrega, y el problema desaparece:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;pedidos&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pedidos&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToList&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;EsDelUltimoMes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FechaCreacion&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;PedidoResumenDto&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Cliente&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cliente&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Nombre&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Total&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Y sí: ahora funciona. Compila, devuelve resultados correctos y pasa las pruebas.&lt;/p&gt;

&lt;p&gt;Pero ya no estás filtrando en la base de datos. Estás trayendo todos los registros primero, y luego aplicando la lógica en memoria.&lt;/p&gt;

&lt;p&gt;En otras palabras, el SQL que sale ya no se parece a la intención original. Se parece más a esto:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClienteId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Activo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FechaCreacion&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;Pedidos&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sin &lt;code&gt;WHERE&lt;/code&gt;. Sin proyección útil. Sin límite.&lt;/p&gt;

&lt;p&gt;Si la tabla tiene 50,000 filas, esas 50,000 filas viajan completas desde la base de datos hasta tu aplicación. Solo después haces el filtro en C# para devolver quizá 3 resultados al usuario.&lt;/p&gt;

&lt;p&gt;Ese es el tipo de problema que no se nota con 200 registros, pero sí con años de operación acumulada.&lt;/p&gt;

&lt;h3&gt;
  
  
  La forma correcta
&lt;/h3&gt;

&lt;p&gt;La solución real es expresar el filtro usando operadores que EF Core sí conozca y pueda traducir.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;fechaLimite&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddMonths&lt;/span&gt;&lt;span class="p"&gt;(-&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;pedidos&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pedidos&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FechaCreacion&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;fechaLimite&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;PedidoResumenDto&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Cliente&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cliente&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Nombre&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Total&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ahora sí el filtro vive donde debe vivir: en SQL.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Nombre&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Total&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;Pedidos&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;Clientes&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClienteId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FechaCreacion&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2025-02-14'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La regla práctica aquí no es “nunca uses métodos”. Es esta: &lt;strong&gt;mientras la lógica se exprese con operadores y funciones que EF Core conoce, podrá empujarla a SQL&lt;/strong&gt;. Cuando no pueda, corres el riesgo de que la evaluación ocurra fuera de la base de datos o de que EF directamente falle al traducir.&lt;/p&gt;

&lt;p&gt;Si necesitas lógica de negocio más compleja, aplícala después de materializar, pero solo sobre un conjunto de datos que ya filtraste correctamente en la base.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Nota: en algunos escenarios complejos, usar Raw SQL es perfectamente válido. No es una derrota; es una herramienta más.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Problema 2: la proyección parece pequeña, pero la consulta deja de ser eficiente
&lt;/h2&gt;

&lt;p&gt;El segundo problema es más silencioso.&lt;/p&gt;

&lt;p&gt;Aquí el &lt;code&gt;Where&lt;/code&gt; sí se traduce. La consulta sí filtra en la base de datos. Los resultados son correctos. El detalle está en la proyección.&lt;/p&gt;

&lt;h3&gt;
  
  
  El escenario
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;pedidos&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pedidos&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FechaCreacion&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;fechaLimite&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;PedidoResumenDto&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Cliente&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cliente&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Nombre&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Estatus&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ObtenerEtiqueta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Activo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;ObtenerEtiqueta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;activo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="n"&gt;activo&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s"&gt;"Activo"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Inactivo"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A simple vista, parece una proyección pequeña. Solo quieres tres campos.&lt;/p&gt;

&lt;p&gt;El problema es que &lt;code&gt;ObtenerEtiqueta&lt;/code&gt; sigue siendo lógica local de C#. EF Core no puede traducir ese método directamente. En la proyección final, eso puede hacer que parte del trabajo termine resolviéndose del lado del cliente.&lt;/p&gt;

&lt;p&gt;Y cuando eso pasa, la consulta puede traer &lt;strong&gt;más información de la necesaria&lt;/strong&gt; para completar esa proyección en memoria.&lt;/p&gt;

&lt;p&gt;La parte peligrosa no es solo “que funcione”. Es que desde el código parece una consulta mínima, cuando en realidad el SQL puede estar cargando más columnas de las que tú creías.&lt;/p&gt;

&lt;p&gt;No siempre será literalmente un &lt;code&gt;SELECT *&lt;/code&gt;, pero sí puede ocurrir que EF traiga suficiente información como para volver la proyección más costosa de lo que aparenta en C#.&lt;/p&gt;

&lt;h3&gt;
  
  
  La forma correcta
&lt;/h3&gt;

&lt;p&gt;Si el formateo no necesita vivir en SQL, sepáralo de la consulta:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;pedidos&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pedidos&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FechaCreacion&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;fechaLimite&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;PedidoResumenDto&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Cliente&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cliente&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Nombre&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;EsActivo&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Activo&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Dejar que la capa presentación muestre los datos&lt;/span&gt;
&lt;span class="c1"&gt;// o&lt;/span&gt;
&lt;span class="c1"&gt;// 👇🏼&lt;/span&gt;
&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;pedido&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;pedidos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;pedido&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Estatus&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ObtenerEtiqueta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pedido&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EsActivo&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Así la base de datos solo devuelve lo necesario, y el formateo ocurre después, en memoria, pero ya sobre un conjunto pequeño y bien proyectado.&lt;/p&gt;

&lt;p&gt;Si la lógica sí puede expresarse con algo traducible, entonces mejor dejarla en la consulta:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;pedidos&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pedidos&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FechaCreacion&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;fechaLimite&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;PedidoResumenDto&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Cliente&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cliente&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Nombre&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Estatus&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Activo&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s"&gt;"Activo"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Inactivo"&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Una expresión ternaria simple como esa normalmente sí se traduce a &lt;code&gt;CASE WHEN&lt;/code&gt; en SQL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cómo inspeccionar lo que EF Core realmente está haciendo
&lt;/h2&gt;

&lt;p&gt;Si no ves el SQL, estás adivinando.&lt;/p&gt;

&lt;p&gt;Y adivinar con consultas que hoy corren sobre 500 filas y mañana correrán sobre 5 millones suele salir caro.&lt;/p&gt;

&lt;h3&gt;
  
  
  Opción 1: logs en desarrollo
&lt;/h3&gt;

&lt;p&gt;Puedes habilitar logs del contexto para ver el SQL ejecutado:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddDbContext&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AppDbContext&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseSqlServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connectionString&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LogLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Information&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
           &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EnableSensitiveDataLogging&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;EnableSensitiveDataLogging()&lt;/code&gt; conviene dejarlo solo en desarrollo.&lt;/p&gt;

&lt;p&gt;En la salida, busca entradas como esta:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Executed DbCommand (X ms) [Parameters=[...], CommandType='Text', CommandTimeout='30']
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ahí está la consulta real.&lt;/p&gt;

&lt;h3&gt;
  
  
  Opción 2: &lt;code&gt;ToQueryString()&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Cuando quieres revisar una consulta específica, esta suele ser la forma más directa:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pedidos&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FechaCreacion&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;fechaLimite&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;PedidoResumenDto&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Cliente&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cliente&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Nombre&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Estatus&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Activo&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s"&gt;"Activo"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Inactivo"&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToQueryString&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;pedidos&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ToQueryString()&lt;/code&gt; funciona sobre cualquier &lt;code&gt;IQueryable&lt;/code&gt; antes de materializarlo. Si tienes dudas sobre traducción, úsalo. Te ahorra suposiciones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un detalle extra en consultas de lectura
&lt;/h2&gt;

&lt;p&gt;Si esa consulta solo existe para mostrar datos y no vas a modificar las entidades después, vale la pena considerar &lt;code&gt;AsNoTracking()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;pedidos&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pedidos&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsNoTracking&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FechaCreacion&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;fechaLimite&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;PedidoResumenDto&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Cliente&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cliente&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Nombre&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Total&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No arregla problemas de traducción ni evita consultas mal diseñadas. Pero sí reduce trabajo del change tracker en escenarios de solo lectura, y eso puede sumar bastante cuando la pantalla consulta muchos registros.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sobre asistentes de código y queries “que funcionan”
&lt;/h2&gt;

&lt;p&gt;Los asistentes de código suelen optimizar para darte algo que compile, que se vea razonable y que devuelva el resultado esperado.&lt;/p&gt;

&lt;p&gt;Eso está bien. Pero una consulta correcta funcionalmente no siempre es una consulta sana desde el punto de vista de traducción o performance.&lt;/p&gt;

&lt;p&gt;El asistente no ve el volumen real de tu tabla. No está mirando el plan de ejecución. No sabe si ese query hoy devuelve 20 registros o si mañana va a inspeccionar medio millón para construir una pantalla.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Por eso esta revisión sigue siendo responsabilidad del developer&lt;/strong&gt;: no basta con que la consulta compile y pase pruebas. Hay que confirmar qué SQL genera y dónde se está ejecutando realmente cada parte de la lógica.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resumen
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situación&lt;/th&gt;
&lt;th&gt;Qué ocurre&lt;/th&gt;
&lt;th&gt;Cómo detectarlo&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Método local no traducible dentro de &lt;code&gt;Where&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;EF Core normalmente falla al traducir o te obliga a mover la evaluación fuera de SQL&lt;/td&gt;
&lt;td&gt;Excepción de traducción&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;ToList()&lt;/code&gt; antes del filtro&lt;/td&gt;
&lt;td&gt;La consulta se materializa completa y el filtrado ocurre en memoria&lt;/td&gt;
&lt;td&gt;Revisando el código y el SQL generado&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lógica no traducible en la proyección final&lt;/td&gt;
&lt;td&gt;Parte de la proyección puede resolverse en cliente y traer más datos de los necesarios&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ToQueryString()&lt;/code&gt;, logs y revisión de columnas consultadas&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;La idea central de todo esto es simple: &lt;strong&gt;no confíes en que una query está bien solo porque funciona&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;En EF Core, una consulta puede ser correcta en resultados y aun así ser innecesariamente cara. Y la forma más confiable de descubrirlo no es mirando el LINQ: es mirando el SQL.&lt;/p&gt;

&lt;h2&gt;
  
  
  ¿Qué sigue?
&lt;/h2&gt;

&lt;p&gt;Este es el primero de una serie sobre cosas que EF Core hace de forma poco obvia y que solo se vuelven dolorosas cuando los datos son reales.&lt;/p&gt;

&lt;p&gt;El siguiente artículo va sobre el problema N+1: esa consulta que en desarrollo parece hacer unas pocas llamadas a la base, pero en producción termina haciendo cientos o miles.&lt;/p&gt;

&lt;p&gt;Si alguna vez te pasó algo parecido, seguramente no fue porque el código estuviera “mal” en apariencia. Fue porque el costo real estaba escondido en un lugar que casi nadie revisa a tiempo.&lt;/p&gt;

</description>
      <category>efcore</category>
      <category>dotnet</category>
      <category>csharp</category>
    </item>
    <item>
      <title>Primeros pasos con Microsoft Agent Framework: construyendo un chatbot de soporte con C#</title>
      <dc:creator>Isaac Ojeda</dc:creator>
      <pubDate>Tue, 24 Feb 2026 03:33:14 +0000</pubDate>
      <link>https://dev.to/isaacojeda/primeros-pasos-con-microsoft-agent-framework-construyendo-un-chatbot-de-soporte-con-c-46m0</link>
      <guid>https://dev.to/isaacojeda/primeros-pasos-con-microsoft-agent-framework-construyendo-un-chatbot-de-soporte-con-c-46m0</guid>
      <description>&lt;h2&gt;
  
  
  Introducción
&lt;/h2&gt;

&lt;p&gt;Microsoft lleva años apostando por herramientas para construir aplicaciones con IA: primero llegó &lt;strong&gt;Semantic Kernel&lt;/strong&gt;, después &lt;strong&gt;AutoGen&lt;/strong&gt;, y ahora ambos convergen en su sucesor directo: &lt;strong&gt;Microsoft Agent Framework (MAF)&lt;/strong&gt;. Creado por los mismos equipos, MAF unifica lo mejor de ambos mundos — la estabilidad y características enterprise de Semantic Kernel con las abstracciones simples para agentes multi-turno de AutoGen — y agrega nuevas capacidades como workflows basados en grafos y un sistema robusto de manejo de estado.&lt;/p&gt;

&lt;p&gt;Al momento de escribir este artículo, MAF está en &lt;strong&gt;public preview&lt;/strong&gt;, disponible para .NET y Python bajo licencia MIT.&lt;/p&gt;

&lt;p&gt;En este artículo vamos a aprender los conceptos fundamentales del framework y a construir algo concreto: una API con ASP.NET Core 10 que expone un chatbot de soporte técnico capaz de responder preguntas basándose en documentación Markdown interna, manteniendo el contexto de la conversación entre mensajes.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;El código completo está disponible en &lt;a href="https://github.com/isaacOjeda/DevToPosts/tree/main/AgentFramework/SupportBot" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Desarrollo
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ¿Qué es Microsoft Agent Framework?
&lt;/h3&gt;

&lt;p&gt;MAF gira en torno a dos conceptos centrales que vale la pena entender antes de escribir código:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agents&lt;/strong&gt; son sistemas que usan un LLM para procesar entradas, tomar decisiones, llamar herramientas y generar respuestas. Son ideales cuando la tarea es dinámica y no puedes predecir de antemano exactamente qué pasos se van a necesitar — como una conversación de soporte donde el usuario puede preguntar cualquier cosa.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Workflows&lt;/strong&gt; son secuencias de pasos definidas explícitamente, conectadas en un grafo. Son la elección correcta cuando el flujo es predecible y necesitas control determinista sobre la ejecución — como un pipeline de procesamiento de datos o un flujo de aprobaciones.&lt;/p&gt;

&lt;p&gt;La documentación oficial tiene una regla de oro que me parece honesta y útil: &lt;em&gt;"If you can write a function to handle the task, do that instead of using an AI agent."&lt;/em&gt; No todo necesita un agente. En este artículo construimos un agente porque la naturaleza conversacional e impredecible del soporte técnico es exactamente el caso de uso para el que están diseñados.&lt;/p&gt;

&lt;h3&gt;
  
  
  Los 5 conceptos clave
&lt;/h3&gt;

&lt;p&gt;El tutorial oficial de Microsoft Learn organiza el aprendizaje en 5 pasos progresivos. Aquí los resumo en mis propias palabras antes de ver el código:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Tu primer agente.&lt;/strong&gt; Un &lt;code&gt;AIAgent&lt;/code&gt; se crea a partir de un cliente de chat (Azure OpenAI, OpenAI, etc.) con instrucciones y un nombre. Es stateless por diseño — el mismo agente puede atender múltiples conversaciones en paralelo. Correrlo es tan simple como &lt;code&gt;await agent.RunAsync("pregunta")&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Tools.&lt;/strong&gt; Cualquier método C# decorado con &lt;code&gt;[Description]&lt;/code&gt; se puede convertir en una herramienta que el agente puede llamar cuando lo necesite, usando &lt;code&gt;AIFunctionFactory.Create()&lt;/code&gt;. El LLM decide autónomamente cuándo y con qué argumentos invocarla. Esto es lo que le da al agente la capacidad de actuar sobre el mundo real.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Multi-turn conversations.&lt;/strong&gt; Como el agente es stateless, el historial de la conversación vive en un &lt;code&gt;AgentSession&lt;/code&gt;. Se crea con &lt;code&gt;agent.CreateSessionAsync()&lt;/code&gt; y se pasa en cada llamada. El agente recuerda todo lo que ocurrió en esa sesión.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Memory y Persistencia.&lt;/strong&gt; La sesión es serializable a &lt;code&gt;JsonElement&lt;/code&gt;. Esto permite guardarla en cualquier almacenamiento (memoria, base de datos, Redis) y reconstruirla después con &lt;code&gt;agent.DeserializeSessionAsync()&lt;/code&gt;. Para un chat de soporte, esto significa que el usuario puede retomar una conversación donde la dejó, incluso si el servidor se reinició.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Workflows.&lt;/strong&gt; Para orquestar múltiples agentes o pasos en secuencias definidas, se usa &lt;code&gt;WorkflowBuilder&lt;/code&gt;. Se definen &lt;code&gt;Executors&lt;/code&gt; (unidades de procesamiento) y se conectan con &lt;code&gt;Edges&lt;/code&gt;. En este artículo no los implementamos porque no añaden valor real al caso de uso — pero al final menciono cuándo sí tendría sentido usarlos.&lt;/p&gt;

&lt;h3&gt;
  
  
  El ejemplo: SupportBot
&lt;/h3&gt;

&lt;p&gt;El escenario es simple: una empresa tiene documentación interna en archivos &lt;code&gt;.md&lt;/code&gt; — guías de usuario, FAQs, manuales de módulos. En lugar de que los empleados busquen manualmente en esos archivos, un chatbot lee la documentación y responde en lenguaje natural, manteniendo el hilo de la conversación.&lt;/p&gt;

&lt;p&gt;Deliberadamente dejé fuera RAG y embeddings. La búsqueda por keywords en archivos planos es suficiente para demostrar cómo funciona MAF, y mantiene el ejemplo enfocado en el framework, no en infraestructura de búsqueda vectorial.&lt;/p&gt;

&lt;p&gt;La estructura del proyecto es la siguiente:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SupportBot/
├── Program.cs
├── appsettings.json
├── Docs/
│   ├── accesos.md
│   ├── facturacion.md
│   └── reportes.md
├── Agents/
│   └── SupportAgentFactory.cs
├── Tools/
│   └── DocumentationTool.cs
├── Models/
│   ├── ChatRequest.cs
│   └── ChatResponse.cs
└── Sessions/
    └── InMemorySessionStore.cs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Instalación
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet new webapi &lt;span class="nt"&gt;-n&lt;/span&gt; SupportBot
&lt;span class="nb"&gt;cd &lt;/span&gt;SupportBot

dotnet add package Azure.AI.OpenAI &lt;span class="nt"&gt;--prerelease&lt;/span&gt;
dotnet add package Azure.Identity
dotnet add package Microsoft.Agents.AI &lt;span class="nt"&gt;--prerelease&lt;/span&gt;
dotnet add package Microsoft.Agents.AI.OpenAI &lt;span class="nt"&gt;--prerelease&lt;/span&gt;
dotnet add package Microsoft.Extensions.AI &lt;span class="nt"&gt;--prerelease&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Todos los paquetes de &lt;code&gt;Microsoft.Agents.AI&lt;/code&gt; están en prerelease — el flag es necesario.&lt;/p&gt;

&lt;h3&gt;
  
  
  La Tool: DocumentationTool
&lt;/h3&gt;

&lt;p&gt;Aquí está el corazón del ejemplo. &lt;code&gt;DocumentationTool&lt;/code&gt; es la herramienta que el agente llamará cuando necesite buscar información para responder al usuario.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.ComponentModel&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;SupportBot.Tools&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DocumentationTool&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;_docsPath&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;DocumentationTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;docsPath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_docsPath&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;docsPath&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Busca en la documentación interna del sistema información sobre un tema específico. Úsala siempre antes de responder una pregunta del usuario."&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;GetDocumentation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"El tema o concepto sobre el que buscar. Por ejemplo: 'accesos', 'facturación', 'reportes', 'contraseña', etc."&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
        &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;Directory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_docsPath&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"No hay documentación disponible en este momento."&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Directory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetFiles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_docsPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"*.md"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"No hay documentación disponible en este momento."&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;keywords&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToLowerInvariant&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sc"&gt;' '&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StringSplitOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RemoveEmptyEntries&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;matchingFiles&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;fileName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetFileNameWithoutExtension&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ToLowerInvariant&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReadAllText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ToLowerInvariant&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;keywords&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;fileName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;ToList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;filesToRead&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;matchingFiles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;matchingFiles&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StringBuilder&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;filesToRead&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;fileName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetFileNameWithoutExtension&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ReadAllText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AppendLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"=== &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;fileName&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToUpperInvariant&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s"&gt; ==="&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AppendLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AppendLine&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hay dos detalles importantes aquí. El primero es que los atributos &lt;code&gt;[Description]&lt;/code&gt; no son decorativos — MAF los usa para construir el schema que el LLM recibe, y de su calidad depende que el modelo entienda bien cuándo y cómo llamar la herramienta. El segundo es la estrategia de fallback: si no encuentra archivos que hagan match con el tema, retorna todos los documentos. Es una decisión pragmática — preferimos que el agente tenga demasiado contexto a que se quede sin información para responder.&lt;/p&gt;

&lt;h3&gt;
  
  
  El Agente: SupportAgentFactory
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Azure.AI.OpenAI&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Agents.AI&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Extensions.AI&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;SupportBot.Tools&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;SupportBot.Agents&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SupportAgentFactory&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;AIAgent&lt;/span&gt; &lt;span class="n"&gt;_agent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;SupportAgentFactory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IConfiguration&lt;/span&gt; &lt;span class="n"&gt;configuration&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IWebHostEnvironment&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;endpoint&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;configuration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"AzureOpenAI:Endpoint"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"AzureOpenAI:Endpoint no configurado"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;deploymentName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;configuration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"AzureOpenAI:DeploymentName"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="s"&gt;"gpt-4o-mini"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;apiKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;configuration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"AzureOpenAI:ApiKey"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"AzureOpenAI:ApiKey no configurado"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;docsPath&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Combine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ContentRootPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Docs"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;docTool&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;DocumentationTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;docsPath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;instructions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"""
&lt;/span&gt;            &lt;span class="n"&gt;Eres&lt;/span&gt; &lt;span class="n"&gt;un&lt;/span&gt; &lt;span class="n"&gt;asistente&lt;/span&gt; &lt;span class="n"&gt;de&lt;/span&gt; &lt;span class="n"&gt;soporte&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="err"&gt;é&lt;/span&gt;&lt;span class="n"&gt;cnico&lt;/span&gt; &lt;span class="n"&gt;interno&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="n"&gt;Tu&lt;/span&gt; &lt;span class="err"&gt;ú&lt;/span&gt;&lt;span class="n"&gt;nica&lt;/span&gt; &lt;span class="n"&gt;fuente&lt;/span&gt; &lt;span class="n"&gt;de&lt;/span&gt; &lt;span class="n"&gt;informaci&lt;/span&gt;&lt;span class="err"&gt;ó&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;
            &lt;span class="n"&gt;es&lt;/span&gt; &lt;span class="n"&gt;la&lt;/span&gt; &lt;span class="n"&gt;documentaci&lt;/span&gt;&lt;span class="err"&gt;ó&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="n"&gt;del&lt;/span&gt; &lt;span class="n"&gt;sistema&lt;/span&gt; &lt;span class="n"&gt;que&lt;/span&gt; &lt;span class="n"&gt;puedes&lt;/span&gt; &lt;span class="n"&gt;consultar&lt;/span&gt; &lt;span class="n"&gt;con&lt;/span&gt; &lt;span class="n"&gt;la&lt;/span&gt; &lt;span class="n"&gt;herramienta&lt;/span&gt; &lt;span class="n"&gt;GetDocumentation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;

            &lt;span class="n"&gt;Reglas&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;SIEMPRE&lt;/span&gt; &lt;span class="n"&gt;consulta&lt;/span&gt; &lt;span class="n"&gt;la&lt;/span&gt; &lt;span class="n"&gt;documentaci&lt;/span&gt;&lt;span class="err"&gt;ó&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="n"&gt;antes&lt;/span&gt; &lt;span class="n"&gt;de&lt;/span&gt; &lt;span class="n"&gt;responder&lt;/span&gt; &lt;span class="n"&gt;cualquier&lt;/span&gt; &lt;span class="n"&gt;pregunta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
            &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;Responde&lt;/span&gt; &lt;span class="n"&gt;de&lt;/span&gt; &lt;span class="n"&gt;forma&lt;/span&gt; &lt;span class="n"&gt;clara&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;concisa&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="n"&gt;en&lt;/span&gt; &lt;span class="n"&gt;el&lt;/span&gt; &lt;span class="n"&gt;mismo&lt;/span&gt; &lt;span class="n"&gt;idioma&lt;/span&gt; &lt;span class="n"&gt;que&lt;/span&gt; &lt;span class="n"&gt;el&lt;/span&gt; &lt;span class="n"&gt;usuario&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
            &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;Si&lt;/span&gt; &lt;span class="n"&gt;la&lt;/span&gt; &lt;span class="n"&gt;documentaci&lt;/span&gt;&lt;span class="err"&gt;ó&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="n"&gt;no&lt;/span&gt; &lt;span class="n"&gt;contiene&lt;/span&gt; &lt;span class="n"&gt;la&lt;/span&gt; &lt;span class="n"&gt;respuesta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dilo&lt;/span&gt; &lt;span class="n"&gt;claramente&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
            &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;No&lt;/span&gt; &lt;span class="n"&gt;inventes&lt;/span&gt; &lt;span class="n"&gt;informaci&lt;/span&gt;&lt;span class="err"&gt;ó&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="n"&gt;que&lt;/span&gt; &lt;span class="n"&gt;no&lt;/span&gt; &lt;span class="n"&gt;est&lt;/span&gt;&lt;span class="err"&gt;é&lt;/span&gt; &lt;span class="n"&gt;en&lt;/span&gt; &lt;span class="n"&gt;la&lt;/span&gt; &lt;span class="n"&gt;documentaci&lt;/span&gt;&lt;span class="err"&gt;ó&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
            &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;Si&lt;/span&gt; &lt;span class="n"&gt;el&lt;/span&gt; &lt;span class="n"&gt;usuario&lt;/span&gt; &lt;span class="n"&gt;saluda&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="n"&gt;hace&lt;/span&gt; &lt;span class="n"&gt;preguntas&lt;/span&gt; &lt;span class="n"&gt;generales&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;responde&lt;/span&gt; &lt;span class="n"&gt;amablemente&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="n"&gt;pregunta&lt;/span&gt; &lt;span class="n"&gt;en&lt;/span&gt; &lt;span class="n"&gt;qu&lt;/span&gt;&lt;span class="err"&gt;é&lt;/span&gt; &lt;span class="n"&gt;puedes&lt;/span&gt; &lt;span class="n"&gt;ayudar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
            &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;No&lt;/span&gt; &lt;span class="n"&gt;respondas&lt;/span&gt; &lt;span class="n"&gt;preguntas&lt;/span&gt; &lt;span class="n"&gt;fuera&lt;/span&gt; &lt;span class="n"&gt;del&lt;/span&gt; &lt;span class="n"&gt;contexto&lt;/span&gt; &lt;span class="n"&gt;del&lt;/span&gt; &lt;span class="n"&gt;sistema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
            &lt;span class="s"&gt;""";
&lt;/span&gt;
        &lt;span class="n"&gt;_agent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;AzureOpenAIClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Uri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClientModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ApiKeyCredential&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetChatClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deploymentName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsIChatClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsAIAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;instructions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;instructions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"SupportAgent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;AIFunctionFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;docTool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetDocumentation&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
            &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;AIAgent&lt;/span&gt; &lt;span class="n"&gt;Agent&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_agent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El flujo de construcción es una cadena: &lt;code&gt;AzureOpenAIClient&lt;/code&gt; → &lt;code&gt;GetChatClient()&lt;/code&gt; → &lt;code&gt;AsIChatClient()&lt;/code&gt; → &lt;code&gt;AsAIAgent()&lt;/code&gt;. El último paso es donde se convierten las tools registradas y las instrucciones en la configuración que el agente usará en cada conversación.&lt;/p&gt;

&lt;p&gt;Nótese que el agente se construye una sola vez y se registra como singleton. Como es stateless, puede atender todas las conversaciones concurrentes sin problema — el estado de cada conversación vive en su propia &lt;code&gt;AgentSession&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Manejo de Sesiones: InMemorySessionStore
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Collections.Concurrent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Text.Json&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;SupportBot.Sessions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;InMemorySessionStore&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;ConcurrentDictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;JsonElement&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_sessions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;TryGetSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="n"&gt;JsonElement&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_sessions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;SaveSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;JsonElement&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_sessions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;DeleteSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_sessions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryRemove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El store guarda sesiones como &lt;code&gt;JsonElement&lt;/code&gt; — el formato en que MAF las serializa nativamente. &lt;code&gt;ConcurrentDictionary&lt;/code&gt; garantiza thread-safety sin necesidad de locks manuales. Para producción, esto se reemplazaría por Redis o una base de datos, pero la interfaz del store no cambiaría.&lt;/p&gt;

&lt;h3&gt;
  
  
  Los Endpoints: Program.cs
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Text.Json&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Agents.AI&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;SupportBot.Agents&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;SupportBot.Models&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;SupportBot.Sessions&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;WebApplication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SupportAgentFactory&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;InMemorySessionStore&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/chat"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ChatRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;SupportAgentFactory&lt;/span&gt; &lt;span class="n"&gt;agentFactory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;InMemorySessionStore&lt;/span&gt; &lt;span class="n"&gt;sessionStore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SessionId&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BadRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SessionId es requerido."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BadRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Message es requerido."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;agentFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;AgentSession&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sessionStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SessionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;serializedSession&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DeserializeSessionAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;serializedSession&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateSessionAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RunAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;updatedSession&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SerializeSessionAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;sessionStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SessionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;updatedSession&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ChatResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SessionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapDelete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/chat/{sessionId}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;InMemorySessionStore&lt;/span&gt; &lt;span class="n"&gt;sessionStore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;sessionStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DeleteSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NoContent&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El endpoint &lt;code&gt;POST /chat&lt;/code&gt; sigue un patrón claro: cargar o crear sesión → ejecutar el agente → serializar y guardar la sesión actualizada → retornar la respuesta. El &lt;code&gt;sessionId&lt;/code&gt; lo genera el cliente (puede ser un GUID) y lo manda en cada request para mantener el hilo de la conversación.&lt;/p&gt;

&lt;p&gt;Un detalle de la versión RC1: &lt;code&gt;SerializeSessionAsync&lt;/code&gt; es async, a diferencia de lo que indica la documentación inicial del framework.&lt;/p&gt;

&lt;h3&gt;
  
  
  El agente en acción
&lt;/h3&gt;

&lt;p&gt;Con la API corriendo, así se ve una conversación multi-turn real:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Primer mensaje:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;POST&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;/chat&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sessionId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"usr-42"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hola, no puedo entrar al sistema, olvidé mi contraseña"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sessionId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"usr-42"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reply"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"¡Hola! Para resetear tu contraseña sigue estos pasos: ve a la pantalla de login y haz clic en '¿Olvidaste tu contraseña?', ingresa tu correo corporativo y recibirás un enlace válido por 24 horas. Si no recibes el correo en 10 minutos, revisa tu carpeta de spam o contacta a TI."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Mensaje de seguimiento (mismo sessionId):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;POST&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;/chat&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sessionId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"usr-42"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"¿Y si tampoco recuerdo mi correo corporativo?"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sessionId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"usr-42"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reply"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"En ese caso, la cuenta puede quedar bloqueada tras 5 intentos fallidos. Te recomiendo contactar directamente al equipo de soporte en soporte@empresa.com o llamar al ext. 100 para que puedan verificar tu identidad y ayudarte a recuperar el acceso."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El agente recuerda que estamos hablando de un problema de acceso y responde en contexto, sin necesidad de que el usuario repita la situación.&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Cuándo agregarías Workflows aquí?
&lt;/h3&gt;

&lt;p&gt;En este ejemplo los Workflows no añaden valor — el flujo conversacional es impredecible por naturaleza y el agente lo maneja bien solo. Pero hay escenarios donde sí tendría sentido introducirlos:&lt;/p&gt;

&lt;p&gt;Si quisieras &lt;strong&gt;clasificar automáticamente la intención&lt;/strong&gt; antes de responder (¿es una pregunta de accesos, facturación o reportes?) y enrutar a agentes especializados según el tema, un Workflow con un executor de clasificación seguido de ejecutores especializados sería la arquitectura correcta.&lt;/p&gt;

&lt;p&gt;Si quisieras &lt;strong&gt;escalar a un humano&lt;/strong&gt; cuando el agente no encuentra respuesta, un Workflow con un paso de "human-in-the-loop" integrado en el grafo lo haría de forma limpia y auditable.&lt;/p&gt;

&lt;p&gt;La regla es simple: si puedes predecir los pasos, usa un Workflow. Si la conversación es abierta e impredecible, deja que el agente decida.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusión
&lt;/h2&gt;

&lt;p&gt;Microsoft Agent Framework simplifica genuinamente la construcción de agentes conversacionales en .NET. La abstracción de &lt;code&gt;AIAgent&lt;/code&gt;, el manejo de sesiones serializable y el sistema de tools via atributos &lt;code&gt;[Description]&lt;/code&gt; permiten tener algo funcional con muy poco código de infraestructura — lo que queda es lógica de negocio real.&lt;/p&gt;

&lt;p&gt;El hecho de que aún esté en preview se nota en algunos detalles: la API cambia entre versiones (como el caso de &lt;code&gt;SerializeSessionAsync&lt;/code&gt; que en RC1 es async), y la documentación a veces no refleja el estado actual del código. Dicho esto, para proyectos nuevos donde el timeline lo permite, ya vale la pena apostar por él en lugar de Semantic Kernel — es el camino hacia adelante según el propio equipo de Microsoft.&lt;/p&gt;

&lt;p&gt;Los siguientes pasos naturales para este proyecto serían reemplazar el &lt;code&gt;InMemorySessionStore&lt;/code&gt; por Redis o SQL Server para persistencia real, agregar autenticación al endpoint, e integrar un frontend — pero eso es material para otro artículo.&lt;/p&gt;




&lt;h2&gt;
  
  
  Referencias
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://learn.microsoft.com/en-us/agent-framework/overview/agent-framework-overview" rel="noopener noreferrer"&gt;Microsoft Agent Framework Overview&lt;/a&gt; — Microsoft Learn&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://learn.microsoft.com/en-us/agent-framework/get-started/" rel="noopener noreferrer"&gt;Get Started: Steps 1–5&lt;/a&gt; — Microsoft Learn&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/microsoft/agent-framework" rel="noopener noreferrer"&gt;Repositorio oficial con samples&lt;/a&gt; — GitHub&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://devblogs.microsoft.com/semantic-kernel/semantic-kernel-and-microsoft-agent-framework/" rel="noopener noreferrer"&gt;Anuncio oficial: Semantic Kernel y Microsoft Agent Framework&lt;/a&gt; — Microsoft Dev Blog&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://learn.microsoft.com/en-us/agent-framework/migration-guide/from-semantic-kernel/" rel="noopener noreferrer"&gt;Migration Guide from Semantic Kernel&lt;/a&gt; — Microsoft Learn&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>dotnet</category>
      <category>csharp</category>
      <category>aspnetcore</category>
    </item>
    <item>
      <title>Sistema de Control de Jobs en Tiempo Real con Channels y Background Services en .NET</title>
      <dc:creator>Isaac Ojeda</dc:creator>
      <pubDate>Sat, 01 Nov 2025 22:42:58 +0000</pubDate>
      <link>https://dev.to/isaacojeda/sistema-de-control-de-jobs-en-tiempo-real-con-channels-y-background-services-en-net-529e</link>
      <guid>https://dev.to/isaacojeda/sistema-de-control-de-jobs-en-tiempo-real-con-channels-y-background-services-en-net-529e</guid>
      <description>&lt;h2&gt;
  
  
  Introducción
&lt;/h2&gt;

&lt;p&gt;En el desarrollo moderno de aplicaciones, es común necesitar ejecutar &lt;strong&gt;procesos en segundo plano&lt;/strong&gt; que se comuniquen con nuestra API de forma eficiente y segura. Tradicionalmente, esto se resolvía con implementaciones complejas usando locks, colas manuales o infraestructura externa como RabbitMQ. Sin embargo, .NET ofrece una solución simple pero elegante: &lt;strong&gt;System.Threading.Channels&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;En este artículo, exploraremos cómo construir un sistema de control de jobs en tiempo real utilizando:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔧 &lt;strong&gt;Channels&lt;/strong&gt; para comunicación thread-safe entre componentes&lt;/li&gt;
&lt;li&gt;🔄 &lt;strong&gt;Background Services&lt;/strong&gt; para tareas recurrentes&lt;/li&gt;
&lt;li&gt;🚀 &lt;strong&gt;Minimal APIs&lt;/strong&gt; para endpoints modernos y limpios&lt;/li&gt;
&lt;li&gt;⚡ &lt;strong&gt;TaskCompletionSource&lt;/strong&gt; para comunicación bidireccional&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Al finalizar, tendrás un proyecto funcional que puedes adaptar para casos de uso reales como procesamiento de emails, análisis de imágenes, generación de reportes, y más.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Nota: El código fuente siempre lo encontrarás en mi github -&amp;gt; &lt;a href="https://github.com/isaacOjeda/DevToPosts/tree/main/ApiBackgroundChannels" rel="noopener noreferrer"&gt;DevToPosts/ApiBackgroundChannels at main · isaacOjeda/DevToPosts&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  ¿Qué son los Channels?
&lt;/h2&gt;

&lt;p&gt;Los &lt;strong&gt;Channels&lt;/strong&gt; en .NET son estructuras de datos thread-safe diseñadas para escenarios &lt;strong&gt;productor-consumidor&lt;/strong&gt;. Piensa en ellos como una "tubería" donde un lado escribe datos y el otro los lee, sin preocuparte por locks o sincronización manual.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Por qué usarlos?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Thread-safe por diseño&lt;/li&gt;
&lt;li&gt;✅ Alta performance con bajo overhead&lt;/li&gt;
&lt;li&gt;✅ Backpressure integrado (control de flujo)&lt;/li&gt;
&lt;li&gt;✅ Ideal para comunicación entre hilos/tareas&lt;/li&gt;
&lt;li&gt;✅ Alternativa simple a colas externas (RabbitMQ, Redis) para escenarios internos&lt;/li&gt;
&lt;li&gt;✅ Optimizado para async/await (usa &lt;code&gt;ValueTask&lt;/code&gt; internamente)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Arquitectura del Proyecto
&lt;/h2&gt;

&lt;p&gt;Este proyecto demuestra cómo controlar un &lt;strong&gt;Background Job&lt;/strong&gt; desde una API usando Channels para comunicación bidireccional:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────────┐         ┌─────────┐         ┌──────────────────┐
│  API Request │ ──────&amp;gt; │ Channel │ ──────&amp;gt; │ Background Job   │
│  (Productor) │         │ (Cola)  │         │ (Consumidor)     │
└──────────────┘         └─────────┘         └──────────────────┘
      ↑                                             │
      └────── TaskCompletionSource ─────────────────┘
                     (Respuesta)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Paso 1: Definir el Modelo de Comunicación
&lt;/h2&gt;

&lt;p&gt;Primero, necesitamos estructuras para enviar comandos y recibir respuestas:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;CommandType&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Stop&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;GetStatus&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;JobCommand&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;CommandType&lt;/span&gt; &lt;span class="n"&gt;Type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;TaskCompletionSource&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;JobStatus&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;?&lt;/span&gt; &lt;span class="n"&gt;ResponseTask&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;JobStatus&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;IsRunning&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;ExecutionCount&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;LastExecutionTime&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Message&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;💡 Clave:&lt;/strong&gt; &lt;code&gt;TaskCompletionSource&lt;/code&gt; nos permite crear una Task que completaremos manualmente cuando tengamos la respuesta, haciendo posible la comunicación bidireccional.&lt;/p&gt;

&lt;h2&gt;
  
  
  Paso 2: Crear el Background Service (Consumidor)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;JobProcessor&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BackgroundService&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;Channel&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;JobCommand&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_channel&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;_isJobRunning&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;_executionCount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;ExecuteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;stoppingToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"JobProcessor iniciado. Esperando comandos..."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// ✅ Patrón recomendado por Microsoft: WaitToReadAsync + TryRead&lt;/span&gt;
        &lt;span class="c1"&gt;// Más eficiente que ReadAllAsync para alta concurrencia&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WaitToReadAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stoppingToken&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryRead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;try&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;ProcessCommandAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stoppingToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;_logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Error procesando comando"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                    &lt;span class="c1"&gt;// ✅ Notificar errores al productor&lt;/span&gt;
                    &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResponseTask&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;TrySetException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;ProcessCommandAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JobCommand&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;CommandType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;_isJobRunning&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;RunRecurringJobAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

                &lt;span class="c1"&gt;// Enviar respuesta al productor&lt;/span&gt;
                &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResponseTask&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;SetResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;JobStatus&lt;/span&gt; 
                &lt;span class="p"&gt;{&lt;/span&gt; 
                    &lt;span class="n"&gt;IsRunning&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
                    &lt;span class="n"&gt;Message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Job iniciado"&lt;/span&gt; 
                &lt;span class="p"&gt;});&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;CommandType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;_isJobRunning&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResponseTask&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;SetResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;JobStatus&lt;/span&gt; 
                &lt;span class="p"&gt;{&lt;/span&gt; 
                    &lt;span class="n"&gt;IsRunning&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
                    &lt;span class="n"&gt;Message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Job detenido"&lt;/span&gt; 
                &lt;span class="p"&gt;});&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;CommandType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetStatus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResponseTask&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;SetResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;JobStatus&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;IsRunning&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_isJobRunning&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;ExecutionCount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_executionCount&lt;/span&gt;
                &lt;span class="p"&gt;});&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;WaitToReadAsync()&lt;/code&gt; + &lt;code&gt;TryRead()&lt;/code&gt;&lt;/strong&gt;: Patrón recomendado por Microsoft para mejor performance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;TrySetException()&lt;/code&gt;&lt;/strong&gt;: Propaga errores al productor de forma segura&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bucle anidado&lt;/strong&gt;: Procesa múltiples comandos en batch cuando están disponibles&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Paso 3: Configurar el Channel y el Servicio
&lt;/h2&gt;

&lt;p&gt;En &lt;code&gt;Program.cs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✅ Bounded Channel con opciones optimizadas (recomendado por Microsoft)&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddSingleton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreateBounded&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;JobCommand&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;BoundedChannelOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;FullMode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BoundedChannelFullMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Wait&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Backpressure automático&lt;/span&gt;
        &lt;span class="n"&gt;SingleWriter&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Múltiples endpoints pueden escribir&lt;/span&gt;
        &lt;span class="n"&gt;SingleReader&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;   &lt;span class="c1"&gt;// Solo un BackgroundService consume&lt;/span&gt;
    &lt;span class="p"&gt;}));&lt;/span&gt;

&lt;span class="c1"&gt;// Registrar el Background Service&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddHostedService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;JobProcessor&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;💡 ¿Bounded vs Unbounded?&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Característica&lt;/th&gt;
&lt;th&gt;Unbounded&lt;/th&gt;
&lt;th&gt;Bounded&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Capacidad&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Ilimitada&lt;/td&gt;
&lt;td&gt;Limitada (configurable)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Memoria&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Puede crecer sin control&lt;/td&gt;
&lt;td&gt;Controlada&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Backpressure&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Sí (automático)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Uso recomendado&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Productores lentos&lt;/td&gt;
&lt;td&gt;Productores rápidos&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Performance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Writes síncronos&lt;/td&gt;
&lt;td&gt;Writes pueden ser async&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Modos de Bounded Channel (&lt;code&gt;FullMode&lt;/code&gt;):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Wait&lt;/code&gt;&lt;/strong&gt; (recomendado): Espera hasta que haya espacio (backpressure)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;DropWrite&lt;/code&gt;&lt;/strong&gt;: Descarta el nuevo elemento&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;DropOldest&lt;/code&gt;&lt;/strong&gt;: Descarta el elemento más antiguo&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;DropNewest&lt;/code&gt;&lt;/strong&gt;: Descarta el elemento más reciente&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Opciones de optimización:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;SingleWriter&lt;/code&gt;&lt;/strong&gt;: &lt;code&gt;true&lt;/code&gt; = mejor performance si solo un productor escribe&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;SingleReader&lt;/code&gt;&lt;/strong&gt;: &lt;code&gt;true&lt;/code&gt; = mejor performance si solo un consumidor lee&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;AllowSynchronousContinuations&lt;/code&gt;&lt;/strong&gt;: &lt;code&gt;false&lt;/code&gt; (default) para evitar bloqueos&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Paso 4: Crear los Endpoints (Productores)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;IEndpointRouteBuilder&lt;/span&gt; &lt;span class="nf"&gt;MapJobEndpoints&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="n"&gt;IEndpointRouteBuilder&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;jobGroup&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapGroup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"api/job"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;jobGroup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"start"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Channel&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;JobCommand&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Crear TaskCompletionSource para esperar la respuesta&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;tcs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;TaskCompletionSource&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;JobStatus&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;JobCommand&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CommandType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;ResponseTask&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tcs&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;

        &lt;span class="c1"&gt;// ✅ WriteAsync maneja backpressure automáticamente&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Esperar respuesta del consumidor&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;tcs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="c1"&gt;// Endpoints similares para stop y status...&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;💡 Flujo:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;API recibe request → Crea &lt;code&gt;TaskCompletionSource&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Escribe comando en el Channel con &lt;code&gt;WriteAsync()&lt;/code&gt; (maneja backpressure)&lt;/li&gt;
&lt;li&gt;Espera que el consumidor complete la Task&lt;/li&gt;
&lt;li&gt;Retorna la respuesta al cliente&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  ¿Por qué este patrón?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Sin Channels:
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Locks manuales, propenso a errores&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt; &lt;span class="n"&gt;_lock&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_queue&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;AddCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Command&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_lock&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;_queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Con Channels:
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✅ Thread-safe automático, limpio, con backpressure&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Casos de Uso Reales con Channels
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. &lt;strong&gt;Cola de Emails/Notificaciones&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Microsoft recomienda Bounded para prevenir OutOfMemory&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;emailChannel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreateBounded&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;EmailMessage&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;BoundedChannelOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;FullMode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BoundedChannelFullMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Wait&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// API recibe requests → Encola en Channel → Background envía emails en lotes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. &lt;strong&gt;Procesamiento de Imágenes&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Channel&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ImageProcessingJob&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;imageChannel&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// Upload de imágenes → Channel → Worker redimensiona/optimiza en background&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. &lt;strong&gt;Logs Centralizados&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// DropOldest para logs: si está lleno, descarta los más antiguos&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;logChannel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreateBounded&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;LogEntry&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;BoundedChannelOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;FullMode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BoundedChannelFullMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DropOldest&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. &lt;strong&gt;Rate Limiting / Throttling&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;boundedChannel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreateBounded&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// Limita a 100 requests concurrentes, el resto espera (backpressure)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. &lt;strong&gt;Event Sourcing Interno&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Channel&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;DomainEvent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;eventChannel&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// Eventos de dominio → Channel → Múltiples handlers procesan en paralelo&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  6. &lt;strong&gt;Pipeline de Datos (ejemplo oficial de Microsoft)&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Patrón de processing pipeline con múltiples stages&lt;/span&gt;
&lt;span class="n"&gt;Channel&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;RawData&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;inputChannel&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;Channel&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ProcessedData&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;outputChannel&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// Stage 1: Raw → Validated → Stage 2: Validated → Enriched&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Usa Channels cuando:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Comunicación dentro de la misma aplicación&lt;/li&gt;
&lt;li&gt;✅ Necesitas alta performance y bajo latency&lt;/li&gt;
&lt;li&gt;✅ Quieres simplicidad sin infraestructura externa&lt;/li&gt;
&lt;li&gt;✅ Trabajas con async/await&lt;/li&gt;
&lt;li&gt;✅ Necesitas backpressure automático&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Usa Queue externo (RabbitMQ/Azure Service Bus) cuando:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;❌ Necesitas comunicación entre múltiples aplicaciones/servicios&lt;/li&gt;
&lt;li&gt;❌ Requieres persistencia de mensajes&lt;/li&gt;
&lt;li&gt;❌ Necesitas escalabilidad horizontal&lt;/li&gt;
&lt;li&gt;❌ Requieres garantías de entrega (at-least-once, exactly-once)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  ¿Por qué usar Channels?
&lt;/h3&gt;

&lt;p&gt;A lo largo de este tutorial, hemos visto cómo Channels ofrece ventajas significativas:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Simplicidad&lt;/strong&gt;: No necesitas infraestructura externa para empezar&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance&lt;/strong&gt;: Diseñados desde cero para async/await con &lt;code&gt;ValueTask&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seguridad&lt;/strong&gt;: Thread-safe por diseño, sin preocupaciones por race conditions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Control&lt;/strong&gt;: Backpressure automático previene sobrecarga del sistema&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flexibilidad&lt;/strong&gt;: Configuración granular según tus necesidades específicas&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Impacto en tu arquitectura
&lt;/h3&gt;

&lt;p&gt;Este patrón es especialmente valioso cuando:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Estás construyendo &lt;strong&gt;aplicaciones monolíticas modernas&lt;/strong&gt; que necesitan procesamiento asíncrono&lt;/li&gt;
&lt;li&gt;Quieres &lt;strong&gt;reducir costos&lt;/strong&gt; de infraestructura eliminando dependencias de message brokers&lt;/li&gt;
&lt;li&gt;Necesitas &lt;strong&gt;optimizar performance&lt;/strong&gt; con procesamiento en memoria&lt;/li&gt;
&lt;li&gt;Buscas &lt;strong&gt;simplicidad operacional&lt;/strong&gt; sin sacrificar escalabilidad vertical&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Los Channels de .NET demuestran que no siempre necesitas herramientas complejas para resolver problemas complejos. A veces, la solución más elegante es la que viene incorporada en tu framework.&lt;/p&gt;

&lt;h2&gt;
  
  
  Próximos Pasos
&lt;/h2&gt;

&lt;p&gt;¿Listo para llevar este conocimiento al siguiente nivel? Aquí tienes algunas ideas para expandir este proyecto:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. &lt;strong&gt;Implementar Múltiples Consumidores&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Escalar procesamiento con múltiples workers&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddHostedService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;JobProcessor&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Worker 1&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddHostedService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;JobProcessor&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Worker 2&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddHostedService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;JobProcessor&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Worker 3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Aprenderás:&lt;/strong&gt; Paralelización, distribución de carga, sincronización entre workers&lt;/p&gt;

&lt;h3&gt;
  
  
  2. &lt;strong&gt;Integrar Observabilidad&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Métricas con System.Diagnostics.Metrics&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;meter&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Meter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"BackgroundJobs"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;jobsProcessed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;meter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreateCounter&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;long&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"jobs_processed"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;queueDepth&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;meter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateObservableGauge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"queue_depth"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Aprenderás:&lt;/strong&gt; OpenTelemetry, métricas personalizadas, dashboards con Grafana/Prometheus&lt;/p&gt;

&lt;h3&gt;
  
  
  3. &lt;strong&gt;Implementar Persistencia&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Guardar estado en caso de restart&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PersistentJobProcessor&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BackgroundService&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;IJobStateRepository&lt;/span&gt; &lt;span class="n"&gt;_repository&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;ExecuteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Recuperar jobs pendientes al iniciar&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RestorePendingJobsAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="c1"&gt;// ... continuar procesamiento normal&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Aprenderás:&lt;/strong&gt; State management, recovery strategies, durabilidad&lt;/p&gt;

&lt;h3&gt;
  
  
  4. &lt;strong&gt;Agregar Pipeline de Procesamiento&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Pipeline multi-etapa&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;rawChannel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreateBounded&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;RawData&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;validatedChannel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreateBounded&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ValidatedData&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;enrichedChannel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreateBounded&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;EnrichedData&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Stage 1: Validación&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddHostedService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ValidationProcessor&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// Stage 2: Enriquecimiento&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddHostedService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;EnrichmentProcessor&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// Stage 3: Persistencia&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddHostedService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PersistenceProcessor&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Aprenderás:&lt;/strong&gt; Pipeline pattern, ETL processes, data transformation&lt;/p&gt;

&lt;h3&gt;
  
  
  5. &lt;strong&gt;Implementar Rate Limiting Avanzado&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Rate limiter con ventanas deslizantes&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RateLimitedJobProcessor&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BackgroundService&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;RateLimiter&lt;/span&gt; &lt;span class="n"&gt;_rateLimiter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;ExecuteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WaitToReadAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;lease&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_rateLimiter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AcquireAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lease&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsAcquired&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="c1"&gt;// Procesar job&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Aprenderás:&lt;/strong&gt; Rate limiting patterns, token bucket, leaky bucket&lt;/p&gt;

&lt;h3&gt;
  
  
  6. &lt;strong&gt;Crear Dashboard de Monitoreo&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// SignalR para updates en tiempo real&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddSignalR&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Notificar estado a clientes conectados&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_hubContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Clients&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;All&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SendAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"JobStatusUpdate"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;QueueDepth&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ProcessingRate&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jobsPerSecond&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ActiveJobs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;activeJobCount&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Aprenderás:&lt;/strong&gt; Real-time updates, SignalR, live dashboards&lt;/p&gt;

&lt;h3&gt;
  
  
  7. &lt;strong&gt;Añadir Resiliencia&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Polly para retry policies&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;retryPolicy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Policy&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WaitAndRetryAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retryAttempt&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; 
        &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retryAttempt&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;retryPolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExecuteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; 
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;ProcessJobAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Aprenderás:&lt;/strong&gt; Retry patterns, circuit breakers, fallback strategies&lt;/p&gt;

&lt;h3&gt;
  
  
  8. &lt;strong&gt;Implementar Health Checks&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Health check para el channel&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddHealthChecks&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddCheck&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ChannelHealthCheck&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"channel_health"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddCheck&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;JobProcessorHealthCheck&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"job_processor_health"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ChannelHealthCheck&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IHealthCheck&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;HealthCheckResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;CheckHealthAsync&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;queueDepth&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_channel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;queueDepth&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;1000&lt;/span&gt; 
            &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;HealthCheckResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Healthy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; 
            &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;HealthCheckResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Degraded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Queue depth high"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Aprenderás:&lt;/strong&gt; Health monitoring, readiness/liveness probes, Kubernetes integration&lt;/p&gt;

&lt;h3&gt;
  
  
  9. &lt;strong&gt;Migrar a Arquitectura Distribuida&lt;/strong&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Cuando crezcas más allá de un solo servidor&lt;/span&gt;
&lt;span class="c1"&gt;// Considera migrar a:&lt;/span&gt;
&lt;span class="c1"&gt;// - Azure Service Bus para messaging distribuido&lt;/span&gt;
&lt;span class="c1"&gt;// - Azure Queue Storage para simplicidad y bajo costo&lt;/span&gt;
&lt;span class="c1"&gt;// - RabbitMQ para control total&lt;/span&gt;
&lt;span class="c1"&gt;// - Redis Streams para alta performance&lt;/span&gt;

&lt;span class="c1"&gt;// Mantén la misma interfaz, cambia la implementación&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IJobQueue&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;EnqueueAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JobCommand&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;JobCommand&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;DequeueAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Implementación con Channels (actual)&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;InMemoryJobQueue&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IJobQueue&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Implementación con Azure Service Bus (futuro)&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ServiceBusJobQueue&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IJobQueue&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Aprenderás:&lt;/strong&gt; Estrategias de migración, abstracciones, arquitectura evolutiva&lt;/p&gt;

&lt;h3&gt;
  
  
  Recursos para Continuar Aprendiendo
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Documentación Oficial:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/dotnet/api/system.threading.channels" rel="noopener noreferrer"&gt;System.Threading.Channels API Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-9.0&amp;amp;tabs=visual-studio" rel="noopener noreferrer"&gt;Background tasks with hosted services in ASP.NET Core | Microsoft Learn&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/dotnet/core/extensions/channels" rel="noopener noreferrer"&gt;Channels Library Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/aspnet/core/fundamentals/host/hosted-services" rel="noopener noreferrer"&gt;Background Services in ASP.NET Core&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Artículos Avanzados:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://devblogs.microsoft.com/dotnet/an-introduction-to-system-threading-channels/" rel="noopener noreferrer"&gt;An Introduction to System.Threading.Channels&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/dotnet/standard/parallel-programming/how-to-implement-a-producer-consumer-dataflow-pattern" rel="noopener noreferrer"&gt;Producer/Consumer Patterns with TPL Dataflow&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>dotnet</category>
      <category>architecture</category>
      <category>spanish</category>
      <category>csharp</category>
    </item>
    <item>
      <title>[Parte 4] Construyendo un Agente Conversacional con el Agent Framework de Semantic Kernel</title>
      <dc:creator>Isaac Ojeda</dc:creator>
      <pubDate>Sat, 19 Jul 2025 18:56:35 +0000</pubDate>
      <link>https://dev.to/isaacojeda/parte-4-construyendo-un-agente-conversacional-con-el-agent-framework-de-semantic-kernel-2nn8</link>
      <guid>https://dev.to/isaacojeda/parte-4-construyendo-un-agente-conversacional-con-el-agent-framework-de-semantic-kernel-2nn8</guid>
      <description>&lt;h2&gt;
  
  
  🧠 Crea tu propio agente conversacional con Semantic Kernel
&lt;/h2&gt;

&lt;p&gt;¿Te imaginas construir un asistente conversacional que entienda lenguaje natural y, además, sea capaz de ejecutar funciones reales de tu sistema? Con el &lt;strong&gt;Agent Framework de Semantic Kernel&lt;/strong&gt;, eso ya no es solo posible, sino sorprendentemente sencillo.&lt;/p&gt;

&lt;p&gt;En este tutorial te voy a mostrar cómo crear un &lt;strong&gt;agente inteligente especializado en gestión de facturas&lt;/strong&gt;, diseñado para ayudar a abogados en una notaría pública. Este agente no solo entiende lo que le pides, sino que también puede:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Consultar el estado de una factura.&lt;/li&gt;
&lt;li&gt;Crear prefacturas automáticamente.&lt;/li&gt;
&lt;li&gt;Ver facturas vencidas.&lt;/li&gt;
&lt;li&gt;Marcar facturas como pagadas.&lt;/li&gt;
&lt;li&gt;Obtener información detallada de un cliente.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Todo esto lo logra invocando funciones reales en C#, conectadas a una base de datos real y expuestas mediante un plugin personalizado.&lt;/p&gt;

&lt;p&gt;Durante el artículo veremos paso a paso:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cómo configurar Semantic Kernel con Azure OpenAI.&lt;/li&gt;
&lt;li&gt;Cómo definir tus propias funciones con &lt;code&gt;[KernelFunction]&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Cómo crear un &lt;code&gt;ChatCompletionAgent&lt;/code&gt; con instrucciones personalizadas.&lt;/li&gt;
&lt;li&gt;Cómo ejecutar al agente dentro de una API HTTP.&lt;/li&gt;
&lt;li&gt;Y cómo mantener conversaciones con historial persistente.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Además, el proyecto incluye un cliente web de ejemplo (tipo chat) para que puedas probarlo todo en vivo y adaptar la solución a tus propios casos de uso.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🔗 Todo el código está disponible en este repositorio:&lt;br&gt;&lt;br&gt;
&lt;a href="https://github.com/isaacOjeda/DevToPosts/tree/main/SemanticKernelSeries/SemanticKernelLearning04" rel="noopener noreferrer"&gt;github.com/isaacOjeda/DevToPosts/tree/main/SemanticKernelSeries/SemanticKernelLearning04&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc7f8juqe3rrci13y7v6c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc7f8juqe3rrci13y7v6c.png" alt=" " width="800" height="791"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🧠 ¿Qué es el Agent Framework en Semantic Kernel?
&lt;/h2&gt;

&lt;p&gt;El &lt;strong&gt;Agent Framework&lt;/strong&gt; es una de las capacidades más potentes introducidas en las versiones recientes de &lt;a href="https://github.com/microsoft/semantic-kernel" rel="noopener noreferrer"&gt;Semantic Kernel&lt;/a&gt;: un sistema que permite &lt;strong&gt;crear agentes conversacionales inteligentes&lt;/strong&gt; con la capacidad de razonar, planificar e invocar funciones definidas por el desarrollador para lograr un objetivo.&lt;/p&gt;

&lt;p&gt;A diferencia de un chatbot tradicional basado en reglas, un agente en Semantic Kernel:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Comprende instrucciones en lenguaje natural.&lt;/li&gt;
&lt;li&gt;Decide qué acciones tomar en base al contexto disponible.&lt;/li&gt;
&lt;li&gt;Invoca funciones (también llamadas “skills” o “plugins”) que tú defines en código.&lt;/li&gt;
&lt;li&gt;Puede encadenar múltiples pasos de razonamiento para llegar a un resultado.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Este modelo es ideal para construir asistentes especializados con conocimientos de negocio y herramientas personalizadas.&lt;/p&gt;

&lt;h3&gt;
  
  
  🎯 Caso de uso: un asistente de facturación
&lt;/h3&gt;

&lt;p&gt;En este tutorial vamos a construir un ejemplo real usando el Agent Framework: un &lt;strong&gt;asistente conversacional para abogados en una notaría pública&lt;/strong&gt;, cuya función es &lt;strong&gt;gestionar facturas&lt;/strong&gt; mediante lenguaje natural.&lt;/p&gt;

&lt;p&gt;El asistente será capaz de:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Verificar si una factura está pagada o vencida.&lt;/li&gt;
&lt;li&gt;Crear prefacturas para clientes nuevos.&lt;/li&gt;
&lt;li&gt;Consultar el listado de facturas pendientes de pago.&lt;/li&gt;
&lt;li&gt;Marcar una factura como pagada.&lt;/li&gt;
&lt;li&gt;Obtener información detallada de un cliente y su historial de facturación.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Todo esto será posible gracias a una capa de funciones en C# expuestas al agente mediante anotaciones &lt;code&gt;[KernelFunction]&lt;/code&gt;, y alimentadas por un servicio de backend que accede a la base de datos.&lt;/p&gt;

&lt;h3&gt;
  
  
  🧬 ¿Por qué usar agentes en lugar de comandos o controladores tradicionales?
&lt;/h3&gt;

&lt;p&gt;Con el Agent Framework:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;El usuario no necesita conocer los comandos disponibles.&lt;/li&gt;
&lt;li&gt;El agente elige por sí mismo la función que debe usar.&lt;/li&gt;
&lt;li&gt;Puedes cambiar o extender el comportamiento del sistema sin modificar el frontend.&lt;/li&gt;
&lt;li&gt;La experiencia es más natural, fluida y adaptable a distintos usuarios.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Esto hace que construir asistentes LLM especializados con Semantic Kernel sea una excelente opción para &lt;strong&gt;interfaces inteligentes&lt;/strong&gt; sobre sistemas de negocio existentes, especialmente en entornos con operaciones rutinarias, como la gestión de facturas.&lt;/p&gt;

&lt;h2&gt;
  
  
  ⚙️ Requisitos previos y setup del entorno
&lt;/h2&gt;

&lt;p&gt;Antes de construir nuestro asistente de facturación, necesitamos preparar nuestro proyecto con las dependencias necesarias y configurar el acceso a un modelo de lenguaje compatible (en este caso, Azure OpenAI).&lt;/p&gt;

&lt;p&gt;Este ejemplo está construido sobre .NET 9, con soporte para SQLite como base de datos local, y aprovecha la integración entre Semantic Kernel y Azure OpenAI.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🗂️ &lt;strong&gt;Puedes consultar el código completo y funcional de este proyecto en el repositorio:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
🔗 &lt;a href="https://github.com/isaacOjeda/DevToPosts/tree/main/SemanticKernelSeries/SemanticKernelLearning04" rel="noopener noreferrer"&gt;github.com/isaacOjeda/DevToPosts/tree/main/SemanticKernelSeries/SemanticKernelLearning04&lt;/a&gt;&lt;br&gt;&lt;br&gt;
Allí encontrarás todos los archivos de configuración, modelos, servicios y ejemplos necesarios para correr este asistente de forma local o integrarlo en tu aplicación.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  📦 Paquetes necesarios
&lt;/h3&gt;

&lt;p&gt;El Agent Framework se encuentra dentro del paquete &lt;code&gt;Microsoft.SemanticKernel.Agents.Core&lt;/code&gt;, por lo que debes incluir explícitamente las dependencias en tu archivo &lt;code&gt;.csproj&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;ItemGroup&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.SemanticKernel"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"1.60.0"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.SemanticKernel.Agents.Core"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"1.60.0"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.AspNetCore.OpenApi"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"9.0.7"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.EntityFrameworkCore.Sqlite"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"9.0.0"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.EntityFrameworkCore.Design"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"9.0.0"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.VisualStudio.Azure.Containers.Tools.Targets"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"1.22.1"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/ItemGroup&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Esto nos da acceso al núcleo de Semantic Kernel, las capacidades de agentes, así como herramientas necesarias para la persistencia y desarrollo web.&lt;/p&gt;

&lt;h3&gt;
  
  
  🧪 Configurando el Kernel
&lt;/h3&gt;

&lt;p&gt;El kernel es el núcleo que conecta el modelo de lenguaje con tus plugins. En este ejemplo, usamos Azure OpenAI para procesar instrucciones en lenguaje natural. A continuación se muestra cómo configurar el kernel con los valores de &lt;code&gt;appsettings.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;IServiceCollection&lt;/span&gt; &lt;span class="nf"&gt;AddSemanticKernel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="n"&gt;IServiceCollection&lt;/span&gt; &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IConfiguration&lt;/span&gt; &lt;span class="n"&gt;configuration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;azureOpenAIConfig&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetSection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"AzureOpenAI"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddAzureOpenAIChatCompletion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;deploymentName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;azureOpenAIConfig&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"DeploymentName"&lt;/span&gt;&lt;span class="p"&gt;]!,&lt;/span&gt;
        &lt;span class="n"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;azureOpenAIConfig&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"ApiKey"&lt;/span&gt;&lt;span class="p"&gt;]!,&lt;/span&gt;
        &lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;azureOpenAIConfig&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Endpoint"&lt;/span&gt;&lt;span class="p"&gt;]!,&lt;/span&gt;
        &lt;span class="n"&gt;modelId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;azureOpenAIConfig&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"ModelId"&lt;/span&gt;&lt;span class="p"&gt;]!&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddTransient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;serviceProvider&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Kernel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;serviceProvider&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La configuración en &lt;code&gt;appsettings.json&lt;/code&gt; se vería algo así:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"AzureOpenAI"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"DeploymentName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"notaria-assistant"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ApiKey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TU_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Endpoint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://tuservicio.openai.azure.com/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ModelId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gpt-35-turbo"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Con esto, el &lt;code&gt;Kernel&lt;/code&gt; queda disponible vía inyección de dependencias y listo para clonar, personalizar o conectar a nuestros agentes.&lt;/p&gt;

&lt;h3&gt;
  
  
  🧱 Servicios base y modelos de dominio
&lt;/h3&gt;

&lt;p&gt;Nuestro agente necesita acceder a datos del mundo real, por lo que construimos un servicio &lt;code&gt;InvoiceService&lt;/code&gt; con métodos como:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;GetInvoiceByNumberAsync&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CreateInvoiceAsync&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MarkInvoiceAsPaidAsync&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GetUnpaidInvoicesAsync&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GetCustomerByEmailAsync&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Este servicio será inyectado en el plugin que veremos en la próxima sección, y expone la lógica de negocio que el agente puede usar para resolver instrucciones. Si ya tienes una capa de servicios en tu aplicación, puedes integrarla directamente.&lt;/p&gt;

&lt;h2&gt;
  
  
  🧩 Definir tus funciones (skills/plugins)
&lt;/h2&gt;

&lt;p&gt;Una de las capacidades más poderosas del Agent Framework es permitir que tus agentes ejecuten &lt;strong&gt;funciones reales escritas en C#&lt;/strong&gt;. Estas funciones se registran como "plugins" usando decoradores especiales, y luego el agente las puede invocar cuando detecta que son necesarias para cumplir una instrucción del usuario.&lt;/p&gt;

&lt;p&gt;En este caso, hemos creado un plugin llamado &lt;code&gt;InvoicesPlugin&lt;/code&gt;, que expone cinco funciones relacionadas con facturación. Cada una está decorada con el atributo &lt;code&gt;[KernelFunction]&lt;/code&gt;, lo que la hace visible para Semantic Kernel.&lt;/p&gt;

&lt;h3&gt;
  
  
  🧠 ¿Qué es un plugin?
&lt;/h3&gt;

&lt;p&gt;Un plugin en Semantic Kernel es simplemente una &lt;strong&gt;clase con métodos públicos asincrónicos&lt;/strong&gt; decorados con &lt;code&gt;[KernelFunction]&lt;/code&gt;. Además, puedes usar &lt;code&gt;[Description]&lt;/code&gt; en los parámetros y métodos para mejorar la comprensión del agente sobre lo que hace cada función.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🗂️ El código completo del plugin puede consultarse en el repositorio:&lt;br&gt;&lt;br&gt;
&lt;a href="https://github.com/isaacOjeda/DevToPosts/tree/main/SemanticKernelSeries/SemanticKernelLearning04" rel="noopener noreferrer"&gt;Ver InvoicesPlugin.cs en GitHub&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  🧾 Ejemplo 1: Verificar el estado de una factura
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;KernelFunction&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Verifica el estado de pago de una factura específica usando su número de factura."&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;VerifyPaymentAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Número de la factura a verificar (ej: INV-202412-0001)"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;numeroFactura&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;invoice&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;invoiceService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetInvoiceByNumberAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;numeroFactura&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invoice&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;$"❌ No se encontró ninguna factura con el número: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;numeroFactura&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;StringBuilder&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AppendLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"📋 **Factura: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InvoiceNumber&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;**"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AppendLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"🔸 Estado: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;GetStatusText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// ...otros campos visuales&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Nota:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
En este ejemplo, todas las funciones del plugin reciben y retornan datos en formato &lt;code&gt;string&lt;/code&gt;, ya que el enfoque está en mantener el ejemplo simple y fácil de seguir.&lt;br&gt;&lt;br&gt;
Sin embargo, el Agent Framework de Semantic Kernel &lt;strong&gt;soporta objetos complejos como parámetros de entrada y salida&lt;/strong&gt;. Puedes retornar clases personalizadas, listas u objetos anidados, y el agente será capaz de interpretarlos, formatearlos o incluso razonar sobre ellos en sus respuestas.&lt;/p&gt;

&lt;p&gt;En un escenario real, podrías devolver una &lt;code&gt;InvoiceDetail&lt;/code&gt; con propiedades estructuradas en lugar de una cadena formateada, lo que abre la puerta a integraciones más ricas o adaptadas al canal (por ejemplo, APIs, UIs o chatbots).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Este método se encarga de buscar una factura por número y devolver su estado, fecha de vencimiento, monto y otra información útil. Como puedes ver, el resultado se formatea para ser legible y amigable, incluyendo emojis para mejorar la experiencia.&lt;/p&gt;

&lt;h3&gt;
  
  
  ✍️ Ejemplo 2: Crear una prefactura
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;KernelFunction&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Realiza una prefactura para un cliente usando email, descripción, monto y días hasta el vencimiento."&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;CreateInvoiceDraftAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;clienteEmail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;descripcion&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;monto&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;diasVencimiento&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;invoiceService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetCustomerByEmailAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clienteEmail&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;$"❌ No se encontró ningún cliente con el email: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;clienteEmail&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;dueDate&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UtcNow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddDays&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;diasVencimiento&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;invoice&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;invoiceService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateInvoiceAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;descripcion&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;monto&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dueDate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Factura generada automáticamente"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;$"✅ Prefactura creada: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InvoiceNumber&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; para &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Este método genera una factura en estado "borrador", útil para cuando un abogado quiere adelantar una facturación para un cliente específico.&lt;/p&gt;

&lt;h3&gt;
  
  
  🧠 ¿Por qué usar &lt;code&gt;[KernelFunction]&lt;/code&gt;?
&lt;/h3&gt;

&lt;p&gt;Al decorar tus funciones con este atributo, estás dando al agente la capacidad de:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Descubrir qué herramientas tiene disponibles.&lt;/li&gt;
&lt;li&gt;Seleccionar de forma automática la función correcta en base a la intención del usuario.&lt;/li&gt;
&lt;li&gt;Combinar funciones si es necesario (por ejemplo, buscar cliente → generar factura).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Gracias a esto, no necesitas escribir prompts complejos ni reglas condicionales. El agente decide qué hacer en tiempo de ejecución.&lt;/p&gt;

&lt;h2&gt;
  
  
  🤖 Crear un agente y asignarle objetivos
&lt;/h2&gt;

&lt;p&gt;Una vez que tenemos definido nuestro plugin con las funciones necesarias, es hora de &lt;strong&gt;crear un agente&lt;/strong&gt; que lo utilice. Un agente es una instancia del tipo &lt;code&gt;ChatCompletionAgent&lt;/code&gt; que sabe cómo interactuar con un modelo de lenguaje, tiene acceso a un conjunto de funciones y puede operar con base en instrucciones personalizadas.&lt;/p&gt;

&lt;p&gt;En este paso lo conectamos todo: el kernel, el plugin, y la intención del asistente.&lt;/p&gt;

&lt;h3&gt;
  
  
  🧠 ¿Qué es un &lt;code&gt;ChatCompletionAgent&lt;/code&gt;?
&lt;/h3&gt;

&lt;p&gt;El &lt;code&gt;ChatCompletionAgent&lt;/code&gt; es una implementación del Agent Framework de Semantic Kernel que permite interactuar con modelos de lenguaje compatibles (como GPT-4 o GPT-3.5) de forma conversacional. Este tipo de agente:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Usa un kernel preconfigurado con modelos y funciones.&lt;/li&gt;
&lt;li&gt;Puede tener instrucciones personalizadas (“persona” o “rol”).&lt;/li&gt;
&lt;li&gt;Decide automáticamente qué función usar, si corresponde.&lt;/li&gt;
&lt;li&gt;Mantiene un contexto conversacional.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🏗️ Registrando el agente con DI
&lt;/h3&gt;

&lt;p&gt;En este ejemplo, configuramos el agente como un servicio usando la extensión &lt;code&gt;AddAgents&lt;/code&gt;. Esto permite que el agente se cree con acceso al kernel y al &lt;code&gt;InvoicesPlugin&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddKeyedTransient&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ChatCompletionAgent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"AssistantAgent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;kernel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetRequiredService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Kernel&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;agentKernel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;kernel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Clone&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Registramos el plugin de facturas&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;invoiceService&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetRequiredService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;InvoiceService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
    &lt;span class="n"&gt;agentKernel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ImportPluginFromObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InvoicesPlugin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invoiceService&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ChatCompletionAgent&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Asistente"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Instructions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"""
&lt;/span&gt;            &lt;span class="n"&gt;Eres&lt;/span&gt; &lt;span class="n"&gt;un&lt;/span&gt; &lt;span class="n"&gt;asistente&lt;/span&gt; &lt;span class="n"&gt;de&lt;/span&gt; &lt;span class="n"&gt;abogados&lt;/span&gt; &lt;span class="n"&gt;en&lt;/span&gt; &lt;span class="n"&gt;una&lt;/span&gt; &lt;span class="n"&gt;notar&lt;/span&gt;&lt;span class="err"&gt;í&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="err"&gt;ú&lt;/span&gt;&lt;span class="n"&gt;blica&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; 
            &lt;span class="n"&gt;Tu&lt;/span&gt; &lt;span class="n"&gt;tarea&lt;/span&gt; &lt;span class="n"&gt;es&lt;/span&gt; &lt;span class="n"&gt;ayudar&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;los&lt;/span&gt; &lt;span class="n"&gt;abogados&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;gestionar&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="n"&gt;verificar&lt;/span&gt; &lt;span class="n"&gt;el&lt;/span&gt; &lt;span class="n"&gt;estado&lt;/span&gt; &lt;span class="n"&gt;de&lt;/span&gt; &lt;span class="n"&gt;las&lt;/span&gt; &lt;span class="n"&gt;facturas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;

            &lt;span class="n"&gt;Funciones&lt;/span&gt; &lt;span class="n"&gt;disponibles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;Verificar&lt;/span&gt; &lt;span class="n"&gt;el&lt;/span&gt; &lt;span class="n"&gt;estado&lt;/span&gt; &lt;span class="n"&gt;de&lt;/span&gt; &lt;span class="n"&gt;pago&lt;/span&gt; &lt;span class="n"&gt;de&lt;/span&gt; &lt;span class="n"&gt;facturas&lt;/span&gt; &lt;span class="n"&gt;espec&lt;/span&gt;&lt;span class="err"&gt;í&lt;/span&gt;&lt;span class="n"&gt;ficas&lt;/span&gt;
            &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;Crear&lt;/span&gt; &lt;span class="n"&gt;prefacturas&lt;/span&gt; &lt;span class="n"&gt;para&lt;/span&gt; &lt;span class="n"&gt;clientes&lt;/span&gt;
            &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;Consultar&lt;/span&gt; &lt;span class="n"&gt;facturas&lt;/span&gt; &lt;span class="n"&gt;pendientes&lt;/span&gt; &lt;span class="n"&gt;de&lt;/span&gt; &lt;span class="n"&gt;pago&lt;/span&gt;
            &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;Marcar&lt;/span&gt; &lt;span class="n"&gt;facturas&lt;/span&gt; &lt;span class="n"&gt;como&lt;/span&gt; &lt;span class="n"&gt;pagadas&lt;/span&gt;
            &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;Consultar&lt;/span&gt; &lt;span class="n"&gt;informaci&lt;/span&gt;&lt;span class="err"&gt;ó&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="n"&gt;de&lt;/span&gt; &lt;span class="n"&gt;clientes&lt;/span&gt;

            &lt;span class="n"&gt;Siempre&lt;/span&gt; &lt;span class="n"&gt;proporciona&lt;/span&gt; &lt;span class="n"&gt;informaci&lt;/span&gt;&lt;span class="err"&gt;ó&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="n"&gt;clara&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="n"&gt;detallada&lt;/span&gt; &lt;span class="n"&gt;sobre&lt;/span&gt; &lt;span class="n"&gt;el&lt;/span&gt; &lt;span class="n"&gt;estado&lt;/span&gt; &lt;span class="n"&gt;de&lt;/span&gt; &lt;span class="n"&gt;las&lt;/span&gt; &lt;span class="n"&gt;facturas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
            &lt;span class="n"&gt;Usa&lt;/span&gt; &lt;span class="n"&gt;emojis&lt;/span&gt; &lt;span class="n"&gt;para&lt;/span&gt; &lt;span class="n"&gt;hacer&lt;/span&gt; &lt;span class="n"&gt;las&lt;/span&gt; &lt;span class="n"&gt;respuestas&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="err"&gt;á&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="n"&gt;visuales&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="err"&gt;á&lt;/span&gt;&lt;span class="n"&gt;ciles&lt;/span&gt; &lt;span class="n"&gt;de&lt;/span&gt; &lt;span class="n"&gt;entender&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
        &lt;span class="s"&gt;""",
&lt;/span&gt;        &lt;span class="n"&gt;Kernel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;agentKernel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Arguments&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;KernelArguments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;OpenAIPromptExecutionSettings&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;FunctionChoiceBehavior&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FunctionChoiceBehavior&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Auto&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;🧩 Este agente se construye a partir de un &lt;code&gt;Kernel&lt;/code&gt; clonado. Así puedes tener múltiples agentes con diferentes plugins, instrucciones o configuraciones sin interferencias entre ellos.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  🗣️ Instrucciones: cómo le decimos al agente quién es
&lt;/h3&gt;

&lt;p&gt;La propiedad &lt;code&gt;Instructions&lt;/code&gt; permite definir la personalidad, contexto y objetivos del agente. Aquí estamos usando un sistema de "prompt largo" al estilo system message:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Instructions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"""
&lt;/span&gt;    &lt;span class="n"&gt;Eres&lt;/span&gt; &lt;span class="n"&gt;un&lt;/span&gt; &lt;span class="n"&gt;asistente&lt;/span&gt; &lt;span class="n"&gt;de&lt;/span&gt; &lt;span class="n"&gt;abogados&lt;/span&gt; &lt;span class="n"&gt;en&lt;/span&gt; &lt;span class="n"&gt;una&lt;/span&gt; &lt;span class="n"&gt;notar&lt;/span&gt;&lt;span class="err"&gt;í&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="err"&gt;ú&lt;/span&gt;&lt;span class="n"&gt;blica&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; 
    &lt;span class="n"&gt;Tu&lt;/span&gt; &lt;span class="n"&gt;tarea&lt;/span&gt; &lt;span class="n"&gt;es&lt;/span&gt; &lt;span class="n"&gt;ayudar&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;los&lt;/span&gt; &lt;span class="n"&gt;abogados&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;gestionar&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="n"&gt;verificar&lt;/span&gt; &lt;span class="n"&gt;el&lt;/span&gt; &lt;span class="n"&gt;estado&lt;/span&gt; &lt;span class="n"&gt;de&lt;/span&gt; &lt;span class="n"&gt;las&lt;/span&gt; &lt;span class="n"&gt;facturas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="s"&gt;"""
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Este mensaje guía al modelo para que entienda el contexto en el que opera, el lenguaje que debe usar y el tipo de resultados esperados.&lt;/p&gt;

&lt;h3&gt;
  
  
  ⚙️ Control de funciones automáticas
&lt;/h3&gt;

&lt;p&gt;El fragmento clave para habilitar la selección automática de funciones es:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Arguments&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;KernelArguments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;OpenAIPromptExecutionSettings&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;FunctionChoiceBehavior&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FunctionChoiceBehavior&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Auto&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Esto permite que el modelo analice la intención del usuario y decida si debe invocar alguna función del plugin registrado. No es necesario indicarle explícitamente el nombre de la función: el agente elige por sí mismo en función del mensaje recibido.&lt;/p&gt;

&lt;p&gt;Con este paso, ya tenemos un &lt;strong&gt;asistente especializado&lt;/strong&gt; corriendo sobre Semantic Kernel, con acceso completo a las funciones del plugin de facturación y configurado para operar en el contexto de una notaría pública.&lt;/p&gt;

&lt;h2&gt;
  
  
  🚀 Ejecutar el agente y manejar conversaciones
&lt;/h2&gt;

&lt;p&gt;Una vez que nuestro agente está registrado y conectado a sus funciones, llega el momento de &lt;strong&gt;ponerlo en acción&lt;/strong&gt;. En este ejemplo, creamos una API HTTP que permite a cualquier cliente (como una app web o móvil) interactuar con el asistente de forma conversacional.&lt;/p&gt;

&lt;p&gt;Todo el ciclo se orquesta desde un conjunto de endpoints dentro de &lt;code&gt;AgentEndpoints.cs&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  🌐 Endpoint para iniciar una conversación
&lt;/h3&gt;

&lt;p&gt;El primer paso en cualquier interacción es &lt;strong&gt;crear un hilo de conversación&lt;/strong&gt;. Esto permite almacenar el historial y mantener contexto entre múltiples mensajes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/start"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ConversationService&lt;/span&gt; &lt;span class="n"&gt;conversationService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;conversationId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;conversationService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateConversationAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;ConversationId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conversationId&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Este endpoint genera un &lt;code&gt;ConversationId&lt;/code&gt; único que luego será utilizado en cada mensaje posterior. La conversación se almacena en la base de datos con su historial asociado.&lt;/p&gt;

&lt;h3&gt;
  
  
  💬 Enviar una pregunta al agente
&lt;/h3&gt;

&lt;p&gt;El segundo endpoint es el más importante: recibe un mensaje del usuario y devuelve la respuesta del agente. Lo interesante es que &lt;strong&gt;reconstruye el historial completo&lt;/strong&gt; antes de invocar el modelo, lo que permite mantener coherencia y continuidad.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AskQuestionRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;FromKeyedServices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"AssistantAgent"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="n"&gt;ChatCompletionAgent&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ConversationService&lt;/span&gt; &lt;span class="n"&gt;conversationService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ... Obtener historial desde base de datos&lt;/span&gt;
    &lt;span class="c1"&gt;// ... Agregar mensaje del usuario actual&lt;/span&gt;
    &lt;span class="c1"&gt;// ... Crear ChatHistoryAgentThread y lanzar la invocación&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  📜 Cómo se construye el historial
&lt;/h3&gt;

&lt;p&gt;El historial de conversación se crea a partir de los mensajes previos almacenados en la base de datos:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;chatHistory&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ChatHistory&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;dbMessage&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;conversation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Messages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dbMessage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Role&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;chatHistory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddUserMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dbMessage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dbMessage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Role&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"assistant"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;chatHistory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddAssistantMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dbMessage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Después se añade el nuevo mensaje del usuario, y se encapsula todo en un &lt;code&gt;ChatHistoryAgentThread&lt;/code&gt;, que es el tipo de contexto que entiende el agente:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;chatHistory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddUserMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Question&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;thread&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ChatHistoryAgentThread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chatHistory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ThreadId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  🧠 Ejecutar al agente y procesar respuestas
&lt;/h3&gt;

&lt;p&gt;El agente se invoca de forma asíncrona usando &lt;code&gt;InvokeAsync&lt;/code&gt;, lo que permite recibir múltiples respuestas si el modelo decide dividir su salida (especialmente útil cuando hay invocación de funciones):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;responses&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;InvokeAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;thread&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;allResponses&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;agentResponse&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;agentResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;allResponses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Luego se combinan todas las respuestas en un solo mensaje y se guarda nuevamente en la base de datos:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;finalResponse&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"\n\n"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;allResponses&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;conversationService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddMessageAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ThreadId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;finalResponse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"assistant"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  ✅ Respuesta final
&lt;/h3&gt;

&lt;p&gt;El resultado es una experiencia completamente conversacional, persistente y con funciones automatizadas. El cliente (por ejemplo, una SPA en Blazor o React) solo necesita enviar preguntas con un &lt;code&gt;ConversationId&lt;/code&gt; y mostrar las respuestas generadas.&lt;/p&gt;

&lt;h3&gt;
  
  
  📌 ¿Por qué usar historial y contexto?
&lt;/h3&gt;

&lt;p&gt;Esto es clave para que el agente pueda entender preguntas como:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“¿Y esa factura de Juan ya se pagó?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Sin historial, el modelo no sabría a qué se refiere “esa factura”. Al mantener contexto, el agente puede hacer inferencias y ejecutar funciones con mayor precisión.&lt;/p&gt;

&lt;p&gt;Con esto, tenemos todo lo necesario para una experiencia de agente conversacional &lt;strong&gt;real, persistente, funcional y extensible&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;📁 &lt;strong&gt;Nota:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Recuerda que el código completo está disponible en el repositorio, incluyendo el contexto de la base de datos y la implementación completa de &lt;code&gt;ConversationService&lt;/code&gt;, que se encarga de crear conversaciones, almacenar mensajes y reconstruir el historial.&lt;/p&gt;

&lt;p&gt;🔗 &lt;a href="https://github.com/isaacOjeda/DevToPosts/tree/main/SemanticKernelSeries/SemanticKernelLearning04" rel="noopener noreferrer"&gt;Ver el proyecto en GitHub&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  🧰 Extras, buenas prácticas y posibles extensiones
&lt;/h2&gt;

&lt;p&gt;Hasta este punto ya tienes un agente conversacional funcional, conectado a un modelo de lenguaje, con un plugin personalizado, y persistencia completa de las conversaciones. Sin embargo, el Agent Framework de Semantic Kernel ofrece muchas oportunidades para llevar tu solución al siguiente nivel.&lt;/p&gt;

&lt;p&gt;A continuación, te comparto algunas ideas para extender o mejorar tu asistente.&lt;/p&gt;

&lt;h3&gt;
  
  
  🔗 Agentes con múltiples plugins
&lt;/h3&gt;

&lt;p&gt;El &lt;code&gt;Kernel&lt;/code&gt; puede importar múltiples plugins, no solo uno. Por ejemplo, podrías tener:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;InvoicesPlugin&lt;/code&gt;: para gestión de facturas.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CalendarPlugin&lt;/code&gt;: para gestionar eventos o vencimientos.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;EmailPlugin&lt;/code&gt;: para enviar notificaciones automáticas.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;agentKernel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ImportPluginFromObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CalendarPlugin&lt;/span&gt;&lt;span class="p"&gt;(...));&lt;/span&gt;
&lt;span class="n"&gt;agentKernel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ImportPluginFromObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;EmailPlugin&lt;/span&gt;&lt;span class="p"&gt;(...));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Esto permite que el mismo agente decida qué herramienta usar dependiendo del mensaje del usuario, sin que tengas que escribir lógica adicional.&lt;/p&gt;

&lt;h3&gt;
  
  
  🧠 Agentes que planifican múltiples pasos
&lt;/h3&gt;

&lt;p&gt;El Agent Framework incluye soporte experimental para &lt;strong&gt;planificación automática&lt;/strong&gt;, donde el modelo puede decidir ejecutar múltiples funciones encadenadas para lograr un objetivo complejo.&lt;/p&gt;

&lt;p&gt;Por ejemplo:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Crea una prefactura para Carlos y márcala como pagada.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Esto podría generar un plan con dos pasos: &lt;code&gt;CreateInvoiceDraftAsync&lt;/code&gt; → &lt;code&gt;MarkInvoiceAsPaidAsync&lt;/code&gt;.&lt;br&gt;&lt;br&gt;
El agente puede manejar estas secuencias si se configura con &lt;code&gt;FunctionCalling&lt;/code&gt; en modo &lt;code&gt;Auto()&lt;/code&gt; y si las funciones están bien descritas.&lt;/p&gt;

&lt;h3&gt;
  
  
  🧪 Testing y depuración
&lt;/h3&gt;

&lt;p&gt;Cuando construyes agentes que llaman funciones reales, es importante:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Probar con mensajes ambiguos o incompletos para ver cómo reacciona el agente.&lt;/li&gt;
&lt;li&gt;Monitorear los logs de funciones invocadas.&lt;/li&gt;
&lt;li&gt;Verificar qué parámetros está eligiendo el modelo.&lt;/li&gt;
&lt;li&gt;Utilizar &lt;code&gt;FunctionResult&lt;/code&gt; para obtener metadata sobre las llamadas.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;También puedes capturar los mensajes generados por el modelo para analizarlos más adelante, por ejemplo, almacenando las decisiones o planes generados en una tabla de auditoría.&lt;/p&gt;

&lt;h3&gt;
  
  
  ⚠️ Cuida la experiencia conversacional
&lt;/h3&gt;

&lt;p&gt;Algunos consejos prácticos para que el asistente se sienta más natural:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Usa emojis y lenguaje cercano si el público lo permite (como hicimos aquí).&lt;/li&gt;
&lt;li&gt;Asegúrate de que las respuestas siempre incluyan lo esencial, incluso si no hay datos (ej: “no hay facturas vencidas”).&lt;/li&gt;
&lt;li&gt;Controla el tamaño del historial para evitar prompts excesivamente largos.&lt;/li&gt;
&lt;li&gt;Maneja errores con mensajes claros: si una factura no existe, si el email es inválido, etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🧩 Integración con interfaces visuales
&lt;/h3&gt;

&lt;p&gt;Aunque este ejemplo se enfoca en la API HTTP y el backend del agente, la interacción no termina ahí. Puedes conectar este agente conversacional a diferentes interfaces gráficas según tu contexto:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Una aplicación web (por ejemplo, Blazor, React, Angular).&lt;/li&gt;
&lt;li&gt;Un chatbot embebido en tu intranet o sitio público.&lt;/li&gt;
&lt;li&gt;Clientes móviles nativos.&lt;/li&gt;
&lt;li&gt;Canales como Microsoft Teams, Telegram o WhatsApp.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;💬 &lt;strong&gt;Importante:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
El repositorio del proyecto incluye un &lt;strong&gt;chat funcional ya implementado&lt;/strong&gt; que se comunica con esta API y permite mantener conversaciones con el agente.&lt;br&gt;&lt;br&gt;
Este cliente está disponible como referencia y punto de exploración: puedes ver cómo se estructuran los hilos, cómo se muestra el historial y cómo se consume el endpoint &lt;code&gt;/api/agent&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;🔗 &lt;a href="https://github.com/isaacOjeda/DevToPosts/tree/main/SemanticKernelSeries/SemanticKernelLearning04" rel="noopener noreferrer"&gt;Ver el repositorio en GitHub&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Este ejemplo es ideal para experimentar con el flujo completo: desde el frontend que envía preguntas, hasta el agente que responde y persiste el historial.&lt;/p&gt;

&lt;h3&gt;
  
  
  🌱 ¿Qué más podrías construir con este enfoque?
&lt;/h3&gt;

&lt;p&gt;El Agent Framework es ideal para automatizar tareas repetitivas en sistemas internos. Algunos ejemplos reales donde este enfoque encaja:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Asistentes de soporte técnico interno (consultar errores, reiniciar servicios).&lt;/li&gt;
&lt;li&gt;Asistentes legales que redactan contratos o extraen cláusulas.&lt;/li&gt;
&lt;li&gt;Agentes que combinan datos de distintas fuentes (facturas + clientes + CRM).&lt;/li&gt;
&lt;li&gt;Robots de backoffice que ayudan a generar informes o consolidar información.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;📌 &lt;strong&gt;Consejo final:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Piensa en tu agente como un &lt;em&gt;colega digital&lt;/em&gt;: cuantas más herramientas le des, más tareas podrá resolver sin que tú tengas que intervenir manualmente.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  ✅ Conclusión
&lt;/h2&gt;

&lt;p&gt;El &lt;strong&gt;Agent Framework de Semantic Kernel&lt;/strong&gt; abre una puerta poderosa para construir aplicaciones inteligentes que combinan lenguaje natural con lógica de negocio real. En este artículo, creamos un &lt;strong&gt;agente conversacional especializado en facturación&lt;/strong&gt;, capaz de razonar sobre peticiones del usuario e invocar funciones de una aplicación en .NET.&lt;/p&gt;

&lt;p&gt;Hemos visto cómo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Configurar un &lt;code&gt;Kernel&lt;/code&gt; con Azure OpenAI.&lt;/li&gt;
&lt;li&gt;Exponer funciones reales como plugins usando &lt;code&gt;[KernelFunction]&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Construir y personalizar un agente con instrucciones específicas.&lt;/li&gt;
&lt;li&gt;Mantener conversaciones persistentes con historial contextual.&lt;/li&gt;
&lt;li&gt;Integrar todo en una API funcional, lista para usarse desde un cliente.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Este enfoque es aplicable a muchísimos casos reales: desde automatización interna hasta asistentes legales, técnicos o administrativos.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 Lo mejor de todo es que puedes extender esta base fácilmente: agregar nuevos plugins, integrar fuentes de datos adicionales, o escalar hacia canales como webchat, Teams o bots en producción.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Si te interesa explorar más sobre cómo usar Semantic Kernel en escenarios del mundo real, o quieres construir asistentes más sofisticados, este es apenas el comienzo.&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;Recuerda que el código completo está disponible aquí:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://github.com/isaacOjeda/DevToPosts/tree/main/SemanticKernelSeries/SemanticKernelLearning04" rel="noopener noreferrer"&gt;github.com/isaacOjeda/DevToPosts/tree/main/SemanticKernelSeries/SemanticKernelLearning04&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  📚 Referencias
&lt;/h2&gt;

&lt;p&gt;Si quieres seguir explorando el Agent Framework de Semantic Kernel y todas sus posibilidades en C#, aquí tienes enlaces oficiales de documentación que complementan lo visto en este artículo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/semantic-kernel/frameworks/agent/?pivots=programming-language-csharp" rel="noopener noreferrer"&gt;Semantic Kernel Agent Framework | Microsoft Learn&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/semantic-kernel/frameworks/agent/agent-architecture?pivots=programming-language-csharp" rel="noopener noreferrer"&gt;Semantic Kernel Agent Architecture | Microsoft Learn&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/semantic-kernel/frameworks/agent/agent-api?pivots=programming-language-csharp" rel="noopener noreferrer"&gt;The Semantic Kernel Common Agent API surface | Microsoft Learn&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/semantic-kernel/frameworks/agent/agent-functions?pivots=programming-language-csharp" rel="noopener noreferrer"&gt;Configuring Agents with Semantic Kernel Plugins. | Microsoft Learn&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/semantic-kernel/frameworks/agent/agent-types/chat-completion-agent?pivots=programming-language-csharp" rel="noopener noreferrer"&gt;Exploring the Semantic Kernel ChatCompletionAgent | Microsoft Learn&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/training/paths/develop-ai-agents-on-azure/?source=learn" rel="noopener noreferrer"&gt;Develop AI Agents on Azure - Training | Microsoft Learn&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>azure</category>
      <category>csharp</category>
      <category>openai</category>
      <category>semantickernel</category>
    </item>
    <item>
      <title>Semantic Kernel – Parte 03: Embeddings y Retrieval-Augmented Generation (RAG)</title>
      <dc:creator>Isaac Ojeda</dc:creator>
      <pubDate>Sun, 16 Feb 2025 17:00:31 +0000</pubDate>
      <link>https://dev.to/isaacojeda/semantic-kernel-parte-03-embeddings-y-retrieval-augmented-generation-rag-4fjn</link>
      <guid>https://dev.to/isaacojeda/semantic-kernel-parte-03-embeddings-y-retrieval-augmented-generation-rag-4fjn</guid>
      <description>&lt;h2&gt;
  
  
  &lt;strong&gt;Introducción&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;En esta tercera parte de nuestra serie sobre &lt;strong&gt;Semantic Kernel&lt;/strong&gt;, nos adentramos en la integración de &lt;strong&gt;Embeddings&lt;/strong&gt; y &lt;strong&gt;Retrieval-Augmented Generation (RAG)&lt;/strong&gt; para mejorar la generación de contenido y la recuperación de información. Utilizando herramientas como &lt;strong&gt;Ollama&lt;/strong&gt; para la creación de embeddings y &lt;strong&gt;Qdrant&lt;/strong&gt; como vector store, podemos construir sistemas más inteligentes capaces de generar respuestas más precisas basadas en datos externos y actualizados, en lugar de depender únicamente de los datos con los que fue entrenado el modelo. A través de esta configuración, aprovechamos la sinergia entre el poder de los modelos de lenguaje y la capacidad de búsqueda semántica para proporcionar soluciones más efectivas a las consultas de los usuarios. En este artículo, exploraremos cómo implementar estas tecnologías en &lt;strong&gt;Semantic Kernel&lt;/strong&gt;, con ejemplos prácticos para configurar los servicios y gestionar los embeddings de manera eficiente.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;¿Qué son los Embeddings?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Los &lt;strong&gt;embeddings&lt;/strong&gt; son representaciones numéricas de texto en un espacio vectorial de alta dimensión. Estas representaciones permiten medir la similitud semántica entre palabras, frases o documentos completos. En el contexto de &lt;strong&gt;Semantic Kernel&lt;/strong&gt;, los embeddings se utilizan para mejorar la búsqueda de información y la generación de respuestas basadas en conocimiento almacenado.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Características clave de los Embeddings:&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Capturan el significado semántico del texto.&lt;/li&gt;
&lt;li&gt;Permiten encontrar contenido relacionado aunque no se utilicen exactamente las mismas palabras.&lt;/li&gt;
&lt;li&gt;Se almacenan en bases de datos especializadas llamadas &lt;strong&gt;vector stores&lt;/strong&gt; (como &lt;strong&gt;Qdrant&lt;/strong&gt;).&lt;/li&gt;
&lt;li&gt;Se generan con modelos de lenguaje avanzados, como los proporcionados por &lt;strong&gt;Ollama&lt;/strong&gt; u &lt;strong&gt;Open AI&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;¿Qué es RAG (Retrieval-Augmented Generation)?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Retrieval-Augmented Generation (RAG)&lt;/strong&gt; es una técnica que combina la recuperación de información con la generación de respuestas mediante un modelo de lenguaje. En lugar de confiar solo en los datos con los que fue entrenado el modelo, RAG permite buscar información relevante en bases de conocimiento externas y utilizarla para generar respuestas más precisas y actualizadas.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;Funcionamiento de RAG en Semantic Kernel:&lt;/strong&gt;
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Consulta del usuario&lt;/strong&gt;: Un usuario hace una pregunta o solicitud.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Búsqueda semántica&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;Se generan embeddings de la consulta.&lt;/li&gt;
&lt;li&gt;Se comparan con los embeddings almacenados en el vector store (Qdrant) para recuperar información relevante.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generación de respuesta&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;La información recuperada se pasa como contexto al modelo de lenguaje.&lt;/li&gt;
&lt;li&gt;Se genera una respuesta más precisa y fundamentada en los datos encontrados.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Beneficios de RAG en Semantic Kernel:&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mejor comprensión de consultas&lt;/strong&gt;: Permite obtener respuestas relevantes incluso si el usuario no usa palabras exactas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conocimiento actualizado&lt;/strong&gt;: Se pueden agregar nuevos datos al sistema sin necesidad de reentrenar el modelo de IA.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optimización del procesamiento&lt;/strong&gt;: Reduce el costo computacional al recuperar solo la información relevante en lugar de analizar todo el corpus de datos.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Configurando Semantic Kernel con Embeddings y un Vector Store&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;En esta sección, configuraremos &lt;strong&gt;Semantic Kernel&lt;/strong&gt; para trabajar con &lt;strong&gt;Embeddings&lt;/strong&gt; y un &lt;strong&gt;Vector Store&lt;/strong&gt;, utilizando &lt;strong&gt;Aspire&lt;/strong&gt; para orquestar los servicios.&lt;/p&gt;

&lt;p&gt;Los servicios principales que vamos a configurar son:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;llama3.2&lt;/strong&gt;: Modelo generador de texto.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;nomic-embed-text&lt;/strong&gt;: Modelo generador de embeddings.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Qdrant&lt;/strong&gt;: Almacenamiento y recuperación de embeddings.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aspire&lt;/strong&gt;: Orquestación y despliegue de los servicios.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Configuración de Aspire&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Aspire nos permite definir y gestionar los servicios necesarios en un solo archivo de configuración. En este caso, configuramos &lt;strong&gt;Qdrant&lt;/strong&gt; como nuestro vector store y &lt;strong&gt;llama3.2&lt;/strong&gt; y &lt;strong&gt;nomic-embed-text&lt;/strong&gt; como proveedor de embeddings.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Nota 💡: Esto ya lo hemos visto en los otros posts, siempre usa como referencia el código fuente (&lt;a href="https://github.com/isaacOjeda/DevToPosts/tree/main/SemanticKernelSeries/SemanticKernelLearning03" rel="noopener noreferrer"&gt;DevToPosts/SemanticKernelSeries/SemanticKernelLearning03 at main · isaacOjeda/DevToPosts&lt;/a&gt;) para más información.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DistributedApplication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Configuración de Qdrant&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;qdrant&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddQdrant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"qdrant"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithLifetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ContainerLifetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Persistent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Configuración de Ollama&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;ollama&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddOllama&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ollama"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithDataVolume&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithOpenWebUI&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Modelos de Ollama&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;llamaModel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ollama&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"llama"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"llama3.2"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ollama&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"embed-text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"nomic-embed-text"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Configuración del API Service con referencias a los modelos y Qdrant&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;apiService&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddProject&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Projects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SemanticKernelLearning03_ApiService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"apiservice"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithReference&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;llamaModel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithReference&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithReference&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;qdrant&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Construcción y ejecución de Aspire&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt; &lt;strong&gt;Qdrant&lt;/strong&gt; se configura como un servicio persistente.

&lt;ul&gt;
&lt;li&gt;Se hace de esta forma para que no sea tan lento de inicializar en cada inicio de Aspire.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt; &lt;strong&gt;Ollama&lt;/strong&gt; se configura con almacenamiento de datos y una interfaz web.

&lt;ul&gt;
&lt;li&gt; Se añaden los modelos &lt;strong&gt;"llama3.2"&lt;/strong&gt; para generación de texto y &lt;strong&gt;"nomic-embed-text"&lt;/strong&gt; para embeddings.
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Por último se agrega nuestra API en ASP.NET Core.
&lt;h3&gt;
  
  
  &lt;strong&gt;Configuración de Semantic Kernel con Aspire&lt;/strong&gt;
&lt;/h3&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Aspire se encargará de inyectar las &lt;strong&gt;cadenas de conexión&lt;/strong&gt; en la API, según los nombres definidos en la configuración.&lt;/p&gt;

&lt;p&gt;Para hacer que &lt;strong&gt;Semantic Kernel&lt;/strong&gt; utilice correctamente los servicios orquestados por Aspire, debemos procesar estas cadenas de conexión. Como lo hemos hecho en ejemplos anteriores, parsearemos la información recibida y estructuraremos mejor nuestra configuración.&lt;/p&gt;

&lt;p&gt;Para lograr esto, &lt;strong&gt;separaremos la configuración de dependencias en un archivo específico&lt;/strong&gt;, lo que nos permitirá mantener un código más limpio y modular.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;Creando &lt;code&gt;DependencyConfig.cs&lt;/code&gt; para gestionar las dependencias&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;En este archivo, crearemos el método de extensión &lt;code&gt;AddSemanticKernel&lt;/code&gt;, el cual:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Obtiene las cadenas de conexión inyectadas por Aspire.
&lt;/li&gt;
&lt;li&gt;Extrae los detalles necesarios de &lt;strong&gt;Ollama&lt;/strong&gt; (para generación de texto y embeddings).
&lt;/li&gt;
&lt;li&gt;Extrae la información de conexión a &lt;strong&gt;Qdrant&lt;/strong&gt; (Vector Store).
&lt;/li&gt;
&lt;li&gt;Registra los servicios de &lt;strong&gt;Semantic Kernel&lt;/strong&gt; con las dependencias configuradas.
&lt;/li&gt;
&lt;li&gt;Agrega un servicio de &lt;strong&gt;búsqueda semántica&lt;/strong&gt; (&lt;code&gt;ITextSearch&lt;/code&gt;) que permite realizar búsquedas en una colección de vectores, el cual usaremos más adelante.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Aquí está el código en &lt;code&gt;DependencyConfig.cs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DependencyConfig&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;AddSemanticKernel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="n"&gt;WebApplicationBuilder&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;completionModel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
            &lt;span class="nf"&gt;GetModelDetailsFromConnectionString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetConnectionString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"llama"&lt;/span&gt;&lt;span class="p"&gt;)!);&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;embeddingModel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
            &lt;span class="nf"&gt;GetModelDetailsFromConnectionString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetConnectionString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"embed-text"&lt;/span&gt;&lt;span class="p"&gt;)!);&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
            &lt;span class="nf"&gt;GetQdrantDetailsFromConnectionString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetConnectionString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"qdrant"&lt;/span&gt;&lt;span class="p"&gt;)!);&lt;/span&gt;

        &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddKernel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddOllamaChatCompletion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;completionModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddOllamaTextEmbeddingGeneration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embeddingModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddQdrantVectorStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddKeyedTransient&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ITextSearch&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;BlogPost&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VectorName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;vectorStore&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetRequiredService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IVectorStore&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;embeddingGenerationService&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetRequiredService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ITextEmbeddingGenerationService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;blogposts&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;vectorStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetCollection&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BlogPost&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;BlogPost&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VectorName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="n"&gt;blogposts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateCollectionIfNotExistsAsync&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;ConfigureAwait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;textSearch&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;VectorStoreTextSearch&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;BlogPost&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;blogposts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;embeddingGenerationService&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;textSearch&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Explicación del código&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Obtenemos los detalles de conexión&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;GetModelDetailsFromConnectionString&lt;/code&gt; extrae el modelo y endpoint desde las cadenas de conexión de &lt;strong&gt;Ollama&lt;/strong&gt; (chat y embeddings).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GetQdrantDetailsFromConnectionString&lt;/code&gt; extrae el &lt;strong&gt;host, puerto y API key&lt;/strong&gt; de Qdrant.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Registramos los servicios de Semantic Kernel&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;AddOllamaChatCompletion&lt;/code&gt; para generación de texto con &lt;strong&gt;Ollama&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AddOllamaTextEmbeddingGeneration&lt;/code&gt; para generación de &lt;strong&gt;embeddings&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AddQdrantVectorStore&lt;/code&gt; para almacenar y recuperar datos desde &lt;strong&gt;Qdrant&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configuramos un servicio de búsqueda semántica (&lt;code&gt;ITextSearch&lt;/code&gt;)&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Se obtiene la colección de &lt;strong&gt;vectores&lt;/strong&gt; asociada a &lt;code&gt;BlogPost&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Si la colección no existe, se &lt;strong&gt;crea automáticamente&lt;/strong&gt; en Qdrant.&lt;/li&gt;
&lt;li&gt;Se inicializa un objeto &lt;code&gt;VectorStoreTextSearch&amp;lt;BlogPost&amp;gt;&lt;/code&gt;, permitiendo realizar &lt;strong&gt;búsquedas semánticas&lt;/strong&gt; en la colección.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Almacenando Embeddings en Qdrant&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Para almacenar datos en Qdrant, primero debemos definir nuestro modelo de datos que será indexado en la base de datos vectorial.&lt;br&gt;&lt;br&gt;
En este ejemplo, queremos guardar una serie de posts y realizar búsquedas semánticas sobre su contenido.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Advertencia ⚠️: Las búsquedas vectoriales que estamos viendo aquí están en &lt;strong&gt;Preview (Alpha)&lt;/strong&gt;, por lo que podrían cambiar en el futuro. Actualmente, &lt;strong&gt;no se recomienda su uso en producción&lt;/strong&gt;, a menos que estés dispuesto a evolucionar junto con el framework.&lt;br&gt;
Anteriormente, se utilizaban los &lt;strong&gt;Memory de Semantic Kernel&lt;/strong&gt; para búsquedas similares, pero estos están en proceso de ser considerados &lt;strong&gt;"Legacy"&lt;/strong&gt;, por lo que su uso a largo plazo podría no ser viable.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  &lt;strong&gt;Definición del modelo &lt;code&gt;BlogPost&lt;/code&gt;&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;El modelo &lt;code&gt;BlogPost&lt;/code&gt; representará las entradas del blog en nuestra base de datos vectorial.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BlogPost&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;VectorName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"blogposts"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;VectorStoreRecordKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;TextSearchResultLink&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;BlogPostId&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;VectorStoreRecordData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IsFilterable&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;TextSearchResultName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Title&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;VectorStoreRecordData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IsFullTextSearchable&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;TextSearchResultValue&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Description&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;VectorStoreRecordVector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;768&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DistanceFunction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DotProductSimilarity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IndexKind&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Hnsw&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;ReadOnlyMemory&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;?&lt;/span&gt; &lt;span class="n"&gt;DescriptionEmbedding&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;VectorStoreRecordData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IsFilterable&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;Tags&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Atributos de &lt;code&gt;BlogPost&lt;/code&gt;&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;BlogPostId&lt;/code&gt;&lt;/strong&gt;: Identificador único del post. Se marca como clave primaria con &lt;code&gt;[VectorStoreRecordKey]&lt;/code&gt; y como enlace de resultados de búsqueda con &lt;code&gt;[TextSearchResultLink]&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Title&lt;/code&gt;&lt;/strong&gt;: Almacena el título del post. Usado como filtro con &lt;code&gt;[VectorStoreRecordData(IsFilterable = true)]&lt;/code&gt; y como nombre principal en los resultados de búsqueda con &lt;code&gt;[TextSearchResultName]&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Description&lt;/code&gt;&lt;/strong&gt;: Contiene el contenido del post. Es indexado para búsquedas de texto completo con &lt;code&gt;[VectorStoreRecordData(IsFullTextSearchable = true)]&lt;/code&gt; y su valor es mostrado en los resultados con &lt;code&gt;[TextSearchResultValue]&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;DescriptionEmbedding&lt;/code&gt;&lt;/strong&gt;: Representación vectorial de &lt;code&gt;Description&lt;/code&gt;. Almacenado como un vector de 768 dimensiones usando &lt;code&gt;[VectorStoreRecordVector]&lt;/code&gt;, con &lt;code&gt;DotProductSimilarity&lt;/code&gt; como función de distancia y &lt;code&gt;Hnsw&lt;/code&gt; para indexación eficiente.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Tags&lt;/code&gt;&lt;/strong&gt;: Etiquetas relacionadas con el post. Pueden ser usadas como filtros en consultas con &lt;code&gt;[VectorStoreRecordData(IsFilterable = true)]&lt;/code&gt;.
### &lt;strong&gt;Creando un endpoint para almacenar &lt;code&gt;BlogPost&lt;/code&gt; en Qdrant&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Para guardar nuevos &lt;code&gt;BlogPost&lt;/code&gt;, crearemos un endpoint que recibirá los datos del post, generará su embedding y lo almacenará en la base de datos vectorial.&lt;/p&gt;

&lt;p&gt;Nuestro servicio ya tiene configurados &lt;code&gt;ITextEmbeddingGenerationService&lt;/code&gt; (para generar embeddings) e &lt;code&gt;IVectorStore&lt;/code&gt; (para interactuar con Qdrant).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreateBlogPost&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;Tags&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;BlogPostId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Handler&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;ITextEmbeddingGenerationService&lt;/span&gt; &lt;span class="n"&gt;_embeddingService&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;IVectorStore&lt;/span&gt; &lt;span class="n"&gt;_vectorStore&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;Handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ITextEmbeddingGenerationService&lt;/span&gt; &lt;span class="n"&gt;embeddingService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IVectorStore&lt;/span&gt; &lt;span class="n"&gt;vectorStore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;_embeddingService&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;embeddingService&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;_vectorStore&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;vectorStore&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;blogposts&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_vectorStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetCollection&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BlogPost&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;BlogPost&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VectorName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;blogposts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateCollectionIfNotExistsAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;embeddingContents&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
                &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_embeddingService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GenerateEmbeddingAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;newBlogPost&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;BlogPost&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;BlogPostId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewGuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="n"&gt;Title&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;Description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;DescriptionEmbedding&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;embeddingContents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;Tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tags&lt;/span&gt;
            &lt;span class="p"&gt;};&lt;/span&gt;

            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;blogposts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UpsertAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;newBlogPost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;newBlogPost&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BlogPostId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Explicación del código&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Manejador (&lt;code&gt;Handler&lt;/code&gt;)&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Recibe los servicios &lt;code&gt;ITextEmbeddingGenerationService&lt;/code&gt; y &lt;code&gt;IVectorStore&lt;/code&gt; a través del constructor.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lógica del método &lt;code&gt;Handle&lt;/code&gt;&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Obtiene la colección &lt;code&gt;blogposts&lt;/code&gt; de Qdrant.&lt;/li&gt;
&lt;li&gt;Si la colección no existe, la crea con &lt;code&gt;CreateCollectionIfNotExistsAsync()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Genera el embedding del &lt;code&gt;Description&lt;/code&gt; usando &lt;code&gt;_embeddingService.GenerateEmbeddingAsync()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Crea un &lt;code&gt;BlogPost&lt;/code&gt; con los datos proporcionados y el embedding generado.&lt;/li&gt;
&lt;li&gt;Guarda el post en Qdrant con &lt;code&gt;UpsertAsync()&lt;/code&gt;, que inserta o actualiza el registro.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Nota 💡&lt;/strong&gt;: Para ver cómo este handler se integra con Minimal APIs, revisa el código fuente del proyecto. Para mantener este artículo conciso, omitiremos detalles específicos.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;Probando el endpoint con &lt;code&gt;api.http&lt;/code&gt;&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;Para poblar la base de datos vectorial, podemos hacer una solicitud HTTP de prueba.&lt;/p&gt;

&lt;p&gt;Ejemplo en &lt;code&gt;SemanticKernelLearning03.ApiService.http&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@SemanticKernelLearning03.ApiService_HostAddress = https://localhost:7391

POST {{SemanticKernelLearning03.ApiService_HostAddress}}/api/blog-posts
Content-Type: application/json

{
  "Title": "Introducción a ASP.NET Core",
  "Description": "Una guía completa sobre cómo comenzar con ASP.NET Core y sus características principales.",
  "Tags": ["ASP.NET Core", "C#", "Web Development"]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;Verificando los datos en Qdrant&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Si ejecutamos nuestra solución con Aspire y realizamos la solicitud anterior, podemos inspeccionar los datos en el dashboard de Qdrant y verificar que la colección y los registros existen.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;Consideraciones sobre el tamaño de los vectores&lt;/strong&gt;
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Este ejemplo usa &lt;strong&gt;nomic-embed-text&lt;/strong&gt;, que genera vectores de &lt;strong&gt;768 dimensiones&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Qdrant debe estar configurado para aceptar este tamaño de vector.&lt;/li&gt;
&lt;li&gt;Si usas modelos de OpenAI u otros proveedores, verifica el tamaño del vector antes de insertarlo en Qdrant.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Recuperando Información desde Qdrant&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Para demostrar cómo realizar búsquedas semánticas en los vectores almacenados en Qdrant, podemos hacerlo de distintas maneras: usando el vector directamente o utilizando una abstracción que &lt;strong&gt;Semantic Kernel&lt;/strong&gt; nos proporciona, llamada &lt;code&gt;ITextSearch&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ITextSearch&lt;/code&gt; tiene distintas implementaciones, como &lt;code&gt;BingTextSearch&lt;/code&gt; (que realiza búsquedas en Bing), pero en nuestro caso usaremos &lt;code&gt;VectorStoreTextSearch&lt;/code&gt;, que está diseñado específicamente para realizar búsquedas en bases de datos vectoriales.&lt;/p&gt;

&lt;p&gt;Esta clase necesita conocer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;El modelo de embeddings&lt;/strong&gt; que estamos usando.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;El nombre de la colección en Qdrant&lt;/strong&gt; donde haremos la búsqueda.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;El proceso realmente es sencillo y podríamos hacerlo manualmente construyendo consultas directamente contra la base de datos vectorial. Sin embargo, en este caso, aprovecharemos &lt;code&gt;ITextSearch&lt;/code&gt; para simplificar el proceso.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Nota 💡: Para más información sobre búsquedas vectoriales en Semantic Kernel, consulta la documentación oficial de Microsoft:&lt;br&gt;&lt;br&gt;
&lt;a href="https://learn.microsoft.com/en-us/semantic-kernel/concepts/vector-store-connectors/vector-search?pivots=programming-language-csharp" rel="noopener noreferrer"&gt;Vector search using Semantic Kernel Vector Store connectors (Preview) | Microsoft Learn&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Implementación del Endpoint &lt;code&gt;SearchBlogPost&lt;/code&gt;&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Para realizar la búsqueda semántica, crearemos un nuevo endpoint llamado &lt;code&gt;SearchBlogPost&lt;/code&gt;, que permitirá encontrar los &lt;strong&gt;posts más relevantes&lt;/strong&gt; basados en una consulta en lenguaje natural.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SearchBlogPost&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;BlogPostId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Handler&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nf"&gt;FromKeyedServices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BlogPost&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VectorName&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="n"&gt;ITextSearch&lt;/span&gt; &lt;span class="n"&gt;textSearch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;KernelSearchResults&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TextSearchResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;textResults&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
                &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;textSearch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetTextSearchResultsAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;Top&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;Skip&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;responses&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TextSearchResult&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;textResults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithCancellation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Link&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Explicación del Código&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Uso de &lt;code&gt;ITextSearch&lt;/code&gt;&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Lo recibimos en el constructor con &lt;code&gt;[FromKeyedServices(BlogPost.VectorName)]&lt;/code&gt;. Esto asegura que estamos inyectando la instancia correcta configurada para nuestra colección de &lt;strong&gt;blog posts&lt;/strong&gt; en Qdrant.

&lt;ul&gt;
&lt;li&gt;Esto lo hemos configurado en &lt;code&gt;DependencyConfig&lt;/code&gt; anteriormente.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Realizamos la búsqueda semántica&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Llamamos a &lt;code&gt;GetTextSearchResultsAsync(...);&lt;/code&gt;, donde:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;request.Query&lt;/code&gt; es el término de búsqueda ingresado.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Top = 2&lt;/code&gt; indica que queremos los &lt;strong&gt;dos resultados más relevantes&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Skip = 0&lt;/code&gt; significa que no saltaremos ningún resultado.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Procesamos los resultados&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Iteramos sobre &lt;code&gt;textResults.Results&lt;/code&gt; para extraer:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;result.Link&lt;/code&gt;: Contiene el &lt;code&gt;BlogPostId&lt;/code&gt; almacenado como string, lo convertimos a &lt;code&gt;Guid&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;result.Name&lt;/code&gt;: Corresponde al título del blog post.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;result.Value&lt;/code&gt;: Contiene la descripción del blog post.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Devolvemos la lista de resultados&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Cada &lt;code&gt;Response&lt;/code&gt; representa un &lt;strong&gt;blog post relevante&lt;/strong&gt; basado en la búsqueda semántica.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Una vez que el endpoint &lt;code&gt;SearchBlogPost&lt;/code&gt; está registrado, podemos comenzar a realizar búsquedas semánticas para encontrar los posts más relevantes según el texto ingresado.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ejemplo de Consulta&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Podemos hacer una búsqueda con la siguiente solicitud:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;### Busquedas Semanticas
@Query=arquitectura de software
GET {{SemanticKernelLearning03.ApiService_HostAddress}}/api/blog-posts?Query={{Query}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Respuesta Esperada&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Si hay blog posts relevantes en la base de datos, la API devolverá un listado de los más coincidentes. Un ejemplo de respuesta podría ser:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;    &lt;/span&gt;&lt;span class="nl"&gt;"blogPostId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="err"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"949b7164-2021-4fcc-b58e-9887fdd00d0e"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;    &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="err"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Clean Architecture: Diseño de Software Modular y Escalable"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;    &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="err"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Clean Architecture es un enfoque de diseño de software ..."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;    &lt;/span&gt;&lt;span class="nl"&gt;"blogPostId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="err"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2340f1ac-e810-4067-90f5-a71899c6d42a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;    &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="err"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Principios SOLID en el Desarrollo de Software"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;    &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="err"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Los principios SOLID son un conjunto de cinco principios de..."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Si revisamos el repositorio, podemos notar que hay posts de distintos temas (por ejemplo, &lt;strong&gt;Machine Learning, Bases de Datos, DevOps&lt;/strong&gt;), pero la búsqueda regresó los más relevantes según el texto ingresado. Esto demuestra que el motor de búsqueda semántico está funcionando correctamente.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;Personalizando la Búsqueda&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;Podemos ajustar la consulta cambiando:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;La cantidad de resultados (&lt;code&gt;Top&lt;/code&gt;)&lt;/strong&gt;: Actualmente devuelve &lt;strong&gt;dos posts&lt;/strong&gt;, pero podríamos aumentar este valor si queremos más opciones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;El modelo de embeddings&lt;/strong&gt;: Si cambiamos el modelo de embeddings usado para almacenar los vectores, los resultados pueden variar en precisión y relevancia.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Integración con RAG (Retrieval-Augmented Generation)&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;En esta sección, vamos a implementar un nuevo endpoint que va más allá de una búsqueda semántica. En lugar de solo buscar contenido relevante, vamos a generar texto de manera dinámica a partir de una consulta proporcionada por el usuario. Lo interesante aquí es que el contexto para la generación de la respuesta será el contenido de los &lt;strong&gt;Posts del Blog&lt;/strong&gt; que coincidan con la consulta del usuario.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;¿Cómo funciona?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Si el usuario hace una pregunta como:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cuál es el objetivo principal de clean architecture?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;En lugar de simplemente devolver una lista de posts relacionados, el modelo de Semantic Kernel buscará los posts más relevantes relacionados con la consulta, y usará esos resultados como contexto para generar una respuesta detallada. El modelo no solo devolverá contenido estático, sino que también incorporará la información más relevante y actualizada disponible en los posts, permitiendo una respuesta más precisa y contextualizada.&lt;/p&gt;

&lt;p&gt;Esta técnica es útil porque, por lo general, los modelos de lenguaje (LLMs) se entrenan con datos estáticos que no contienen información actualizada o específica del dominio. Al integrar la &lt;strong&gt;Recuperación de Información (RAG)&lt;/strong&gt;, aprovechamos los datos disponibles en tiempo real para mejorar la relevancia y exactitud de las respuestas generadas.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;¿Cómo lo implementamos?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;En el post anterior, vimos cómo crear &lt;strong&gt;Plugins&lt;/strong&gt; o &lt;strong&gt;Funciones Semánticas&lt;/strong&gt;, y cómo Semantic Kernel se encarga de invocar automáticamente esos plugins cuando es necesario. Ahora, vamos a utilizar un plugin de búsqueda vectorial para recuperar los posts relevantes y luego construir un &lt;strong&gt;prompt personalizado&lt;/strong&gt; que permita al modelo generar una respuesta coherente utilizando esos posts como contexto.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;El Endpoint y el Código&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;El endpoint que vamos a implementar es el siguiente:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;QuestionsBlogPost&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Kernel&lt;/span&gt; &lt;span class="n"&gt;kernel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;FromKeyedServices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BlogPost&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VectorName&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="n"&gt;ITextSearch&lt;/span&gt; &lt;span class="n"&gt;textSearch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;searchPlugin&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;textSearch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateWithGetTextSearchResults&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SearchPlugin"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;kernel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Plugins&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;searchPlugin&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;promptTemplate&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"""
&lt;/span&gt;                                    &lt;span class="n"&gt;Utiliza&lt;/span&gt; &lt;span class="n"&gt;solamente&lt;/span&gt; &lt;span class="n"&gt;los&lt;/span&gt; &lt;span class="n"&gt;siguientes&lt;/span&gt; &lt;span class="n"&gt;contenidos&lt;/span&gt; &lt;span class="n"&gt;para&lt;/span&gt; &lt;span class="n"&gt;contestar&lt;/span&gt; &lt;span class="n"&gt;las&lt;/span&gt; &lt;span class="n"&gt;preguntas&lt;/span&gt; &lt;span class="n"&gt;realizadas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; 
                                    &lt;span class="n"&gt;Si&lt;/span&gt; &lt;span class="n"&gt;no&lt;/span&gt; &lt;span class="n"&gt;sabes&lt;/span&gt; &lt;span class="n"&gt;la&lt;/span&gt; &lt;span class="n"&gt;respuesta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hazle&lt;/span&gt; &lt;span class="n"&gt;saber&lt;/span&gt; &lt;span class="n"&gt;al&lt;/span&gt; &lt;span class="n"&gt;usuario&lt;/span&gt; &lt;span class="n"&gt;que&lt;/span&gt; &lt;span class="n"&gt;no&lt;/span&gt; &lt;span class="n"&gt;se&lt;/span&gt; &lt;span class="n"&gt;encontr&lt;/span&gt;&lt;span class="err"&gt;ó&lt;/span&gt; &lt;span class="n"&gt;informaci&lt;/span&gt;&lt;span class="err"&gt;ó&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="n"&gt;relevante&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;

                                    &lt;span class="n"&gt;Al&lt;/span&gt; &lt;span class="n"&gt;final&lt;/span&gt; &lt;span class="n"&gt;de&lt;/span&gt; &lt;span class="n"&gt;tu&lt;/span&gt; &lt;span class="n"&gt;respuesta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;incluye&lt;/span&gt; &lt;span class="n"&gt;el&lt;/span&gt; &lt;span class="n"&gt;nombre&lt;/span&gt; &lt;span class="n"&gt;del&lt;/span&gt; &lt;span class="n"&gt;contenido&lt;/span&gt; &lt;span class="n"&gt;utilizado&lt;/span&gt; &lt;span class="n"&gt;como&lt;/span&gt; &lt;span class="n"&gt;referencia&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;

                                    &lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SearchPlugin&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;GetTextSearchResults&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)}}&lt;/span&gt;  
                                        &lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="n"&gt;each&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;  
                                        &lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
                                        &lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
                                        &lt;span class="p"&gt;-----------------&lt;/span&gt;
                                        &lt;span class="p"&gt;{{/&lt;/span&gt;&lt;span class="n"&gt;each&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;  
                                    &lt;span class="p"&gt;{{/&lt;/span&gt;&lt;span class="k"&gt;with&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;  

                                    &lt;span class="n"&gt;Pregunta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

                                    &lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
                                    &lt;span class="s"&gt;""";
&lt;/span&gt;
            &lt;span class="n"&gt;KernelArguments&lt;/span&gt; &lt;span class="n"&gt;arguments&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"query"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Query&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

            &lt;span class="n"&gt;HandlebarsPromptTemplateFactory&lt;/span&gt; &lt;span class="n"&gt;promptTemplateFactory&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;kernel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;InvokePromptAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;promptTemplate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;templateFormat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;HandlebarsPromptTemplateFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HandlebarsTemplateFormat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;promptTemplateFactory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;promptTemplateFactory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Explicación del Código&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Handler&lt;/code&gt;&lt;/strong&gt;: Esta clase maneja la lógica de la consulta, invoca el plugin de búsqueda, crea el prompt con el contexto adecuado y genera la respuesta. Recibe un objeto &lt;strong&gt;&lt;code&gt;Kernel&lt;/code&gt;&lt;/strong&gt; y una instancia de &lt;strong&gt;&lt;code&gt;ITextSearch&lt;/code&gt;&lt;/strong&gt; que se utiliza para realizar la búsqueda de contenido relevante en los posts del blog.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;searchPlugin&lt;/code&gt;&lt;/strong&gt;: Este es el plugin de búsqueda que se crea utilizando el método &lt;strong&gt;&lt;code&gt;CreateWithGetTextSearchResults&lt;/code&gt;&lt;/strong&gt;. Este plugin se encarga de realizar la búsqueda semántica en los posts y recuperar los resultados más relevantes. El plugin se añade al &lt;strong&gt;&lt;code&gt;Kernel&lt;/code&gt;&lt;/strong&gt; para que pueda ser invocado cuando sea necesario.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;promptTemplate&lt;/code&gt;&lt;/strong&gt;: Es el template que se utilizará para generar el texto. En este caso, se utiliza &lt;strong&gt;Handlebars&lt;/strong&gt; para permitir la inserción de los resultados de la búsqueda dentro del prompt. La idea es que el modelo genere una respuesta usando solamente los contenidos encontrados en la búsqueda. El template también incluye instrucciones para que el modelo devuelva el nombre y el valor de los posts utilizados como referencia.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{{#with (SearchPlugin-GetTextSearchResults query)}}  
    {{#each this}}  
    Name: {{Name}}  
    Value: {{Value}}  
    -----------------
    {{/each}}  
{{/with}}  
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Al final, se agrega la pregunta del usuario para que el modelo la utilice como parte de la respuesta:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Pregunta:
{{query}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;KernelArguments&lt;/code&gt;&lt;/strong&gt;: Este objeto contiene los argumentos que se pasan al modelo, en este caso, la consulta realizada por el usuario.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;InvokePromptAsync&lt;/code&gt;&lt;/strong&gt;: Este método invoca el modelo utilizando el template y los argumentos proporcionados, lo que genera una respuesta basada en los contenidos recuperados.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Flujo Completo&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;El usuario hace una consulta&lt;/strong&gt; (por ejemplo, "¿Qué es Clean Architecture?").&lt;/li&gt;
&lt;li&gt;El &lt;strong&gt;&lt;code&gt;Kernel&lt;/code&gt;&lt;/strong&gt; busca los posts relevantes utilizando el &lt;strong&gt;plugin de búsqueda vectorial&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;El &lt;strong&gt;prompt personalizado&lt;/strong&gt; es generado e incluye los resultados de la búsqueda.&lt;/li&gt;
&lt;li&gt;El &lt;strong&gt;modelo LLM&lt;/strong&gt; genera una respuesta utilizando los posts recuperados como contexto.&lt;/li&gt;
&lt;li&gt;La &lt;strong&gt;respuesta generada&lt;/strong&gt; se devuelve al usuario, incluyendo la referencia a los contenidos utilizados.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Este enfoque permite mejorar la calidad de las respuestas generadas por el modelo, ya que se aprovecha la información más actualizada y relevante disponible en el dominio. Además, al usar el contenido de los posts, el modelo tiene un contexto más específico y adaptado a las necesidades del usuario, lo que mejora la relevancia y precisión de la respuesta.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Probando el Endpoint&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Una vez que hemos configurado y creado nuestro endpoint, es hora de probarlo. Para ello, podemos hacer una solicitud HTTP como la siguiente, que ilustrará cómo interactuar con el servicio de generación de texto mediante la integración de RAG.&lt;/p&gt;

&lt;p&gt;Ejemplo de solicitud HTTP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;###
@Question=cuál es el objetivo principal de clean architecture?
GET {{SemanticKernelLearning03.ApiService_HostAddress}}/api/blog-posts/qa?Query={{Question}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cuando se ejecuta esta solicitud, obtendremos una respuesta estructurada que incluye no solo la respuesta generada por el modelo, sino también la referencia al contenido utilizado para generar esa respuesta.&lt;/p&gt;

&lt;p&gt;Respuesta esperada:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;El objetivo principal de Clean Architecture es promover la separación de preocupaciones y la modularidad en el diseño de software, lo que permite a las aplicaciones ser más mantenibles y escalables a lo largo del tiempo.

Reference: Clean Architecture: Diseño de Software Modular y Escalable.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;En este caso, la respuesta del endpoint es completamente dinámica y se genera utilizando los resultados de búsqueda obtenidos de los posts del blog relacionados con la pregunta. Como puedes observar, no solo obtenemos un resultado que responde a la consulta, sino que también proporcionamos el nombre del contenido utilizado como referencia, lo cual permite al usuario conocer la fuente de la información generada.&lt;/p&gt;

&lt;p&gt;En el ejemplo, configuramos el &lt;code&gt;promptTemplate&lt;/code&gt; para que el modelo de lenguaje solo utilizara los contenidos relevantes encontrados a través de la búsqueda de texto vectorial. De esta forma, garantizamos que la generación del modelo sea específica y relevante para el contexto proporcionado por los posts del blog.&lt;/p&gt;

&lt;p&gt;Este template establece claramente la regla para que el modelo solo use los resultados de búsqueda como contexto, lo cual optimiza la relevancia de las respuestas generadas.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Escalabilidad y Flexibilidad del Prompt&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;El prompt puede ser adaptado para ser más específico y mejorar la calidad de las respuestas generadas, por ejemplo, agregando instrucciones más detalladas sobre el formato de la respuesta o proporcionando una estructura más precisa para guiar al modelo. La flexibilidad del sistema permite ajustar el nivel de detalle según la necesidad del caso de uso.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ejemplo de otro Prompt:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Enlista los puntos más importantes al querer optimizar consultas SQL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Resultado generado:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;¡Claro! A continuación, te presento los puntos más importantes para optimizar consultas SQL:

1. **Índices**: Los índices ayudan a acelerar la búsqueda de datos en una base de datos. Deben ser creados en las columnas que se utilizan en la cláusula WHERE o en la cláusula ORDER BY.
2. **Normalización y desnormalización de datos**: La normalización ayuda a reducir la cantidad de datos duplicados en una base de datos, mientras que la desnormalización puede mejorar el rendimiento en ciertas situaciones.
.
.
.

Referencia: Optimización de Consultas SQL en Aplicaciones .NET
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Como vemos, el modelo genera una lista de los puntos más importantes y proporciona la referencia adecuada para la información. Este tipo de integración con Semantic Kernel y RAG hace que sea posible tener respuestas altamente relevantes y específicas basadas en contenido actual y relacionado.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusión
&lt;/h2&gt;

&lt;p&gt;A lo largo de este post, hemos aprendido a configurar y utilizar Semantic Kernel para aprovechar el poder de los embeddings y la técnica de Retrieval-Augmented Generation (RAG). Desde la generación y almacenamiento de vectores en Qdrant, hasta la realización de búsquedas semánticas y la generación de respuestas contextualizadas mediante prompts personalizados, hemos visto cómo combinar distintas tecnologías para crear soluciones de IA avanzadas y especializadas.&lt;/p&gt;

&lt;p&gt;Esta integración no solo mejora la precisión y relevancia de las respuestas, sino que también demuestra el potencial de unir funciones semánticas, RAG y bases de datos vectoriales para abordar desafíos reales en el manejo de información. Con estas herramientas, podrás desarrollar aplicaciones que se adaptan al contexto de tus datos, ofreciendo respuestas dinámicas y actualizadas.&lt;/p&gt;

&lt;p&gt;¡El camino hacia aplicaciones inteligentes y contextualmente precisas está abierto! Sigue experimentando y optimizando estas técnicas para llevar tus proyectos al siguiente nivel.&lt;/p&gt;

&lt;h2&gt;
  
  
  Referencias
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/semantic-kernel/concepts/text-search/out-of-the-box-textsearch/vectorstore-textsearch?pivots=programming-language-csharp" rel="noopener noreferrer"&gt;Using the Semantic Kernel Vector Store text search (Preview) | Microsoft Learn&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/semantic-kernel/concepts/text-search/text-search-plugins?pivots=programming-language-csharp" rel="noopener noreferrer"&gt;Semantic Kernel Text Search Plugins (Preview) | Microsoft Learn&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/semantic-kernel/concepts/text-search/text-search-vector-stores?pivots=programming-language-csharp" rel="noopener noreferrer"&gt;Semantic Kernel Text Search with Vector Stores (Preview) | Microsoft Learn&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/semantic-kernel/concepts/prompts/handlebars-prompt-templates?pivots=programming-language-csharp" rel="noopener noreferrer"&gt;Using the Handlebars prompt template language | Microsoft Learn&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>csharp</category>
      <category>semantickernel</category>
      <category>ollama</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>Extendiendo Semantic Kernel: Creando Plugins para Consultas Dinámicas</title>
      <dc:creator>Isaac Ojeda</dc:creator>
      <pubDate>Thu, 16 Jan 2025 17:34:53 +0000</pubDate>
      <link>https://dev.to/isaacojeda/extendiendo-semantic-kernel-creando-plugins-para-consultas-dinamicas-56j</link>
      <guid>https://dev.to/isaacojeda/extendiendo-semantic-kernel-creando-plugins-para-consultas-dinamicas-56j</guid>
      <description>&lt;h3&gt;
  
  
  &lt;strong&gt;1. Introducción&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;En el tutorial &lt;a href="https://dev.to/isaacojeda/semantic-kernel-crea-un-api-para-generacion-de-texto-con-ollama-y-aspire-686"&gt;anterior&lt;/a&gt;, exploramos cómo configurar y utilizar &lt;strong&gt;Semantic Kernel&lt;/strong&gt; junto con &lt;strong&gt;Aspire&lt;/strong&gt; y &lt;strong&gt;Ollama&lt;/strong&gt; para crear un API básico que generaba resúmenes de texto. Ahora, vamos a dar un paso más y enfocarnos en una de las características más potentes de Semantic Kernel: los &lt;strong&gt;plugins&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;En este tutorial, aprenderás a crear y utilizar plugins en Semantic Kernel. Los plugins son clases o componentes que encapsulan funciones específicas, permitiendo extender las capacidades del kernel con nuevas habilidades personalizadas.&lt;/p&gt;

&lt;p&gt;Para ejemplificarlo, desarrollaremos dos plugins:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Un plugin que retorna la hora actual en formato UTC.&lt;/li&gt;
&lt;li&gt;Un plugin que utiliza datos de geolocalización y clima para proporcionar información meteorológica de una ciudad específica.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Los plugins en Semantic Kernel permiten agregar habilidades modulares y reutilizables. Esto no solo facilita la escalabilidad del proyecto, sino que también abre las puertas para integrar servicios externos o lógica personalizada de manera sencilla y eficiente.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Nota 💡: Aquí podrás encontrar el código fuente de este tutorial: &lt;a href="https://github.com/isaacOjeda/DevToPosts/tree/main/SemanticKernelSeries/SemanticKernelLearning02" rel="noopener noreferrer"&gt;DevToPosts/SemanticKernelSeries/SemanticKernelLearning02 at main · isaacOjeda/DevToPosts&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;2. Qué son los Plugins en Semantic Kernel&lt;/strong&gt;
&lt;/h3&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;¿Qué es un Plugin en Semantic Kernel?&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;Un &lt;strong&gt;plugin&lt;/strong&gt; en Semantic Kernel es una clase que contiene métodos decorados con atributos especiales, como &lt;code&gt;[KernelFunction]&lt;/code&gt;, para exponer su funcionalidad al kernel. Esto permite que esas funciones sean llamadas por el kernel como si fueran partes integradas de su sistema.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;¿Por qué usar Plugins?&lt;/strong&gt;
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Modularidad:&lt;/strong&gt; Los plugins encapsulan lógica específica, lo que los hace fáciles de mantener y reutilizar.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flexibilidad:&lt;/strong&gt; Puedes crear plugins para realizar tareas como interactuar con APIs externas, procesar datos o realizar cálculos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integración sencilla:&lt;/strong&gt; Los plugins se registran fácilmente en el kernel, haciéndolos disponibles para su uso en el API o dentro de flujos más complejos.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;Componentes clave de un Plugin&lt;/strong&gt;
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Métodos decorados con &lt;code&gt;[KernelFunction]&lt;/code&gt;:&lt;/strong&gt; Esto marca las funciones que el kernel puede invocar.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Descripción:&lt;/strong&gt; Los métodos pueden incluir descripciones para documentar su propósito, facilitando su descubrimiento.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dependencias externas:&lt;/strong&gt; Los plugins pueden interactuar con servicios externos, como APIs, utilizando patrones conocidos como &lt;code&gt;IHttpClientFactory&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;Plugins que construiremos en este tutorial&lt;/strong&gt;
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;TimeInformationService:&lt;/strong&gt; Un plugin que proporciona la hora actual en UTC.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WeatherInformationService:&lt;/strong&gt; Un plugin que utiliza una API externa para obtener información de geolocalización y clima.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ambos plugins serán integrados en nuestro kernel y expuestos a través del API que desarrollamos en el tutorial anterior.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;¿Cómo funcionan los Plugins en Semantic Kernel?&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;Los plugins registrados en el kernel pueden ser invocados en tiempo de ejecución mediante prompts o directamente desde el código. El kernel gestiona automáticamente la ejecución de estos métodos, permitiendo integrarlos en flujos de procesamiento complejos.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;3. Desarrollo del Ejemplo&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Ahora que entendemos qué son los plugins en &lt;strong&gt;Semantic Kernel&lt;/strong&gt;, vamos a desarrollar e integrar dos plugins personalizados en nuestro proyecto:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;TimeInformationService:&lt;/strong&gt; Un plugin simple que devuelve la hora actual en UTC.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WeatherInformationService:&lt;/strong&gt; Un plugin más avanzado que utiliza una API externa para obtener información sobre el clima en una ciudad.&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;Paso 1: Crear los Plugins&lt;/strong&gt;
&lt;/h4&gt;

&lt;h5&gt;
  
  
  &lt;strong&gt;TimeInformationService&lt;/strong&gt;
&lt;/h5&gt;

&lt;p&gt;El siguiente plugin proporciona la hora actual en formato UTC. Es un ejemplo sencillo para entender cómo funcionan los plugins básicos en Semantic Kernel.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;/// &amp;lt;summary&amp;gt;&lt;/span&gt;
&lt;span class="c1"&gt;/// A plugin that returns the current time.&lt;/span&gt;
&lt;span class="c1"&gt;/// &amp;lt;/summary&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TimeInformationService&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;KernelFunction&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Retrieves the current time in UTC."&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;GetCurrentUtcTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UtcNow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"R"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Aquí destacamos:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;El atributo &lt;code&gt;[KernelFunction]&lt;/code&gt; expone el método al kernel.&lt;/li&gt;
&lt;li&gt;El método devuelve la hora en formato "R" (RFC1123), que es legible y estándar.&lt;/li&gt;
&lt;/ul&gt;

&lt;h5&gt;
  
  
  &lt;strong&gt;WeatherInformationService&lt;/strong&gt;
&lt;/h5&gt;

&lt;p&gt;Este plugin es más complejo. Obtiene información sobre una ciudad (coordenadas, país, etc.) y también el clima actual utilizando servicios externos.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;WeatherInformationService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IHttpClientFactory&lt;/span&gt; &lt;span class="n"&gt;httpClientFactory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;KernelFunction&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Get weather information for a city."&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;WeatherInfo&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetWeatherByCity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;cityName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cityName&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ArgumentException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"City name cannot be null or empty."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cityName&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

        &lt;span class="c1"&gt;// Step 1: Get city coordinates&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;GetCityCoordinatesAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cityName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Step 2: Get weather data&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;weatherResult&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;GetWeatherDataAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Latitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Longitude&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;WeatherInfo&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;City&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Country&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Temperature&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;weatherResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CurrentWeather&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Temperature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;WindSpeed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;weatherResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CurrentWeather&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Windspeed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;WeatherCode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;weatherResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CurrentWeather&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Weathercode&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;GeocodingResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetCityCoordinatesAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;cityName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// ...For more info, check the source code&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;WeatherApiResponse&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetWeatherDataAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// ...For more info, check the source code&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Puntos clave:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Este plugin utiliza &lt;code&gt;IHttpClientFactory&lt;/code&gt; para realizar llamadas HTTP a APIs externas.

&lt;ul&gt;
&lt;li&gt;Como puedes ver, la inyección de dependencias funciona sin problema, por lo que aquí podríamos inyectar lo que se necesite que el plugin realice&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;El Kernel utiliza los nombres de los métodos y los parámetros para darles significado y así usarlo según el prompt solicitado.&lt;/li&gt;

&lt;/ul&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;Paso 2: Registrar los Plugins en el Kernel&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;Para que el kernel pueda usar estos plugins, debemos registrarlos en la configuración del servicio:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;kernel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddKernel&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Registrar el servicio de hora&lt;/span&gt;
&lt;span class="n"&gt;kernel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ImportPlugin&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TimeInformationService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Registrar el servicio de clima&lt;/span&gt;
&lt;span class="n"&gt;kernel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ImportPlugin&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;WeatherInformationService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  &lt;strong&gt;Paso 3: Exponer los Plugins Utilizando el Kernel&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;En lugar de llamar directamente a los métodos de los servicios, utilizaremos el &lt;strong&gt;Kernel&lt;/strong&gt; de Semantic Kernel para invocar los plugins mediante prompts. Esto nos permitirá combinar capacidades y hacer el uso más dinámico de los plugins.&lt;/p&gt;

&lt;p&gt;Haremos la prueba con un simple endpoint que invoque el prompt&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/chat"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ChatRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Kernel&lt;/span&gt; &lt;span class="n"&gt;kernel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;OpenAIPromptExecutionSettings&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;FunctionChoiceBehavior&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FunctionChoiceBehavior&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Auto&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="c1"&gt;// Aquí podríamos hacer un Prompt más explícito para el modelo si nuestra intención fuera&lt;/span&gt;
    &lt;span class="c1"&gt;// solo contestar preguntas sobre el clima.&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;kernel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;InvokePromptAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  &lt;strong&gt;Explicación del Código&lt;/strong&gt;
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Uso del Kernel&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;El método &lt;code&gt;InvokePromptAsync&lt;/code&gt; utiliza el kernel para procesar el prompt y determinar qué función de los plugins debe ejecutarse.&lt;/li&gt;
&lt;li&gt;En este caso, el Kernel automáticamente seleccionará el método correcto (&lt;code&gt;GetCurrentUtcTime&lt;/code&gt; o &lt;code&gt;GetWeatherByCity&lt;/code&gt;) basado en el texto del prompt.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integración Flexible&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;La estructura permite enviar cualquier pregunta o texto, dejando que el Kernel interprete y seleccione el método adecuado.

&lt;ul&gt;
&lt;li&gt;También podemos hacer prompts donde seamos explícitos de que Plugin queremos que el modelo utilice, pero por ahora lo dejamos en automático y que el modelo decida.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;4. Probar los Endpoints&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Con los endpoints configurados y los plugins integrados en el &lt;strong&gt;Kernel&lt;/strong&gt;, es momento de validarlos y observar cómo funcionan.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;Ejemplo 1: Consultar el Clima&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;Utilizando &lt;strong&gt;Postman&lt;/strong&gt;, envía la siguiente solicitud al endpoint configurado:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt; &lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"question"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Cuál es el clima actual en Londres"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Al depurar la aplicación, notarás que el modelo entiende la intención del prompt, lo asocia con nuestro plugin y determina que queremos el clima actual de Londres. Esto sucede porque hemos registrado una función específica que devuelve esta información:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb1omsz3jsf1r2gqu742m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb1omsz3jsf1r2gqu742m.png" alt="Image description" width="800" height="158"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;La respuesta sería algo similar a:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt; &lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt; &lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"El clima actual en Londres es frío y nublado, con una temperatura de 8.6°C y un viento que sopla a 6.8 km/h. La codificación del tiempo es 3, lo que indica condiciones climáticas mezcladas, con algunas nubes y posibles lluvias."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  &lt;strong&gt;Ejemplo 2: Consultar Información de una Ciudad&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;Ahora, realiza una consulta diferente, como esta:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt; &lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"question"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Cuál es la población actual de Madrid?"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;En este caso, el modelo invocará automáticamente otra función registrada en el plugin que se encarga de obtener información sobre una ciudad:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7pf76oqlglt0322h9whj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7pf76oqlglt0322h9whj.png" alt="Image description" width="800" height="144"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;La respuesta será algo como:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt; &lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt; &lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"La población actual de Madrid es de aproximadamente 3,255,594 personas, según la información proporcionada por el servicio de información geográfica."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;¿Notas el poder detrás de esta integración? Con esta configuración, el &lt;strong&gt;Kernel&lt;/strong&gt; puede realizar tareas que antes requerían múltiples servicios separados. Todo se reduce a cómo estructuramos los prompts y las funciones de nuestros plugins.&lt;/p&gt;

&lt;p&gt;¡Y esto es solo el principio! Aún no hemos explorado características avanzadas como &lt;strong&gt;memoria semántica&lt;/strong&gt; y &lt;strong&gt;RAG (Retrieval-Augmented Generation)&lt;/strong&gt;, pero con lo que ya tenemos, podemos construir soluciones extremadamente flexibles y personalizadas para cualquier dominio o industria.&lt;/p&gt;

&lt;p&gt;Con prompts más específicos, puedes resolver problemas complejos y automatizar procesos avanzados. La única limitación es tu creatividad y las necesidades de tu proyecto.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;5. Extender Funcionalidades y Consideraciones Finales&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Ahora que hemos visto cómo registrar plugins y probarlos con el &lt;strong&gt;Kernel&lt;/strong&gt;, reflexionemos sobre cómo podemos extender estas funcionalidades para adaptarlas a necesidades más complejas. Aquí algunas ideas y recomendaciones para el siguiente nivel:&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;Incorporar Más Plugins&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;La flexibilidad de &lt;strong&gt;Semantic Kernel&lt;/strong&gt; nos permite agregar más funciones a nuestros plugins o crear nuevos plugins para cubrir otras áreas de interés, como:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Análisis de texto:&lt;/strong&gt; Procesar grandes volúmenes de datos textuales para obtener resúmenes, extraer palabras clave o identificar entidades.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integraciones externas:&lt;/strong&gt; Conectar con APIs de terceros para realizar tareas como manejo de correos electrónicos, acceso a sistemas empresariales o realizar transacciones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Procesamiento de imágenes:&lt;/strong&gt; Usar servicios como OCR para reconocer texto en imágenes o realizar análisis visual.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;Usar Memoria Semántica&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;La &lt;strong&gt;memoria semántica&lt;/strong&gt; de Semantic Kernel puede almacenar y recuperar información contextualmente relevante. Por ejemplo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Recordar interacciones previas para mejorar la experiencia del usuario.&lt;/li&gt;
&lt;li&gt;Contextualizar consultas complejas basándose en datos históricos.&lt;/li&gt;
&lt;li&gt;Implementar personalización profunda en aplicaciones que requieran un historial de acciones o preferencias.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;Optimización de Prompts&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;Un diseño adecuado de prompts es clave para mejorar la precisión de los resultados. Algunas recomendaciones:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sea específico:&lt;/strong&gt; Prompts detallados producen resultados más enfocados.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use ejemplos:&lt;/strong&gt; Proporcionar ejemplos en el prompt puede ayudar al modelo a entender la intención de manera más clara.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evalúe y ajuste:&lt;/strong&gt; Experimente con diferentes configuraciones para obtener mejores resultados en diferentes escenarios.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;Escalabilidad y Producción&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;Si bien en estos ejemplos usamos &lt;strong&gt;Ollama&lt;/strong&gt; localmente para evitar problemas de infraestructura, en un entorno de producción puedes cambiar sin modificar tu código base y usar servicios como &lt;strong&gt;OpenAI&lt;/strong&gt; o &lt;strong&gt;Azure OpenAI&lt;/strong&gt; para aprovechar su escalabilidad y confiabilidad. Esto implica:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Definir claves API para entornos seguros.&lt;/li&gt;
&lt;li&gt;Configurar políticas de acceso según las necesidades de tu organización.&lt;/li&gt;
&lt;li&gt;Monitorear el uso y los costos para optimizar el rendimiento.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;Casos de Uso Empresariales&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;Con los elementos ya implementados, podemos visualizar algunos casos prácticos donde estas capacidades sean útiles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Asistentes de soporte:&lt;/strong&gt; Resolver consultas comunes en tiempo real con información siempre actualizada.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatización de tareas repetitivas:&lt;/strong&gt; Por ejemplo, generar reportes basados en datos o notificaciones programadas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sistemas personalizados:&lt;/strong&gt; Aplicaciones inteligentes que se adapten dinámicamente a las necesidades de los usuarios, como dashboards interactivos o chatbots avanzados.
### &lt;strong&gt;Conclusión&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Con &lt;strong&gt;Semantic Kernel&lt;/strong&gt;, hemos transformado lo que tradicionalmente sería una combinación de servicios aislados en una solución unificada y potente. Desde entender intenciones en lenguaje natural hasta ejecutar acciones basadas en plugins específicos, el Kernel nos proporciona una plataforma flexible y extensible para la creación de aplicaciones inteligentes.&lt;/p&gt;

&lt;p&gt;La verdadera ventaja de esta tecnología radica en su capacidad de evolución. Puedes comenzar con casos de uso simples, como los ejemplos de este tutorial, y escalar hacia soluciones más complejas que integren múltiples fuentes de datos, memoria semántica y funcionalidades avanzadas.&lt;/p&gt;

&lt;p&gt;Tu creatividad y las necesidades de tu proyecto dictarán el camino. Ahora que tienes las bases, ¿qué construirás con &lt;strong&gt;Semantic Kernel&lt;/strong&gt;?&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>ai</category>
      <category>semantickernel</category>
      <category>ollama</category>
    </item>
    <item>
      <title>Semantic Kernel: Crea un API para Generación de Texto con Ollama y Aspire</title>
      <dc:creator>Isaac Ojeda</dc:creator>
      <pubDate>Tue, 14 Jan 2025 20:18:42 +0000</pubDate>
      <link>https://dev.to/isaacojeda/semantic-kernel-crea-un-api-para-generacion-de-texto-con-ollama-y-aspire-686</link>
      <guid>https://dev.to/isaacojeda/semantic-kernel-crea-un-api-para-generacion-de-texto-con-ollama-y-aspire-686</guid>
      <description>&lt;h3&gt;
  
  
  &lt;strong&gt;1. Introducción&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;En este tutorial, aprenderás cómo usar &lt;strong&gt;Semantic Kernel&lt;/strong&gt; para integrar Large Language Models (LLM) y construir un servicio REST que resuma texto. Utilizaremos &lt;strong&gt;Ollama&lt;/strong&gt; como motor local de modelos de lenguaje, lo que nos permitirá evitar el uso de servicios en la nube en esta fase de desarrollo.&lt;/p&gt;

&lt;p&gt;El objetivo principal es configurar un entorno de trabajo funcional en C# que permita a los desarrolladores:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Conectar un modelo de lenguaje local (&lt;em&gt;llama3.2&lt;/em&gt;).&lt;/li&gt;
&lt;li&gt;Crear un kernel para manejar tareas personalizadas.&lt;/li&gt;
&lt;li&gt;Exponer la funcionalidad como un endpoint REST en una API ASP.NET Core.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Al finalizar, tendrás un servicio de API que podrá recibir texto como entrada y devolver un resumen en una sola oración. Esto no solo te ayudará a entender cómo usar Semantic Kernel, sino que también te dará una base sólida para construir aplicaciones prácticas basadas en IA generativa.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;2. Introducción a Semantic Kernel&lt;/strong&gt;
&lt;/h3&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;2.1. ¿Qué es Semantic Kernel?&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;Semantic Kernel es un SDK de código abierto que permite a los desarrolladores crear sus propios agentes personalizados de inteligencia artificial (IA). Al combinar modelos de lenguaje de gran escala (LLMs) con código nativo, los desarrolladores pueden crear agentes de IA que entiendan y respondan a solicitudes en lenguaje natural para realizar una variedad de tareas.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;2.2 ¿Qué es un agente de IA?&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;Un agente de IA es un programa diseñado para alcanzar objetivos predeterminados. Los agentes de IA están impulsados por modelos de lenguaje de gran escala (LLMs) entrenados con cantidades masivas de datos. Estos agentes pueden completar una amplia variedad de tareas con mínima o ninguna intervención humana. Algunos ejemplos de lo que pueden hacer los agentes de IA incluyen:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Escribir código.&lt;/li&gt;
&lt;li&gt;Redactar correos electrónicos.&lt;/li&gt;
&lt;li&gt;Resumir reuniones.&lt;/li&gt;
&lt;li&gt;Proporcionar recomendaciones.&lt;/li&gt;
&lt;li&gt;¡Y mucho más!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Semantic Kernel integra modelos de lenguaje como OpenAI, Azure OpenAI y Hugging Face con lenguajes de programación convencionales como C#, Python y Java. Los desarrolladores pueden crear "plugins" para interactuar con los LLMs y realizar diversas tareas. Además, el SDK de Semantic Kernel incluye plugins predefinidos que pueden mejorar rápidamente una aplicación. Esto permite que los desarrolladores utilicen LLMs en sus propias aplicaciones sin necesidad de aprender los detalles específicos de la API del modelo.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;2.3 Componentes clave del SDK de Semantic Kernel&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Capa de orquestación de IA&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
El núcleo del stack de Semantic Kernel es una capa de orquestación de IA que permite la integración fluida de modelos de IA y plugins. Esta capa combina estos componentes para crear interacciones innovadoras con los usuarios.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conectores&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
El SDK ofrece un conjunto de conectores que permiten a los desarrolladores integrar LLMs en sus aplicaciones existentes. Estos conectores actúan como un puente entre el código de la aplicación y los modelos de IA.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plugins&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
El SDK opera con plugins, que funcionan como el "cuerpo" de la aplicación de IA. Los plugins incluyen solicitudes (prompts) que el modelo de IA debe responder y funciones para realizar tareas especializadas. Los desarrolladores pueden usar plugins predefinidos o crear los suyos propios.&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;strong&gt;3. Desarrollo del Ejemplo&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;En esta sección, construiremos un API REST que utiliza &lt;strong&gt;Semantic Kernel&lt;/strong&gt; y el modelo de lenguaje &lt;strong&gt;llama3.2&lt;/strong&gt; de &lt;strong&gt;Ollama&lt;/strong&gt; para resumir texto en una sola oración. El objetivo es entender cómo configurar el entorno y crear una habilidad básica que pueda ser utilizada desde cualquier aplicación a través de un endpoint.&lt;/p&gt;
&lt;h4&gt;
  
  
  &lt;strong&gt;3.1. Configuración con Aspire&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;Antes de entrar en los detalles del API, configuramos el entorno utilizando &lt;strong&gt;Aspire&lt;/strong&gt;. Este framework facilita la construcción de aplicaciones distribuidas, simplificando la gestión de dependencias y servicios.&lt;/p&gt;

&lt;p&gt;Lo que he hecho para simplificar la configuración, es utilizar Visual Studio para crear la solución ejemplo de Aspire con .NET 9:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9av06ysmd1bwoiyyj7is.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9av06ysmd1bwoiyyj7is.png" alt="Image description" width="624" height="657"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Esta plantilla, también incluye un proyecto de Blazor, en este tutorial no lo utilizaremos, pero al finalizar, puedes utilizar ese proyecto para consumir los endpoints realizados y ya tener una aplicación funcional.&lt;/p&gt;

&lt;p&gt;El código de Aspire tiene los siguientes propósitos:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Configurar Ollama:&lt;/strong&gt; Define el modelo &lt;code&gt;llama3.2&lt;/code&gt; como el motor de lenguaje. Esto nos permite trabajar con un modelo local, eliminando la necesidad de infraestructura compleja en desarrollo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Modularidad:&lt;/strong&gt; Cada proyecto (como &lt;code&gt;SemanticKernelLearning01_ApiService&lt;/code&gt;) se configura como un módulo independiente, permitiendo una estructura clara y extensible.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Nota:&lt;/strong&gt; Usar Ollama local es ideal para pruebas y desarrollo, ya que no requiere configurar una infraestructura en la nube. Sin embargo, en producción puedes cambiar a OpenAI o Azure OpenAI sin modificar el código, aprovechando la flexibilidad de Semantic Kernel.&lt;/p&gt;
&lt;h4&gt;
  
  
  &lt;strong&gt;Código de Aspire:&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;Primero necesitamos los siguientes paquetes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;  &lt;span class="nt"&gt;&amp;lt;ItemGroup&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Aspire.Hosting.AppHost"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"9.0.0"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"CommunityToolkit.Aspire.Hosting.Ollama"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"9.1.0"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/ItemGroup&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DistributedApplication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;ollama&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;builder&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddOllama&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ollama"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="c1"&gt;// &amp;lt;-- Utilizará docker para usar ollama y sus modelos &lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithDataVolume&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;// &amp;lt;-- Volumen de Docker para persistir modelos descargados&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithOpenWebUI&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// &amp;lt;-- UI Estilo ChatGPT&lt;/span&gt;

&lt;span class="c1"&gt;// Descarga el modelo llama3.2 con nombre "llama"&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;llamaModel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ollama&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"llama"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"llama3.2"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Nuestra API depende de Ollama, por lo que se referencia y espera a que esté listo&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddProject&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Projects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SemanticKernelLearning01_ApiService&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"apiservice"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithReference&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;llamaModel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WaitFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;llamaModel&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Aquí configuramos Ollama como motor de generación de texto y vinculamos el modelo &lt;code&gt;llama3.2&lt;/code&gt;. También iniciamos el servicio API como un módulo dentro del proyecto.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;3.2. Configuración del API REST&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;En el siguiente bloque de código, configuramos un API REST utilizando &lt;strong&gt;ASP.NET Core&lt;/strong&gt; y &lt;strong&gt;Semantic Kernel&lt;/strong&gt;. Aquí se define un endpoint que acepta texto como entrada y devuelve su resumen generado por el modelo de lenguaje.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Puntos clave del código:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Kernel y Ollama:&lt;/strong&gt; Se inicializa el kernel de Semantic Kernel y se conecta con Ollama usando la configuración definida en Aspire.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Habilidad de Resumen:&lt;/strong&gt; La lógica para resumir texto se encapsula en un prompt que el modelo interpreta para generar respuestas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exposición del Endpoint:&lt;/strong&gt; Se define un endpoint &lt;code&gt;/api/summarizer&lt;/code&gt; que recibe un objeto JSON con el texto a resumir y devuelve el resultado.&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;Código del API REST:&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;Paquetes necesarios:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;  &lt;span class="nt"&gt;&amp;lt;ItemGroup&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.AspNetCore.OpenApi"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"9.0.0"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.SemanticKernel"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"1.33.0"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;PackageReference&lt;/span&gt; &lt;span class="na"&gt;Include=&lt;/span&gt;&lt;span class="s"&gt;"Microsoft.SemanticKernel.Connectors.Ollama"&lt;/span&gt; &lt;span class="na"&gt;Version=&lt;/span&gt;&lt;span class="s"&gt;"1.33.0-alpha"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/ItemGroup&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Program.cs&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;WebApplication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddServiceDefaults&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddProblemDetails&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddOpenApi&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;kernel&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddKernel&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;modelId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GetOllamaConnectionString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="cp"&gt;#pragma warning disable SKEXP0070
&lt;/span&gt;&lt;span class="n"&gt;kernel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddOllamaTextGeneration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;modelId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Está en alpha, puede cambiar.&lt;/span&gt;
&lt;span class="cp"&gt;#pragma warning restore SKEXP0070
&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseExceptionHandler&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsDevelopment&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapOpenApi&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapDefaultEndpoints&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/summarizer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;TextCompletionRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ITextGenerationService&lt;/span&gt; &lt;span class="n"&gt;textGenerationService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;$"""
&lt;/span&gt;                 &lt;span class="n"&gt;Summarize&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;following&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;one&lt;/span&gt; &lt;span class="n"&gt;sentence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  

                 &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
                 &lt;span class="s"&gt;""";
&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;textGenerationService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetTextContentsAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  &lt;strong&gt;3.3. Explicación General del Flujo&lt;/strong&gt;
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Creación del Kernel:&lt;/strong&gt;
Se configura el kernel para usar Ollama como motor de generación de texto con el modelo &lt;code&gt;llama3.2&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Definición del Endpoint:&lt;/strong&gt;
El endpoint &lt;code&gt;/api/summarizer&lt;/code&gt; recibe un JSON con una propiedad &lt;code&gt;Text&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generación del Resumen:&lt;/strong&gt;
El texto recibido se envía al modelo junto con un prompt que instruye al modelo para resumirlo en una oración.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Devolución del Resultado:&lt;/strong&gt;
El resumen generado se devuelve como respuesta al cliente que llamó al API.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Código restante:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Uri&lt;/span&gt; &lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;modelId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;GetOllamaConnectionString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Este ConnectionString es establecida por Aspire&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;connectionString&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetConnectionString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"llama"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;connectionBuilder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;DbConnectionStringBuilder&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;ConnectionString&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;connectionString&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="n"&gt;Uri&lt;/span&gt; &lt;span class="n"&gt;endpoint&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Uri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connectionBuilder&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Endpoint"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;modelId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;connectionBuilder&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Model"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;modelId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Simple DTO&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;TextCompletionRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Nota 💡: Recuerda que siempre puedes descargar el código desde desde este &lt;a href="https://github.com/isaacOjeda/DevToPosts/tree/main/SemanticKernelSeries/SemanticKernelLearning01" rel="noopener noreferrer"&gt;Repositorio&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;3.4 Probando la solución&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;Si corremos el proyecto de Aspire, automáticamente orquestará lo necesario para que la API pueda comunicarse con ollama:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftv7ykzjwjcw13l0q4l7h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftv7ykzjwjcw13l0q4l7h.png" alt="Image description" width="800" height="221"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Su primera ejecución será lenta, ya que tendrá que descargar el modelo &lt;code&gt;llama3.2&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Existen muchos modelos que podemos ejecutar, tanto para Text Generation y para Embeddings (revisado en próxmos post), puedes revisar más aquí: &lt;a href="https://ollama.com/" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Probemos el endpoint (utilizando Visual Studio y un archivo .http):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@host = https://localhost:7384

### Text Generation
POST {{host}}/api/summarizer
Content-Type: application/json

{
    "text": "Mariana found an ancient book in her grandfather’s library, with a bookmark pointing to a page that read, “Recite these words and your destiny will change.” Intrigued, she read it out loud, and in an instant, she found herself in a bustling market in a medieval town. \rNo one seemed surprised by her modern clothing; in fact, one merchant greeted her as if he knew her. As she searched for answers, an old man explained that she was the heir to a lost legacy and had to make a choice: stay and lead a kingdom on the brink of chaos, or return to her everyday life. \rWith the book in her hands, Mariana took a deep breath and closed her eyes, choosing the challenge she had secretly always wanted."
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Respuesta:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;    &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="err"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Mariana discovers an ancient book that transports her to a medieval town, where she learns she is the heir to a lost legacy and must make a choice between returning to her ordinary life or leading a kingdom on the brink of chaos."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;    &lt;/span&gt;&lt;span class="nl"&gt;"modelId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="err"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"llama3.2"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La ejecución de modelos con ollama claro que suele ser más lento, para acelerar su ejecución debemos de contar con una GPU y de preferencia Nvidia. &lt;/p&gt;

&lt;p&gt;Para operaciones sencillas, me ha funcionado bien y como vemos, la respuesta ha funcionado, todo ejecutandose en nuestra computadora.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;4. Conclusión&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;En este tutorial, exploramos los fundamentos de &lt;strong&gt;Semantic Kernel&lt;/strong&gt; y cómo integrarlo con &lt;strong&gt;Ollama&lt;/strong&gt; para construir un servicio funcional capaz de generar resúmenes de texto. A través de este ejercicio:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Aprendiste a configurar un entorno básico utilizando &lt;strong&gt;Aspire&lt;/strong&gt;, simplificando la administración de servicios distribuidos.&lt;/li&gt;
&lt;li&gt;Descubriste cómo conectar Semantic Kernel con un motor de lenguaje local, como &lt;strong&gt;llama3.2&lt;/strong&gt; de Ollama, para evitar complicaciones de infraestructura en desarrollo.&lt;/li&gt;
&lt;li&gt;Implementaste un API REST con un endpoint práctico que utiliza generación de texto como una habilidad central.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Este ejercicio muestra lo versátil que puede ser Semantic Kernel para crear aplicaciones inteligentes que aprovechan los modelos de lenguaje. Además, la flexibilidad de la arquitectura permite cambiar fácilmente entre diferentes proveedores de modelos, como OpenAI o Azure, sin necesidad de modificar el código base.&lt;/p&gt;

&lt;p&gt;Este tutorial es solo el inicio. A partir de aquí, puedes explorar y agregar nuevas habilidades, integrar bases de datos vectoriales para contextualizar las respuestas o expandir el API con capacidades adicionales.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;5. Próximos Pasos&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Ahora que ya tienes una base sólida para trabajar con &lt;strong&gt;Semantic Kernel&lt;/strong&gt; y &lt;strong&gt;Ollama&lt;/strong&gt;, considera avanzar con los siguientes temas para expandir tus conocimientos:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Ampliar tus habilidades en Semantic Kernel:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Aprende a crear habilidades más complejas con múltiples pasos y dependencias.&lt;/li&gt;
&lt;li&gt;Explora cómo usar conectores para integrar datos externos, como archivos, APIs, o bases de datos.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Incorporar contexto mediante bases de datos vectoriales:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Experimenta con herramientas como Qdrant o Pinecone para agregar contexto a las respuestas basadas en vectores.&lt;/li&gt;
&lt;li&gt;Usa embeddings para conectar preguntas con datos relevantes en tiempo real.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Migrar a un entorno de producción:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Configura la misma API para usar OpenAI o Azure OpenAI como motores de generación de texto.&lt;/li&gt;
&lt;li&gt;Aprende a manejar credenciales y seguridad en aplicaciones en producción.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Construir una interfaz gráfica:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Usa &lt;strong&gt;Blazor&lt;/strong&gt; para crear una interfaz interactiva donde los usuarios puedan interactuar con tus habilidades de Semantic Kernel.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Desarrollar tutoriales más avanzados:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Enseña a otros cómo resolver problemas reales, como análisis de sentimientos, clasificación de texto o generación de contenido adaptado al contexto.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>csharp</category>
      <category>ollama</category>
      <category>ai</category>
      <category>semantickernel</category>
    </item>
    <item>
      <title>Integration Tests en .NET con TestContainers: Pruebas de Base de Datos Aisladas</title>
      <dc:creator>Isaac Ojeda</dc:creator>
      <pubDate>Thu, 07 Nov 2024 21:23:19 +0000</pubDate>
      <link>https://dev.to/isaacojeda/integration-tests-en-net-con-testcontainers-pruebas-de-base-de-datos-aisladas-1pi6</link>
      <guid>https://dev.to/isaacojeda/integration-tests-en-net-con-testcontainers-pruebas-de-base-de-datos-aisladas-1pi6</guid>
      <description>&lt;h2&gt;
  
  
  Introducción a las Pruebas de Integración
&lt;/h2&gt;

&lt;p&gt;En el desarrollo de software, las pruebas de integración son esenciales para verificar que los distintos módulos de una aplicación trabajen correctamente en conjunto. A diferencia de las pruebas unitarias, que se centran en unidades individuales de código, las pruebas de integración se enfocan en asegurarse de que los componentes del sistema interactúan adecuadamente entre sí y, en muchos casos, dependen de servicios externos, como bases de datos, servicios de autenticación o APIs de terceros.&lt;/p&gt;

&lt;p&gt;En este artículo, vamos a trabajar con una Web API de ejemplo que utiliza SQL Server como base de datos. La idea es configurar un entorno de pruebas de integración utilizando &lt;a href="https://www.testcontainers.org/" rel="noopener noreferrer"&gt;TestContainers&lt;/a&gt;, una librería que permite levantar contenedores Docker de forma programática y en tiempo de ejecución, facilitando así la creación de entornos de prueba aislados y controlados.&lt;/p&gt;

&lt;p&gt;Para ejecutar nuestras pruebas, usaremos &lt;strong&gt;xUnit&lt;/strong&gt; como framework de testing y &lt;strong&gt;Respawn&lt;/strong&gt; para resetear la base de datos entre pruebas, lo que ayuda a garantizar que cada prueba comience con un estado limpio y consistente. Gracias a TestContainers, podemos levantar un contenedor de SQL Server específicamente para nuestras pruebas, eliminando la dependencia de una base de datos local o compartida y asegurando que el entorno sea reproducible en cualquier máquina.&lt;/p&gt;

&lt;p&gt;El código fuente completo de esta configuración y las pruebas está disponible en &lt;a href="https://github.com/isaacOjeda/DevToPosts/tree/main/TestContainers" rel="noopener noreferrer"&gt;este repositorio de GitHub&lt;/a&gt;. Si quieres seguir este tutorial, puedes clonar el repositorio y ajustar los pasos según sea necesario para tu propio proyecto.&lt;/p&gt;

&lt;h2&gt;
  
  
  Herramientas y Librerías Utilizadas
&lt;/h2&gt;

&lt;p&gt;Para implementar pruebas de integración con un entorno aislado y controlado, usaremos las siguientes librerías:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;xUnit&lt;/strong&gt;: Un framework de pruebas en .NET, ideal para organizar y ejecutar nuestras pruebas de integración.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testcontainers.MsSql&lt;/strong&gt;: Permite levantar un contenedor de SQL Server en Docker exclusivamente para nuestras pruebas, evitando dependencias locales y asegurando un entorno consistente.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Respawn&lt;/strong&gt;: Resetea la base de datos entre pruebas, manteniendo un estado limpio y libre de datos residuales para cada ejecución.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FluentAssertions&lt;/strong&gt;: Simplifica la escritura de aserciones, haciéndolas más legibles y expresivas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Microsoft.AspNetCore.Mvc.Testing&lt;/strong&gt;: Utilizada para crear un servidor web en memoria para poder exponer los endpoints y poder ejecutar las pruebas de inicio a fin.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Creación de un Contenedor SQL Server con TestContainers
&lt;/h2&gt;

&lt;p&gt;En esta sección, configuraremos un contenedor de SQL Server para nuestras pruebas de integración usando &lt;strong&gt;TestContainers&lt;/strong&gt; y un &lt;strong&gt;ICollectionFixture&lt;/strong&gt; en xUnit. Esto permitirá que todas las pruebas dentro de una misma colección compartan una instancia de contenedor SQL Server, evitando configuraciones repetitivas y asegurando un entorno consistente.&lt;/p&gt;

&lt;h3&gt;
  
  
  Uso de ICollectionFixture y CollectionDefinition en xUnit
&lt;/h3&gt;

&lt;p&gt;xUnit proporciona el concepto de &lt;code&gt;ICollectionFixture&lt;/code&gt; para compartir configuraciones entre pruebas en una misma colección. Con &lt;code&gt;ICollectionFixture&lt;/code&gt;, podemos crear una configuración compartida que solo se inicializa una vez y está disponible para todas las pruebas que pertenecen a una colección específica. Esto es ideal cuando tenemos configuraciones costosas o que queremos evitar inicializar en cada prueba, como en este caso, un contenedor Docker para SQL Server.&lt;/p&gt;

&lt;p&gt;La configuración se organiza en dos partes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ICollectionFixture&lt;/strong&gt;: Define una clase de configuración compartida, en este caso &lt;code&gt;SqlServerContainerFixture&lt;/code&gt;, que contiene toda la lógica para levantar y detener el contenedor SQL Server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CollectionDefinition&lt;/strong&gt;: Agrupa las pruebas que comparten la misma configuración. Asociamos las pruebas a esta colección usando el nombre de la colección (en este caso, &lt;code&gt;"SqlServerContainerFixture"&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Creación del Contenedor SQL Server
&lt;/h3&gt;

&lt;p&gt;A continuación, se muestra el código de &lt;code&gt;SqlServerContainerFixture&lt;/code&gt;, que configura y administra el ciclo de vida de un contenedor SQL Server. Este fixture implementa &lt;code&gt;IAsyncLifetime&lt;/code&gt;, que permite ejecutar lógica de inicialización y limpieza de recursos de forma asincrónica.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SqlServerContainerFixture&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IAsyncLifetime&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;FixtureName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"SqlServerContainerFixture"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;MsSqlContainer&lt;/span&gt; &lt;span class="n"&gt;SqlServerContainer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;ConnectionString&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;SqlServerContainer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetConnectionString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;SqlServerContainerFixture&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Configuramos el contenedor SQL Server usando TestContainers&lt;/span&gt;
        &lt;span class="n"&gt;SqlServerContainer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;MsSqlBuilder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"mcr.microsoft.com/mssql/server:2022-latest"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// Imagen de SQL Server&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithPassword&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Passw0rd!"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// Contraseña obligatoria para SQL Server&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Inicializa el contenedor de forma asincrónica antes de que las pruebas comiencen a ejecutarse&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;InitializeAsync&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;SqlServerContainer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StartAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Detiene y limpia el contenedor después de que todas las pruebas hayan finalizado&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;DisposeAsync&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;SqlServerContainer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StopAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Propiedades y Constructor&lt;/strong&gt;: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;SqlServerContainer&lt;/code&gt;: Es el contenedor de SQL Server configurado a través de la clase &lt;code&gt;MsSqlBuilder&lt;/code&gt; de TestContainers, donde especificamos la imagen docker, la contraseña y el puerto para el contenedor.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ConnectionString&lt;/code&gt;: Proporciona una cadena de conexión a SQL Server, obtenida directamente del contenedor para que las pruebas puedan conectarse a esta base de datos.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Async Lifecycle con IAsyncLifetime&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;InitializeAsync()&lt;/code&gt;: Este método se ejecuta antes de que cualquier prueba en la colección se ejecute. Llama a &lt;code&gt;StartAsync()&lt;/code&gt; para levantar el contenedor de SQL Server.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DisposeAsync()&lt;/code&gt;: Este método se ejecuta una vez que todas las pruebas de la colección han finalizado, liberando recursos al detener el contenedor con &lt;code&gt;StopAsync()&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  Definición de la Colección de Pruebas
&lt;/h4&gt;

&lt;p&gt;A continuación, definimos la colección de pruebas asociada al fixture &lt;code&gt;SqlServerContainerFixture&lt;/code&gt;. Esto se hace con &lt;code&gt;[CollectionDefinition]&lt;/code&gt;, que simplemente establece un nombre para la colección y vincula el fixture a todas las pruebas de la misma colección:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;CollectionDefinition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SqlServerContainerFixture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FixtureName&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DatabaseCollection&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ICollectionFixture&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SqlServerContainerFixture&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Esta clase no necesita código. Su único propósito es asociar&lt;/span&gt;
    &lt;span class="c1"&gt;// el CollectionDefinition con ICollectionFixture.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;[CollectionDefinition]&lt;/code&gt;: Asocia un nombre de colección, en este caso &lt;code&gt;"SqlServerContainerFixture"&lt;/code&gt;, con el fixture &lt;code&gt;SqlServerContainerFixture&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ICollectionFixture&amp;lt;SqlServerContainerFixture&amp;gt;&lt;/code&gt;: Define la dependencia del fixture en todas las pruebas que pertenezcan a la colección.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Al agrupar nuestras pruebas en esta colección, cada una podrá acceder al contenedor de SQL Server compartido, eliminando la necesidad de inicializar un nuevo contenedor para cada prueba individualmente. Esto hace que las pruebas sean más eficientes y garantiza un entorno de datos consistente.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuración de Reseteo de Base de Datos con Respawn y WebApplicationFactory
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Uso de &lt;code&gt;WebApplicationFactory&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;WebApplicationFactory&lt;/code&gt;&lt;/strong&gt; es una clase proporcionada por Microsoft que permite crear un servidor de pruebas en memoria basado en la configuración de la aplicación real. Esto es útil cuando necesitas probar la integración de tu Web API, ya que te permite simular el comportamiento de la aplicación sin necesidad de un servidor físico o configuración externa.&lt;/p&gt;

&lt;p&gt;En este caso, estamos creando un &lt;strong&gt;&lt;code&gt;WebApplicationFixture&lt;/code&gt;&lt;/strong&gt; que extiende &lt;code&gt;WebApplicationFactory&amp;lt;Program&amp;gt;&lt;/code&gt;. Esta clase personalizada actúa como un "wrapper" para iniciar la aplicación en un entorno de pruebas, y también maneja la configuración de la base de datos y su reseteo entre pruebas utilizando &lt;strong&gt;Respawn&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuración de Respawn para el Reseteo de la Base de Datos
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Respawn&lt;/strong&gt; es una librería que facilita el reseteo rápido y eficiente de la base de datos, lo cual es especialmente útil en pruebas de integración. Lo que hace Respawn es eliminar los datos de las tablas entre pruebas, asegurando que cada prueba tenga un entorno limpio y consistente sin la necesidad de restaurar toda la base de datos.&lt;/p&gt;

&lt;p&gt;En nuestro &lt;code&gt;WebApplicationFixture&lt;/code&gt;, estamos utilizando &lt;strong&gt;&lt;code&gt;IAsyncLifetime&lt;/code&gt;&lt;/strong&gt; para manejar la inicialización y limpieza asincrónica de los recursos antes y después de que se ejecuten las pruebas. En particular, se usa para la configuración de Respawn:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;WebApplicationFixture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SqlServerContainerFixture&lt;/span&gt; &lt;span class="n"&gt;sqlServerContainerFixture&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;WebApplicationFactory&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Program&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;,&lt;/span&gt; &lt;span class="n"&gt;IAsyncLifetime&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="n"&gt;Respawner&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;_respawner&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Inicialización de Respawn y base de datos&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="n"&gt;IAsyncLifetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;InitializeAsync&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateScope&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;dbSeed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ServiceProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetRequiredService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AppDbContextSeed&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

        &lt;span class="c1"&gt;// Inicializar Respawn&lt;/span&gt;
        &lt;span class="n"&gt;_respawner&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Respawner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sqlServerContainerFixture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConnectionString&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;RespawnerOptions&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;TablesToIgnore&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"__EFMigrationsHistory"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;

        &lt;span class="c1"&gt;// Asegurar que la base de datos esté creada y con datos de prueba&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;dbSeed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EnsureCreatedAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;dbSeed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SeedAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Reseteo de la base de datos después de las pruebas&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="n"&gt;IAsyncLifetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DisposeAsync&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_respawner&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Reseteamos la base de datos a su estado inicial&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_respawner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ResetAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sqlServerContainerFixture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConnectionString&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;InitializeAsync()&lt;/code&gt;&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Respawner.CreateAsync()&lt;/code&gt;&lt;/strong&gt;: Configuramos Respawn con la cadena de conexión del contenedor SQL Server para reiniciar las tablas relevantes antes de cada clase de pruebas. Las tablas especificadas en &lt;code&gt;TablesToIgnore&lt;/code&gt; no se resetean.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;dbSeed.EnsureCreatedAsync()&lt;/code&gt; y &lt;code&gt;dbSeed.SeedAsync()&lt;/code&gt;&lt;/strong&gt;: Aseguran que la base de datos esté creada y se tengan datos iniciales para las pruebas.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;DisposeAsync()&lt;/code&gt;&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;Cuando las pruebas de una clase han finalizado, &lt;strong&gt;&lt;code&gt;DisposeAsync()&lt;/code&gt;&lt;/strong&gt; es responsable de ejecutar el método &lt;strong&gt;&lt;code&gt;ResetAsync()&lt;/code&gt;&lt;/strong&gt; de Respawn, que limpia la base de datos, dejando las tablas listas para la siguiente clase de pruebas.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  Configuración de la Base de Datos con &lt;code&gt;ConfigureWebHost&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;Dentro de la clase &lt;strong&gt;&lt;code&gt;WebApplicationFixture&lt;/code&gt;&lt;/strong&gt;, también estamos utilizando el método &lt;strong&gt;&lt;code&gt;ConfigureWebHost&lt;/code&gt;&lt;/strong&gt; para ajustar la configuración de la aplicación de pruebas, especialmente la conexión a la base de datos. Esto es necesario porque la aplicación por defecto podría estar configurada para usar una base de datos local o un entorno diferente, por lo que tenemos que "reconfigurar" la cadena de conexión para que apunte al contenedor SQL Server levantado con TestContainers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;ConfigureWebHost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IWebHostBuilder&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseEnvironment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"IntegrationTests"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ConfigureTestServices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;services&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Eliminar configuraciones anteriores de la base de datos&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;descriptor&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SingleOrDefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ServiceType&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DbContextOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AppDbContext&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;));&lt;/span&gt;

        &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;descriptor&lt;/span&gt;&lt;span class="p"&gt;!);&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;descriptorAppDbContext&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SingleOrDefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ServiceType&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AppDbContext&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

        &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;descriptorAppDbContext&lt;/span&gt;&lt;span class="p"&gt;!);&lt;/span&gt;

        &lt;span class="c1"&gt;// Configurar la base de datos para que utilice el contenedor SQL Server&lt;/span&gt;
        &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddDbContext&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AppDbContext&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseSqlServer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sqlServerContainerFixture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConnectionString&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;UseEnvironment("IntegrationTests")&lt;/code&gt;&lt;/strong&gt;: Establece el entorno de pruebas para que la aplicación cargue las configuraciones específicas para pruebas de integración (de ser necesario, podríamos tener un &lt;code&gt;appsettings.IntegrationTests.json&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ConfigureTestServices()&lt;/code&gt;&lt;/strong&gt;: Reconfigura los servicios de la aplicación para que la conexión de la base de datos apunte al contenedor SQL Server. Primero, eliminamos cualquier configuración previa de &lt;code&gt;DbContext&lt;/code&gt; y luego agregamos la nueva configuración que utiliza la cadena de conexión proporcionada por el contenedor.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Con esta configuración, &lt;strong&gt;&lt;code&gt;WebApplicationFixture&lt;/code&gt;&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Levanta la aplicación en memoria utilizando &lt;strong&gt;&lt;code&gt;WebApplicationFactory&lt;/code&gt;&lt;/strong&gt; para simular el servidor real.&lt;/li&gt;
&lt;li&gt;Utiliza &lt;strong&gt;Respawn&lt;/strong&gt; para reiniciar la base de datos antes de cada clase de pruebas, asegurando un entorno limpio.&lt;/li&gt;
&lt;li&gt;Configura la conexión a la base de datos para que apunte al contenedor SQL Server levantado con &lt;strong&gt;TestContainers&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Escribir una Prueba de Integración Básica
&lt;/h2&gt;

&lt;p&gt;Las pruebas de integración en este contexto validan la comunicación entre nuestra aplicación y la base de datos SQL Server. Usando &lt;code&gt;WebApplicationFixture&lt;/code&gt; y &lt;code&gt;SqlServerContainerFixture&lt;/code&gt;, simulamos el entorno de la aplicación y reiniciamos el estado de la base de datos para cada clase de pruebas, asegurando que cada prueba comience desde un estado limpio.&lt;/p&gt;

&lt;h3&gt;
  
  
  Estructura de la Clase de Prueba &lt;code&gt;CreateProductTests&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;La clase &lt;code&gt;CreateProductTests&lt;/code&gt; contiene dos pruebas básicas para el endpoint de creación de productos:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Una prueba para validar que la creación de un producto es exitosa.&lt;/li&gt;
&lt;li&gt;Una prueba para verificar el comportamiento cuando se intenta crear un producto con una categoría inexistente.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;TestContainers.Integration.Tests.Endpoints.ProductTests&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SqlServerContainerFixture&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FixtureName&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreateProductTests&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IClassFixture&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;WebApplicationFixture&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;WebApplicationFixture&lt;/span&gt; &lt;span class="n"&gt;_factory&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;AppDbContext&lt;/span&gt; &lt;span class="n"&gt;_dbContext&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;HttpClient&lt;/span&gt; &lt;span class="n"&gt;_client&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;CreateProductTests&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;WebApplicationFixture&lt;/span&gt; &lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_factory&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;_dbContext&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DbContext&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;_client&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HttpClient&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;[Collection]&lt;/strong&gt;: Esta anotación asegura que &lt;code&gt;SqlServerContainerFixture&lt;/code&gt; se utilice para todas las pruebas dentro de esta clase, compartiendo la misma configuración de base de datos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;IClassFixture&amp;lt;WebApplicationFixture&amp;gt;&lt;/code&gt;&lt;/strong&gt;: &lt;code&gt;WebApplicationFixture&lt;/code&gt; proporciona acceso a un cliente HTTP en memoria (&lt;code&gt;HttpClient&lt;/code&gt;) para simular llamadas HTTP y a &lt;code&gt;AppDbContext&lt;/code&gt; para realizar operaciones directas en la base de datos durante la configuración de pruebas.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Primera Prueba: Creación Exitosa de un Producto
&lt;/h4&gt;

&lt;p&gt;La prueba &lt;code&gt;CreateProduct_ReturnsProduct&lt;/code&gt; verifica que la creación de un producto con una categoría válida sea exitosa y que los datos devueltos sean correctos.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fact&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;CreateProduct_ReturnsProduct&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Arrange&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;GetFirstCategory&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Product&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Test Product"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Test Description"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Price&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;10.0m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CategoryId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="c1"&gt;// Act&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PostAsJsonAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/products"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Assert&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EnsureSuccessStatusCode&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;createdProduct&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadFromJsonAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

    &lt;span class="n"&gt;createdProduct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ShouldNotBeNull&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;createdProduct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ShouldBeEqualTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;createdProduct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Price&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ShouldBeEqualTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Price&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Arrange&lt;/strong&gt;: Se obtiene una categoría válida desde la base de datos (&lt;code&gt;GetFirstCategory&lt;/code&gt;) y se crea un objeto &lt;code&gt;Product&lt;/code&gt; con propiedades válidas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Act&lt;/strong&gt;: Se envía una solicitud HTTP &lt;code&gt;POST&lt;/code&gt; al endpoint &lt;code&gt;/products&lt;/code&gt; con los datos del producto. Esto simula una creación de producto en el sistema.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assert&lt;/strong&gt;: Se verifica que la respuesta sea exitosa y que los datos del producto creado coincidan con los esperados.&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  Segunda Prueba: Creación Fallida de un Producto con Categoría Inexistente
&lt;/h4&gt;

&lt;p&gt;La prueba &lt;code&gt;CreateProduct_Fails_WhenCategoryDoesNotExist&lt;/code&gt; verifica que la creación de un producto falle cuando se especifica una categoría inválida.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fact&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;CreateProduct_Fails_WhenCategoryDoesNotExist&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Arrange&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Product&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Test Product"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Test Description"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Price&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;10.0m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CategoryId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="c1"&gt;// Categoría inválida&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="c1"&gt;// Act&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PostAsJsonAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/products"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Assert&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ShouldBeEqualTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HttpStatusCode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BadRequest&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Arrange&lt;/strong&gt;: Se crea un objeto &lt;code&gt;Product&lt;/code&gt; con una categoría inexistente (CategoryId = 0).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Act&lt;/strong&gt;: Se envía una solicitud HTTP &lt;code&gt;POST&lt;/code&gt; al endpoint &lt;code&gt;/products&lt;/code&gt; con el producto, esperando un fallo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assert&lt;/strong&gt;: Se verifica que el servidor responde con un estado &lt;code&gt;BadRequest&lt;/code&gt; (400), indicando que la categoría especificada no existe.&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  Método Auxiliar &lt;code&gt;GetFirstCategory&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;El método &lt;code&gt;GetFirstCategory&lt;/code&gt; es una función auxiliar que facilita la recuperación de la primera categoría disponible en la base de datos. Este método permite a las pruebas obtener un &lt;code&gt;CategoryId&lt;/code&gt; válido para productos.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Category&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetFirstCategory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_dbContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Categories&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FirstAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Este enfoque de pruebas ayuda a mantener las pruebas de integración eficientes y confiables, verificando los comportamientos principales del endpoint de creación de productos y manejando casos tanto exitosos como de error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cuando Usar Integration Tests o Unit Tests
&lt;/h2&gt;

&lt;p&gt;Las pruebas de software se dividen principalmente en &lt;strong&gt;pruebas unitarias&lt;/strong&gt; e &lt;strong&gt;integración&lt;/strong&gt;, y cada tipo tiene su propósito y contexto de uso. La elección entre uno u otro depende del objetivo de la prueba y del nivel de aislamiento necesario. A continuación, te explicamos cuándo es adecuado utilizar cada tipo de prueba.&lt;/p&gt;

&lt;h3&gt;
  
  
  Unit Tests: Aislando el Comportamiento de una Funcionalidad Específica
&lt;/h3&gt;

&lt;p&gt;Las &lt;strong&gt;pruebas unitarias&lt;/strong&gt; son el tipo más básico de pruebas. Están diseñadas para verificar el comportamiento de una unidad de código de manera aislada, sin depender de recursos externos como bases de datos, servicios web o sistemas de archivos. Este tipo de pruebas se centran en una pequeña parte del sistema, como un método o función individual, asegurando que se ejecute correctamente bajo diferentes condiciones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cuándo usar Unit Tests&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cuando deseas comprobar el comportamiento de una &lt;strong&gt;función o método específico&lt;/strong&gt; en aislamiento.&lt;/li&gt;
&lt;li&gt;Cuando necesitas verificar que una &lt;strong&gt;lógica de negocio interna&lt;/strong&gt; funciona correctamente.&lt;/li&gt;
&lt;li&gt;Para asegurar que el código cumple con los requisitos del &lt;strong&gt;comportamiento esperado&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Cuando los cambios realizados no deberían afectar otras partes del sistema (por ejemplo, al refactorizar código o agregar nuevas características pequeñas).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Ventajas&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rápidos de ejecutar&lt;/strong&gt;: debido a que son pequeños y no requieren recursos externos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aislamiento&lt;/strong&gt;: puedes asegurarte de que las dependencias externas no interfieran.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fáciles de automatizar&lt;/strong&gt;: las pruebas unitarias suelen ser fáciles de configurar y ejecutar en una suite de pruebas automatizadas.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Integration Tests: Validando la Interacción entre Componentes
&lt;/h3&gt;

&lt;p&gt;Las &lt;strong&gt;pruebas de integración&lt;/strong&gt; van un paso más allá, probando cómo diferentes unidades de código interactúan entre sí y con sistemas externos, como bases de datos, API, o sistemas de mensajería. Se aseguran de que los componentes del sistema funcionen juntos de la forma esperada.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cuándo usar Integration Tests&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cuando necesitas verificar cómo los &lt;strong&gt;módulos o servicios interactúan entre sí&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Si deseas probar la integración con &lt;strong&gt;sistemas externos&lt;/strong&gt; como bases de datos, servicios web, o colas de mensajes.&lt;/li&gt;
&lt;li&gt;Para validar que los cambios no afecten negativamente la interacción de diferentes capas del sistema.&lt;/li&gt;
&lt;li&gt;Para garantizar que la &lt;strong&gt;configuración de infraestructura&lt;/strong&gt; y las dependencias externas (como bases de datos) estén correctamente conectadas y funcionando.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Ventajas&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cobertura completa de flujo&lt;/strong&gt;: Validan el flujo real de datos a través de los diferentes componentes del sistema, lo que aumenta la confianza en la funcionalidad de alto nivel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Identificación de problemas de integración&lt;/strong&gt;: Detectan problemas en la forma en que los componentes interactúan entre sí, lo cual no se podría verificar con pruebas unitarias.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reducción de errores en producción&lt;/strong&gt;: Aseguran que el sistema se comporte correctamente en un entorno más cercano al real.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  ¿Unit Tests o Integration Tests? ¿Cuándo usar cada uno?
&lt;/h4&gt;

&lt;p&gt;A menudo, la clave está en la &lt;strong&gt;combinación adecuada&lt;/strong&gt; de ambos tipos de pruebas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Unit Tests&lt;/strong&gt;: Son el primer paso en el ciclo de vida del desarrollo, permitiendo detectar errores rápidamente y de forma aislada.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integration Tests&lt;/strong&gt;: Deben complementarse con las pruebas unitarias para garantizar que todos los componentes funcionen correctamente cuando se ensamblen.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Es decir:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Si estás desarrollando una pequeña funcionalidad que no interactúa con sistemas externos, &lt;strong&gt;prioriza las pruebas unitarias&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Si estás trabajando con interacción entre sistemas, bases de datos o servicios externos, &lt;strong&gt;añade pruebas de integración&lt;/strong&gt; para validar estos comportamientos más complejos.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ambos tipos de pruebas son fundamentales para una estrategia de pruebas completa, y saber cuándo usar cada uno te ayudará a mantener una buena cobertura de pruebas y mejorar la calidad de tu software.&lt;/p&gt;

&lt;h2&gt;
  
  
  Consideraciones para Escenarios Complejos
&lt;/h2&gt;

&lt;p&gt;Cuando trabajamos con escenarios más allá de las simples operaciones CRUD, es crucial que nuestras pruebas de integración sean flexibles y realistas. A continuación, algunos puntos a considerar:&lt;/p&gt;

&lt;h4&gt;
  
  
  Datos de Configuración Inicial (Seed Data)
&lt;/h4&gt;

&lt;p&gt;En escenarios complejos, es probable que necesites un conjunto de datos predefinido para representar un estado inicial específico de la base de datos. Utilizar datos de configuración inicial (o "seed data") permite que las pruebas se ejecuten en condiciones que simulan situaciones de negocio más avanzadas.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Estrategia de Configuración&lt;/strong&gt;: Puedes agregar datos de configuración en la fase de inicialización del &lt;code&gt;WebApplicationFixture&lt;/code&gt;. Esto permite que los datos de prueba específicos estén listos cada vez que se ejecuten las pruebas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Uso de Factories o Builders&lt;/strong&gt;: Para configurar datos complejos, considera el uso de patrones como &lt;code&gt;Factory&lt;/code&gt; o &lt;code&gt;Builder&lt;/code&gt; que ayuden a crear objetos con propiedades adecuadas para el escenario, manteniendo el código de prueba más limpio y reutilizable.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Simulación de Estados de la Aplicación
&lt;/h4&gt;

&lt;p&gt;Algunas pruebas pueden requerir que la aplicación esté en un estado específico antes de ejecutar el test (como el "estado de procesamiento" o "finalizado" de una orden). Para simular estos estados:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Configuración de Estados con Estados Mock o Flags&lt;/strong&gt;: Configura los estados directamente en las entidades de prueba, o utiliza servicios mock (como mocks de notificaciones, correos electrónicos o servicios externos) para simular estados sin requerir que la aplicación ejecute todos los pasos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test Doubles&lt;/strong&gt;: Para escenarios que dependen de servicios externos, considera el uso de "doubles" (dummies, mocks, etc.) que representen estos servicios sin necesidad de hacer llamadas reales. Esto ayuda a centrar la prueba en la lógica de negocio sin complicaciones adicionales.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Verificación de Efectos Colaterales
&lt;/h4&gt;

&lt;p&gt;Cuando las pruebas de integración implican actualizaciones de datos o envío de eventos a otros sistemas, es importante verificar que los efectos colaterales ocurren como se espera.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Simulación de Eventos&lt;/strong&gt;: Para los sistemas basados en eventos, puedes verificar que los eventos se envíen correctamente o incluso simular la recepción de eventos que la aplicación debe procesar.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validación de Estados Finales&lt;/strong&gt;: Después de realizar una operación, verifica que las tablas relevantes y los estados de las entidades reflejen los cambios esperados.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Los escenarios complejos pueden agregar valor significativo a las pruebas de integración, ayudando a identificar problemas difíciles de reproducir en condiciones normales. Aplicar una configuración cuidadosa, incluyendo datos de configuración inicial, control de transacciones, y mocks de estados y dependencias, permitirá manejar estos escenarios de forma efectiva y aumentar la robustez de las pruebas de integración.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusión
&lt;/h2&gt;

&lt;p&gt;Las pruebas de integración son esenciales para garantizar que los componentes de una aplicación funcionen de manera coherente y sin problemas cuando se combinan. A lo largo de este tutorial, hemos visto cómo configurar un entorno de pruebas sólido y flexible utilizando herramientas como &lt;strong&gt;xUnit&lt;/strong&gt;, &lt;strong&gt;Testcontainers&lt;/strong&gt;, &lt;strong&gt;Respawn&lt;/strong&gt;, y &lt;strong&gt;WebApplicationFactory&lt;/strong&gt; en .NET, permitiendo realizar pruebas con una base de datos SQL Server en contenedor y manteniendo un servidor de pruebas en memoria.&lt;/p&gt;

&lt;p&gt;Al seguir las prácticas descritas, desde el uso de contenedores hasta la optimización de la infraestructura de pruebas, puedes asegurar que tu aplicación se mantenga estable a lo largo del ciclo de desarrollo. Este enfoque no solo minimiza errores que solo podrían detectarse en producción, sino que también facilita la identificación y resolución de problemas antes de que escalen.&lt;/p&gt;

&lt;p&gt;Recuerda que, aunque estas pruebas pueden ser más costosas en términos de tiempo de ejecución y configuración que las pruebas unitarias, la inversión en pruebas de integración robustas proporciona un valor significativo. Estas pruebas ayudan a prevenir fallos graves y aseguran una experiencia de usuario final consistente. Adoptar buenas prácticas de optimización y aprovechar herramientas modernas para la gestión de entornos de prueba te permitirá mantener una suite de pruebas rápida y eficiente, fortaleciendo la confiabilidad de tus aplicaciones en el tiempo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Referencias
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.testing.webapplicationfactory-1?view=aspnetcore-8.0" rel="noopener noreferrer"&gt;WebApplicationFactory Class (Microsoft.AspNetCore.Mvc.Testing) | Microsoft Learn&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-8.0" rel="noopener noreferrer"&gt;Integration tests in ASP.NET Core | Microsoft Learn&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://xunit.net/docs/shared-context#assembly-fixture" rel="noopener noreferrer"&gt;Shared Context between Tests &amp;gt; xUnit.net&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dotnet.testcontainers.org/modules/mssql/" rel="noopener noreferrer"&gt;Microsoft SQL Server - Testcontainers for .NET&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>dotnet</category>
      <category>testing</category>
      <category>csharp</category>
      <category>containers</category>
    </item>
  </channel>
</rss>
