<?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: Beto Ramirez</title>
    <description>The latest articles on DEV Community by Beto Ramirez (@betoramiz).</description>
    <link>https://dev.to/betoramiz</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F122575%2F7a47e6b0-caa1-45b5-ba13-09e5afb012bd.jpg</url>
      <title>DEV Community: Beto Ramirez</title>
      <link>https://dev.to/betoramiz</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/betoramiz"/>
    <language>en</language>
    <item>
      <title>"Blindaje, Frontend y Evolución: Tests de Arquitectura, SQLite in-memory y el Camino a Modular Monolith (Parte 3)</title>
      <dc:creator>Beto Ramirez</dc:creator>
      <pubDate>Sun, 27 Sep 2026 20:52:55 +0000</pubDate>
      <link>https://dev.to/betoramiz/blindaje-frontend-y-evolucion-tests-de-arquitectura-sqlite-in-memory-y-el-camino-a-modular-16be</link>
      <guid>https://dev.to/betoramiz/blindaje-frontend-y-evolucion-tests-de-arquitectura-sqlite-in-memory-y-el-camino-a-modular-16be</guid>
      <description>&lt;p&gt;Llegamos a la última entrega de nuestra serie sobre &lt;strong&gt;Vertical Slice Architecture (VSA)&lt;/strong&gt; en .NET.&lt;/p&gt;

&lt;p&gt;En la &lt;a href="https://dev.to/betoramiz/deconstruyendo-clean-architecture-por-que-nos-cansa-y-como-vertical-slice-architecture-nos-devuelve-la-paz-en-net-parte-1"&gt;Parte 1&lt;/a&gt;, eliminamos los proyectos innecesarios y sentamos las bases con Minimal APIs y auto-descubrimiento. En la &lt;a href="https://dev.to/betoramiz/anatomia-de-un-slice-en-produccion-handlers-sin-interfaces-ef-core-sin-repositorios-y-eventos-desacoplados-parte-2"&gt;Parte 2&lt;/a&gt;, escribimos código de producción: handlers cohesivos, persistencia directa con EF Core, validación con filtros y eventos desacoplados.&lt;/p&gt;

&lt;p&gt;Pero cualquier arquitecto o desarrollador senior se estará haciendo la pregunta decisiva:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;«Al meter todo en un solo proyecto Web API, eliminaste los muros del compilador. ¿Qué impide que un desarrollador con prisa llame a un handler privado de otro slice o cree dependencias circulares? ¿Cómo testeamos esto sin volvernos locos? ¿Cómo se conecta con el frontend? ¿Y qué pasa si la aplicación crece a 100 slices?»&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;En este artículo cerramos el círculo: blindaje con tests de arquitectura automáticos, testing honesto sin mocks de repositorio, generación de contratos hacia TypeScript y la hoja de ruta para evolucionar hacia un &lt;strong&gt;Monolito Modular&lt;/strong&gt; sin reescribir tu sistema.&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing sin mentiras: Dile adiós a los mocks de repositorio
&lt;/h2&gt;

&lt;p&gt;En proyectos tradicionales con Clean Architecture, las pruebas unitarias suelen lucir 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="c1"&gt;// El clásico test que prueba el mock, no tu código:&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;mockRepo&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;Mock&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IProductRepository&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;span class="n"&gt;mockRepo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Setup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetByIdAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;It&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsAny&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;&amp;gt;())).&lt;/span&gt;&lt;span class="nf"&gt;ReturnsAsync&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;Product&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;ProductService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mockRepo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Object&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;DoSomethingAsync&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;mockRepo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetByIdAsync&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;Times&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Once&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// "Probamos que llamamos al mock"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;¿Qué acabas de probar? Que si llamas a un método ficticio, ese método ficticio devuelve lo que tú mismo le dijiste que devolviera.&lt;br&gt;&lt;br&gt;
No probaste si tu consulta LINQ era válida, si rompía por una columna nula, si el mapping fallaba o si las restricciones de clave foránea se cumplían. &lt;strong&gt;Ese test miente por omisión.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  La alternativa honesta: SQLite in-memory con esquema real
&lt;/h3&gt;

&lt;p&gt;En nuestra plantilla, no hay repositorios que mockear porque no hay repositorios que escribir. Los handlers reciben &lt;code&gt;AppDbContext&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Para testearlos de forma ultrarrápida y realista, creamos un helper &lt;code&gt;TestDb.Create()&lt;/code&gt; que levanta una base de datos &lt;strong&gt;SQLite in-memory&lt;/strong&gt; con todas las tablas y configuraciones reales creadas 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="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;VsaTemplate.Tests.Common&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;class&lt;/span&gt; &lt;span class="nc"&gt;TestDb&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;AppDbContext&lt;/span&gt; &lt;span class="nf"&gt;Create&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;connection&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;SqliteConnection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Data Source=:memory:"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Open&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;options&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;DbContextOptionsBuilder&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseSqlite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connection&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;db&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;OwnedConnectionDbContext&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;connection&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EnsureCreated&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Aplica el schema real al instante&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;db&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;
  
  
  Cómo luce un test unitario real del Handler
&lt;/h3&gt;

&lt;p&gt;Observa &lt;code&gt;tests/Features/Products/CreateProductTests.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;class&lt;/span&gt; &lt;span class="nc"&gt;CreateProductTests&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&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;Creates_the_product_when_the_data_is_valid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// 1. Arrange: BD real en memoria y mock solo para la frontera (EventDispatcher)&lt;/span&gt;
        &lt;span class="k"&gt;await&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;db&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TestDb&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;fakeDispatcher&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;Mock&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IEventDispatcher&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

        &lt;span class="c1"&gt;// El handler se construye con new directo: ¡sin interfaces, sin DI container!&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;handler&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;CreateProductHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fakeDispatcher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// 2. Act&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;request&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;CreateProduct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Teclado Mecánico"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;99m&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;await&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&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="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="c1"&gt;// 3. Assert: Comprobamos el resultado Y que la fila realmente existe en la BD&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;"Teclado Mecánico"&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="n"&gt;Name&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Products&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;Corre en milisegundos.&lt;/li&gt;
&lt;li&gt;Ejecuta SQL real.&lt;/li&gt;
&lt;li&gt;Solo mockeas las verdaderas fronteras externas (como &lt;code&gt;IEventDispatcher&lt;/code&gt; o pasarelas de pago).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Testear validadores es aún más simple
&lt;/h3&gt;

&lt;p&gt;Como los validadores de FluentValidation viven en el slice, se prueban como funciones puras:&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;void&lt;/span&gt; &lt;span class="nf"&gt;The_validator_rejects_the_empty_name&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;result&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;CreateProduct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Validator&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Validate&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;CreateProduct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Request&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="m"&gt;99m&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;False&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;IsValid&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;Contains&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;Errors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PropertyName&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"Name"&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;Sin base de datos, sin servidor HTTP, sin complejidad.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dos niveles de tests
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Unitarios por Slice (&lt;code&gt;tests/Features/&lt;/code&gt;)&lt;/strong&gt;: Construyes el handler con &lt;code&gt;new&lt;/code&gt;, pasas &lt;code&gt;TestDb&lt;/code&gt; y pruebas la lógica interna de inmediato.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integración HTTP (&lt;code&gt;tests/Integration/&lt;/code&gt;)&lt;/strong&gt;: Usamos &lt;code&gt;WebApplicationFactory&lt;/code&gt; (&lt;code&gt;ApiFactory&lt;/code&gt;) para levantar la API completa, reemplazando la base de datos por SQLite in-memory y enviando peticiones HTTP reales. Esto valida routing, serialización JSON, middleware de autenticación, rate limiting y filtros.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Tests de Arquitectura: El guardián de las reglas
&lt;/h2&gt;

&lt;p&gt;La mayor crítica a Vertical Slice Architecture es:&lt;br&gt;&lt;br&gt;
&lt;em&gt;«Al no tener proyectos separados, cualquiera puede meter un &lt;code&gt;using VsaTemplate.Features.Orders;&lt;/code&gt; dentro de &lt;code&gt;Features.Products&lt;/code&gt; y nadie se va a dar cuenta en el code review».&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Y es una crítica válida. Una violación de arquitectura en un solo proyecto es una línea invisible que se cuela en silencio.&lt;/p&gt;

&lt;p&gt;Para resolver esto de raíz, usamos &lt;strong&gt;NetArchTest.Rules&lt;/strong&gt;. Convertimos nuestras reglas arquitectónicas en pruebas automatizadas de xUnit que corren con &lt;code&gt;dotnet test&lt;/code&gt; y &lt;strong&gt;rompen el build en el Pull Request&lt;/strong&gt; si alguien comete una infracción.&lt;/p&gt;

&lt;p&gt;Observa cómo se ve en &lt;code&gt;tests/Architecture/ArchitectureTests.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="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;void&lt;/span&gt; &lt;span class="nf"&gt;A_slice_does_not_use_use_cases_from_another_slice&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;failures&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;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;slice&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;Slices&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Obtiene todos los tipos de casos de uso de los demás slices&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;foreignUseCases&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;Slices&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;other&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;other&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SelectMany&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UseCaseTypeNames&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToArray&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="c1"&gt;// Verifica que este slice NO tenga dependencia sobre los casos de uso ajenos&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="n"&gt;Types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;InAssembly&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Api&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;That&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;ResideInNamespaceStartingWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"VsaTemplate.Features.&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;slice&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="nf"&gt;ShouldNot&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;HaveDependencyOnAny&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;foreignUseCases&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetResult&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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsSuccessful&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;failures&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddRange&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;FailingTypes&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;t&lt;/span&gt; &lt;span class="p"&gt;=&amp;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;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FullName&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="n"&gt;Assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;True&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;failures&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="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"Estos tipos usan casos de uso de otro slice. Lo público de un slice es su "&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt;
        &lt;span class="s"&gt;"Domain (consultas) y sus Contracts (eventos); para lo demás, comunícate por "&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt;
        &lt;span class="s"&gt;$"eventos (README, regla 1):\n&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="sc"&gt;'\n'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;failures&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;h3&gt;
  
  
  Las 6 reglas vigiladas automáticamente:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Un slice no usa los casos de uso de otro slice&lt;/strong&gt;: Vigila automáticamente cada carpeta dentro de &lt;code&gt;Features/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Common&lt;/code&gt; no usa casos de uso de los slices&lt;/strong&gt;: La infraestructura compartida jamás debe depender de la lógica de negocio.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Toda clase &lt;code&gt;*Endpoints&lt;/code&gt; implementa &lt;code&gt;IFeatureEndpoints&lt;/code&gt;&lt;/strong&gt;: Si a un dev se le olvida implementar la interfaz, el test falla y le avisa que sus rutas no se iban a registrar.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Los event handlers viven en &lt;code&gt;Features&lt;/code&gt;, no en &lt;code&gt;Common&lt;/code&gt;&lt;/strong&gt;: Escuchar eventos es responsabilidad de negocio.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Los contratos anidados se llaman &lt;code&gt;Request&lt;/code&gt;, &lt;code&gt;Response&lt;/code&gt; o &lt;code&gt;Validator&lt;/code&gt;&lt;/strong&gt;: Garantiza coherencia en toda la solución y nombres limpios para el generador de TypeScript.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Todo handler queda registrado por convención&lt;/strong&gt;: Si alguien crea un handler privado o que no cumpla la convención de nombres de Scrutor, el test lo detecta antes de que cause un error HTTP 500 en runtime.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Lección de oro:&lt;/strong&gt; &lt;em&gt;Un test de arquitectura que nunca falla no protege nada&lt;/em&gt;. Durante el desarrollo de la plantilla creamos clases con violaciones a propósito para confirmar que el test fallaba y arrojaba mensajes explicativos y educativos para el desarrollador infractor.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Contrato directo hacia el Frontend (OpenAPI → TypeScript)
&lt;/h2&gt;

&lt;p&gt;¿Cuántas veces has tenido que escribir en Angular o React una interfaz &lt;code&gt;interface ProductDto { id: string; name: string; price: number; }&lt;/code&gt; que ya tenías escrita en C#? Y a las dos semanas, alguien renombra un campo en el backend y el frontend explota silenciosamente.&lt;/p&gt;

&lt;p&gt;No hay que duplicar modelos a mano: &lt;strong&gt;OpenAPI es el puente natural&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  El desafío de VSA con OpenAPI: Colisión de nombres
&lt;/h3&gt;

&lt;p&gt;En VSA, todos los slices anidan sus records y los llaman igual: &lt;code&gt;CreateProduct.Request&lt;/code&gt;, &lt;code&gt;PlaceOrder.Request&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Por defecto, el generador OpenAPI de ASP.NET Core solo toma el nombre de la clase interna (&lt;code&gt;Request&lt;/code&gt;), provocando una colisión de nombres inmediata en el archivo &lt;code&gt;openapi.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;En &lt;code&gt;Common/OpenApi/OpenApiRegistration.cs&lt;/code&gt; lo resolvimos configurando &lt;code&gt;CreateSchemaReferenceId&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;AddOpenApiDocument&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="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;AddOpenApi&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;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Si el tipo está anidado, combinamos el nombre del padre con el hijo:&lt;/span&gt;
        &lt;span class="c1"&gt;// CreateProduct + Request = "CreateProductRequest"&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;CreateSchemaReferenceId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;typeInfo&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
            &lt;span class="n"&gt;typeInfo&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;DeclaringType&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;owner&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;owner&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;typeInfo&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;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="n"&gt;OpenApiOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateDefaultSchemaReferenceId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;typeInfo&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;
  
  
  Métodos limpios con &lt;code&gt;.WithName()&lt;/code&gt; y &lt;code&gt;TypedResults&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Al mapear el endpoint con &lt;code&gt;.WithName(nameof(CreateProduct))&lt;/code&gt;, le asignamos un &lt;code&gt;operationId&lt;/code&gt; explícito. Y al usar &lt;code&gt;Task&amp;lt;Results&amp;lt;Created&amp;lt;Response&amp;gt;, ValidationProblem&amp;gt;&amp;gt;&lt;/code&gt;, ASP.NET Core infiere automáticamente los esquemas de respuesta HTTP 201 y HTTP 400 sin tener que llenar los endpoints de atributos &lt;code&gt;[ProducesResponseType]&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Generación automática en Angular
&lt;/h3&gt;

&lt;p&gt;En el frontend (Angular en este caso), un simple comando lee el JSON generado y crea clientes y tipos con tipado estricto:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx ng-openapi-gen &lt;span class="nt"&gt;--input&lt;/span&gt; ./openapi/VsaTemplate.json &lt;span class="nt"&gt;--output&lt;/span&gt; src/app/api
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El ciclo de feedback es perfecto:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Modificas un &lt;code&gt;record&lt;/code&gt; en C#.&lt;/li&gt;
&lt;li&gt;Compilas la API (&lt;code&gt;dotnet build&lt;/code&gt; actualiza el JSON de OpenAPI).&lt;/li&gt;
&lt;li&gt;Corres &lt;code&gt;ng-openapi-gen&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;El compilador de TypeScript te marca en rojo exactamente qué pantalla de Angular quedó desactualizada.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Registro de Decisiones de Arquitectura.
&lt;/h2&gt;

&lt;p&gt;Para tener una visión de todas las decisiones técnicas tomadas en la plantilla, esta tabla resume el &lt;strong&gt;por qué&lt;/strong&gt; detrás de cada elección:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Decisión Técnica&lt;/th&gt;
&lt;th&gt;Justificación Pragmática&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;VSA en un solo proyecto&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;La complejidad se paga por slice, no por proyecto. Elimina fricción innecesaria.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Endpoints separados de la lógica&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;El archivo &lt;code&gt;Endpoints.cs&lt;/code&gt; actúa como tabla de rutas del slice; el handler queda libre de HTTP.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Handlers sin interfaces&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Una interfaz sin segunda implementación es pura ceremonia.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Registro por convención con Scrutor&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Cero configuración manual en &lt;code&gt;Program.cs&lt;/code&gt;. Un test de arquitectura vigila que nada quede fuera.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Interfaces solo en fronteras&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;TimeProvider&lt;/code&gt;, &lt;code&gt;IEventDispatcher&lt;/code&gt;, pasarelas de pago y proveedores externos.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Sin MediatR&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Minimal APIs despacha directo; los Endpoint Filters cubren los cross-cutting concerns.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Tests con SQLite in-memory&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Prueba el LINQ y SQL real; elimina la necesidad de repositorios ficticios.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Dispatcher de eventos propio (~40 líneas)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Desacopla slices in-process sin librerías externas ni problemas de licenciamiento.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Eventos en &lt;code&gt;Contracts/&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Frontera explícita y verificable por namespace entre features.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;OpenAPI emitido en build + TS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Contrato de tipos compartido entre backend y frontend sin desincronización manual.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Tests de arquitectura con NetArchTest&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Las convenciones se vigilan con tests automáticos en el CI/CD, no con policías en review.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;EF Core Config en el slice, Migraciones en Common&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;La config cambia con la entidad; las migraciones son globales por diseño del &lt;code&gt;ModelSnapshot&lt;/code&gt; de EF.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;FluentValidation en Endpoint Filter&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Menos código por slice; el handler solo recibe peticiones válidas.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;dotnet-ef como tool local&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;El repositorio fija sus versiones de herramientas de desarrollo sin tocar la máquina global del dev.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  El camino de crecimiento: De Slice a Monolito Modular y Microservicios
&lt;/h2&gt;

&lt;p&gt;El mayor temor al comenzar con Vertical Slice Architecture es:&lt;br&gt;&lt;br&gt;
&lt;em&gt;«¿Qué pasa cuando la aplicación crezca a 50 modelos y 200 casos de uso? ¿Nos quedaremos atrapados?»&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;La respuesta es que VSA ofrece el &lt;strong&gt;camino de crecimiento más natural y sin saltos traumáticos&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;      FASE 1: SLICE SIMPLE (HOY)
      Un proyecto Web API.
      Slices en carpetas `Features/`.
      Dispatcher in-process.
                 │
                 ▼ (Un feature crece mucho en complejidad o equipo)
      FASE 2: MÓDULO INDEPENDIENTE
      Se extrae la carpeta a su propio proyecto de clase o DLL.
      DbContext propio y migraciones propias.
      Frontera ya lista gracias a `Contracts/`.
                 │
                 ▼ (Los módulos requieren comunicación resiliente)
      FASE 3: MONOLITO MODULAR
      Reemplazas el dispatcher in-process por Wolverine o MassTransit.
      Outbox pattern, reintentos y mensajería asíncrona.
                 │
                 ▼ (Solo si un módulo requiere despliegue/escala independiente)
      FASE 4: MICROSERVICIO
      El módulo se despliega en su propio contenedor/proceso.
      Cero refactorización de lógica de negocio.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Hoy&lt;/strong&gt;: Todo vive en un solo proyecto, pero perfectamente ordenado por slices.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Un feature crece demasiado&lt;/strong&gt;: Imagina que el módulo &lt;code&gt;Billing&lt;/code&gt; crece y tiene su propio equipo asignado. Como su configuración de EF Core, sus handlers y sus contratos ya viven en su carpeta, convertirlo en un módulo (proyecto de biblioteca de clases separado) es una mudanza limpia y barata.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Comunicación resiliente&lt;/strong&gt;: Cuando varios módulos necesitan coordinarse de forma asíncrona y transaccional, reemplazas el dispatcher simple por &lt;strong&gt;Wolverine&lt;/strong&gt;. Como tus handlers ya son clases con un método &lt;code&gt;Handle&lt;/code&gt;, no tienes que reescribir nada.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Microservicio (solo si hace falta)&lt;/strong&gt;: Si después de todo eso, el módulo de Facturación necesita escalar a 10 instancias en Kubernetes mientras el resto corre en 1, lo extraes como servicio independiente.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;La regla de oro: Nunca te saltes etapas.&lt;/strong&gt; Empezar con microservicios desde el día uno es comprar complejidad antes de tener el problema. Con VSA tienes agilidad inmediata hoy y una puerta abierta al futuro sin deuda técnica.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusión de la serie
&lt;/h2&gt;

&lt;p&gt;A lo largo de estas 3 entregas hemos demostrado que:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No necesitas 5 proyectos para hacer software profesional en .NET.&lt;/li&gt;
&lt;li&gt;Puedes tener código limpio, testeable y desacoplado sin caer en la trampa del boilerplate infinito.&lt;/li&gt;
&lt;li&gt;Con las herramientas modernas de C# (.NET 10 / C# 13-14), Minimal APIs, Scrutor, FluentValidation y NetArchTest, el desarrollo backend vuelve a ser divertido, ágil y legible.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Ponlo en práctica hoy mismo
&lt;/h3&gt;

&lt;p&gt;La plantilla completa está lista para que la uses en tus proyectos personales o en tu empresa:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Instalar el template desde GitHub&lt;/span&gt;
dotnet new &lt;span class="nb"&gt;install &lt;/span&gt;https://github.com/betoramiz/vsa-template.git

&lt;span class="c"&gt;# Crear un proyecto nuevo&lt;/span&gt;
dotnet new vsa-api &lt;span class="nt"&gt;-n&lt;/span&gt; MiEmpresa.Api &lt;span class="nt"&gt;-o&lt;/span&gt; MiEmpresa.Api &lt;span class="nt"&gt;--IncludeSampleFeature&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;👉 &lt;strong&gt;Repositorio oficial:&lt;/strong&gt; &lt;a href="https://github.com/betoramiz/vsa-template" rel="noopener noreferrer"&gt;github.com/betoramiz/vsa-template&lt;/a&gt;&lt;br&gt;&lt;br&gt;
👉 &lt;strong&gt;Wiki con detalles arquitectónicos:&lt;/strong&gt; &lt;a href="https://github.com/betoramiz/vsa-template/wiki" rel="noopener noreferrer"&gt;Documentación en GitHub Wiki&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  ¡Dejame conocer tu experiencia!
&lt;/h3&gt;

&lt;p&gt;¿Te ha tocado sufrir soluciones con decenas de capas y proyectos vacíos? ¿Te animarías a probar Vertical Slice Architecture en tu próximo proyecto en .NET? &lt;/p&gt;

&lt;p&gt;¡Deja tus reflexiones, preguntas o discrepancias en los comentarios! Comparte esta trilogía con tu equipo si crees que les ahorrará horas de boilerplate. ¡Hasta la próxima!&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>architecture</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Anatomía de un Slice en Producción: Handlers sin interfaces, EF Core sin repositorios y eventos desacoplados (Parte 2)</title>
      <dc:creator>Beto Ramirez</dc:creator>
      <pubDate>Sun, 27 Sep 2026 20:37:00 +0000</pubDate>
      <link>https://dev.to/betoramiz/anatomia-de-un-slice-en-produccion-handlers-sin-interfaces-ef-core-sin-repositorios-y-eventos-4cl9</link>
      <guid>https://dev.to/betoramiz/anatomia-de-un-slice-en-produccion-handlers-sin-interfaces-ef-core-sin-repositorios-y-eventos-4cl9</guid>
      <description>&lt;p&gt;En la &lt;a href="https://dev.to/betoramiz/deconstruyendo-clean-architecture-por-que-nos-cansa-y-como-vertical-slice-architecture-nos-devuelve-la-paz-en-net-parte-1"&gt;Parte 1 de esta serie&lt;/a&gt;, exploramos por qué Clean Architecture tradicional suele agotar a los equipos con su laberinto de proyectos y capas. Vimos la filosofía de &lt;strong&gt;Vertical Slice Architecture (VSA)&lt;/strong&gt;: &lt;em&gt;la complejidad se paga por slice, no por proyecto&lt;/em&gt;, y cómo organizar Minimal APIs limpias y auto-descubiertas.&lt;/p&gt;

&lt;p&gt;Pero ahora viene lo bueno: &lt;strong&gt;abrir el capó del motor y escribir código de producción&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;En este artículo responderemos a las preguntas que todo desarrollador .NET se hace al enfrentarse a VSA por primera vez:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;¿Cómo estructuro un caso de uso para no terminar con un archivo inmanejable de 1000 líneas?&lt;/li&gt;
&lt;li&gt;¿Dónde quedaron las interfaces? ¿Es pecado no poner &lt;code&gt;ICreateProductService&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;¿Por qué dejamos de usar MediatR en pleno 2026?&lt;/li&gt;
&lt;li&gt;¿Cómo persistimos con Entity Framework Core sin caer en la trampa del patrón repositorio genérico?&lt;/li&gt;
&lt;li&gt;¿Cómo validamos peticiones y cómo hacemos que dos slices se comuniquen sin acoplarse?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Vamos paso a paso.&lt;/p&gt;




&lt;h2&gt;
  
  
  Anatomía de un caso de uso: Todo lo que cambia junto, vive junto
&lt;/h2&gt;

&lt;p&gt;En Clean Architecture, para entender qué hace "Crear Producto", tienes que abrir la entidad en un proyecto, el DTO en otro, el validador en otro y el handler en otro.&lt;/p&gt;

&lt;p&gt;En VSA aplicamos el principio de &lt;strong&gt;cohesión espacial&lt;/strong&gt;: todo lo relacionado con ese caso de uso específico vive en un único archivo: &lt;code&gt;Features/Products/CreateProduct.cs&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Observa el código completo de 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;using&lt;/span&gt; &lt;span class="nn"&gt;FluentValidation&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.AspNetCore.Http.HttpResults&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;VsaTemplate.Common.Events&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;VsaTemplate.Common.Persistence&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;VsaTemplate.Features.Products.Contracts&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;VsaTemplate.Features.Products.Domain&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;VsaTemplate.Features.Products&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// 1. Contratos y validación agrupados bajo el nombre del caso de uso&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;class&lt;/span&gt; &lt;span class="nc"&gt;CreateProduct&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;Name&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;Price&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;Id&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;Name&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;Price&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;Validator&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AbstractValidator&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="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;Validator&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;RuleFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&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;NotEmpty&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;MaximumLength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;200&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nf"&gt;RuleFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&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;GreaterThan&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="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// 2. La lógica del caso de uso en su clase Handler contigua&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;CreateProductHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AppDbContext&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IEventDispatcher&lt;/span&gt; &lt;span class="n"&gt;eventDispatcher&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;Created&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CreateProduct&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;&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;CreateProduct&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;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;Id&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;Name&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;Name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Trim&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="n"&gt;request&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;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Products&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;product&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChangesAsync&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="c1"&gt;// Publicar evento in-process para otros interesados&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;eventDispatcher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Publish&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;ProductCreated&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;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="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="n"&gt;TypedResults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Created&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="n"&gt;Id&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;new&lt;/span&gt; &lt;span class="n"&gt;CreateProduct&lt;/span&gt;&lt;span class="p"&gt;.&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;product&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;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;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;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Analicemos la genialidad de esta estructura:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Clase estática contenedora&lt;/strong&gt;: &lt;code&gt;CreateProduct&lt;/code&gt; actúa como un espacio de nombres local. Sus DTOs son &lt;code&gt;CreateProduct.Request&lt;/code&gt; y &lt;code&gt;CreateProduct.Response&lt;/code&gt;. Se acabaron los nombres kilométricos como &lt;code&gt;CreateProductCommandDto&lt;/code&gt; o &lt;code&gt;CreateProductResultModel&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Constructor primario de C#&lt;/strong&gt;: Las dependencias (&lt;code&gt;AppDbContext&lt;/code&gt;, &lt;code&gt;IEventDispatcher&lt;/code&gt;) entran directamente en la firma de &lt;code&gt;CreateProductHandler(...)&lt;/code&gt;. Sin campos privados &lt;code&gt;private readonly&lt;/code&gt;, sin boilerplate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Todo a la vista&lt;/strong&gt;: En 45 líneas tienes el contrato de entrada, el contrato de salida, las reglas de validación y la lógica de ejecución. Si el negocio te pide cambiar el largo máximo del nombre a 300 caracteres, modificas &lt;strong&gt;este archivo y nada más&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  El fin de las interfaces ceremoniales: Cero interfaces para tus handlers
&lt;/h2&gt;

&lt;p&gt;Si vienes del mundo corporativo de .NET, tu instinto probablemente te grite:&lt;br&gt;&lt;br&gt;
&lt;em&gt;«¡Espera! ¿Dónde está &lt;code&gt;ICreateProductHandler&lt;/code&gt;? ¿Cómo vas a registrar eso en el contenedor de dependencias? ¿Cómo lo vas a testear?»&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Hablemos con honestidad:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Una interfaz sirve para intercambiar implementaciones en tiempo de ejecución. Un caso de uso de negocio tiene exactamente UNA implementación.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Crear una interfaz &lt;code&gt;ICreateProductHandler&lt;/code&gt; cuya única clase hija es &lt;code&gt;CreateProductHandler&lt;/code&gt; no es desacoplamiento: es &lt;strong&gt;ceremonia&lt;/strong&gt;. Es código que tienes que mantener, renombrar y navegar en el IDE sin recibir ningún beneficio a cambio.&lt;/p&gt;
&lt;h3&gt;
  
  
  ¿Y la testabilidad?
&lt;/h3&gt;

&lt;p&gt;La testabilidad no viene de tener una interfaz; viene de &lt;strong&gt;cómo recibe sus dependencias la clase&lt;/strong&gt;. Como &lt;code&gt;CreateProductHandler&lt;/code&gt; recibe todo por su constructor primario, en tus pruebas unitarias simplemente haces:&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;handler&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;CreateProductHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dbRealOInMemory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fakeDispatcher&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;await&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;¡No necesitas mockear la interfaz del handler porque estás testeando el handler mismo!&lt;/p&gt;

&lt;h3&gt;
  
  
  Dónde SÍ usamos interfaces
&lt;/h3&gt;

&lt;p&gt;La regla nunca fue &lt;em&gt;"no usar interfaces"&lt;/em&gt;, sino &lt;em&gt;"no usar interfaces sin una segunda implementación real"&lt;/em&gt;. Las interfaces se reservan para las &lt;strong&gt;fronteras del sistema&lt;/strong&gt;, donde genuinamente existen implementaciones alternativas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;TimeProvider&lt;/code&gt; (reloj real del sistema vs reloj congelado para tests).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;IEventDispatcher&lt;/code&gt; (despachador in-process vs despachador de test o distribuido).&lt;/li&gt;
&lt;li&gt;Pasarelas de pago (&lt;code&gt;IStripeService&lt;/code&gt; vs &lt;code&gt;FakePaymentGateway&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Envío de correos (&lt;code&gt;SendGridClient&lt;/code&gt; vs &lt;code&gt;NullEmailSender&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Registro automático por convención con Scrutor
&lt;/h3&gt;

&lt;p&gt;Para no tener que registrar a mano cada handler en el contenedor de dependencias con &lt;code&gt;services.AddScoped&amp;lt;CreateProductHandler&amp;gt;()&lt;/code&gt;, usamos la librería &lt;strong&gt;Scrutor&lt;/strong&gt; para escanear el ensamblado al iniciar la aplicación.&lt;/p&gt;

&lt;p&gt;En &lt;code&gt;Composition/UseCaseRegistration.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;namespace&lt;/span&gt; &lt;span class="nn"&gt;VsaTemplate.Composition&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;class&lt;/span&gt; &lt;span class="nc"&gt;UseCaseRegistration&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;AddUseCaseHandlers&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="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;Scan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scan&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;scan&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FromAssemblyOf&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddClasses&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;classes&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;classes&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;t&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; 
                &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;IsPublic&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;IsAbstract&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="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;t&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;EndsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Handler"&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsSelf&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithScopedLifetime&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;¿Qué hace esto?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Busca cualquier clase pública que termine en &lt;code&gt;Handler&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;La registra &lt;code&gt;AsSelf()&lt;/code&gt; (su propio tipo concreto) con ciclo de vida &lt;code&gt;Scoped&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consecuencia:&lt;/strong&gt; Cuando creas un nuevo caso de uso (ej. &lt;code&gt;UpdateProductHandler&lt;/code&gt;), &lt;strong&gt;no tocas ningún archivo de configuración&lt;/strong&gt;. El contenedor ya sabe resolverlo.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  ¿Por qué le dijimos adiós a MediatR?
&lt;/h2&gt;

&lt;p&gt;Durante años, MediatR fue el estándar indiscutible en .NET para implementar el patrón Command/Query. Pero el contexto ha cambiado drásticamente:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    ERA ANTERIOR (Controllers + MediatR)
    HTTP Request ──► Controller ──► MediatR.Send ──► Pipeline ──► Handler
    (Mucha delegación innecesaria para terminar en el mismo método)

    ERA MODERNA (Minimal APIs + Endpoint Filters)
    HTTP Request ──► Minimal API Endpoint ──► Endpoint Filter ──► Handler
    (Directo, nativo, sin intermediarios ni reflexión costosa)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Minimal APIs resuelve el enrutamiento directamente al handler&lt;/strong&gt;: En el mapeo de rutas simplemente inyectas el handler:
&lt;code&gt;(CreateProduct.Request req, CreateProductHandler handler) =&amp;gt; handler.Handle(req)&lt;/code&gt;.
Minimal APIs resuelve el handler del contenedor y lo llama directo. No hay nada que "mediar".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Los cross-cutting concerns ahora son Endpoint Filters&lt;/strong&gt;: El logging, la validación o las métricas que antes metías en &lt;code&gt;IPipelineBehavior&amp;lt;TRequest, TResponse&amp;gt;&lt;/code&gt; ahora se manejan con los filtros nativos de ASP.NET Core (&lt;code&gt;EndpointFilter&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cambio de licenciamiento&lt;/strong&gt;: A partir de 2025, MediatR adoptó un modelo de licencia comercial. Para qué pagar o preocuparte por licencias cuando el framework base de .NET ya te da todas las herramientas necesarias de forma más rápida y limpia.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Persistencia con EF Core: El "antipatrón" del repositorio genérico
&lt;/h2&gt;

&lt;p&gt;Muchos proyectos de Clean Architecture meten una capa obligatoria de repositorios: &lt;code&gt;IRepository&amp;lt;T&amp;gt;&lt;/code&gt;, &lt;code&gt;IProductRepository&lt;/code&gt;, etc.&lt;/p&gt;

&lt;p&gt;Pero recordemos un detalle técnico elemental:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Entity Framework Core ya implementa los patrones Unit of Work (&lt;code&gt;DbContext&lt;/code&gt;) y Repository (&lt;code&gt;DbSet&amp;lt;T&amp;gt;&lt;/code&gt;).&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Crear un &lt;code&gt;ProductRepository&lt;/code&gt; encima de EF Core solo para hacer:&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;Product&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetByIdAsync&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;id&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="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FindAsync&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;es crear una abstracción con fuga (&lt;em&gt;leaky abstraction&lt;/em&gt;). Pierdes la potencia de LINQ, la capacidad de hacer proyecciones directas (&lt;code&gt;.Select()&lt;/code&gt;), paginación eficiente, &lt;code&gt;AsNoTracking()&lt;/code&gt;, o splits queries.&lt;/p&gt;

&lt;p&gt;En nuestra plantilla, los handlers consumen &lt;code&gt;AppDbContext&lt;/code&gt; directamente:&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;product&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Products&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;Id&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;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="nf"&gt;ProductDto&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;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;Name&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;Price&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="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rápido, expresivo y con cero capas intermedias inútiles.&lt;/p&gt;

&lt;h3&gt;
  
  
  La configuración Fluent API vive junto a la entidad
&lt;/h3&gt;

&lt;p&gt;Siguiendo nuestra pregunta guía: &lt;em&gt;¿Qué cambia junto?&lt;/em&gt;, si agregas una columna a &lt;code&gt;Product&lt;/code&gt;, la configuración de base de datos cambia en el mismo commit.&lt;/p&gt;

&lt;p&gt;Por eso, en vez de una carpeta lejana &lt;code&gt;Infrastructure/Data/Configurations/&lt;/code&gt;, colocamos &lt;code&gt;ProductConfiguration.cs&lt;/code&gt; dentro del slice: &lt;code&gt;Features/Products/Domain/&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;namespace&lt;/span&gt; &lt;span class="nn"&gt;VsaTemplate.Features.Products.Domain&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;ProductConfiguration&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IEntityTypeConfiguration&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="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;Configure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EntityTypeBuilder&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;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;Property&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;Name&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;HasMaxLength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;200&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;Property&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;Price&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;HasPrecision&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;18&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;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Property&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;RowVersion&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;IsConcurrencyToken&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;code&gt;AppDbContext&lt;/code&gt; las descubre automáticamente con una sola línea en &lt;code&gt;OnModelCreating&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;modelBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ApplyConfigurationsFromAssembly&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;Program&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;Assembly&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Pero... ¿por qué las migraciones están centralizadas en &lt;code&gt;Common&lt;/code&gt;?
&lt;/h3&gt;

&lt;p&gt;Aquí ocurre algo fascinante. Alguien podría pensar: &lt;em&gt;"Si todo vive en el slice, ¿por qué no ponemos las migraciones dentro de &lt;code&gt;Features/Products/Migrations&lt;/code&gt;?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;La respuesta es técnica: &lt;strong&gt;Entity Framework Core no permite partir las migraciones por carpeta o feature&lt;/strong&gt;.&lt;br&gt;&lt;br&gt;
Cada migración es un cálculo diferencial del modelo completo de datos de la base, apoyado en un único &lt;code&gt;ModelSnapshot&lt;/code&gt; lineal por &lt;code&gt;DbContext&lt;/code&gt;. Si intentaras dividirlas por feature, corromperías la historia secuencial de la base de datos.&lt;/p&gt;

&lt;p&gt;Por eso, las migraciones se generan centralizadas en &lt;code&gt;Common/Persistence/Migrations/&lt;/code&gt;. VSA no es dogma: somos pragmáticos y respetamos la naturaleza de las herramientas que usamos.&lt;/p&gt;


&lt;h2&gt;
  
  
  Validación elegante: FluentValidation en un Endpoint Filter
&lt;/h2&gt;

&lt;p&gt;¿Cómo validamos el request sin llenar el handler de ifs &lt;code&gt;if (string.IsNullOrEmpty(request.Name))&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;En la plantilla utilizamos un filtro de endpoint genérico: &lt;code&gt;ValidationFilter.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;namespace&lt;/span&gt; &lt;span class="nn"&gt;VsaTemplate.Common.Endpoints&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;class&lt;/span&gt; &lt;span class="nc"&gt;ValidationFilterExtensions&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;RouteHandlerBuilder&lt;/span&gt; &lt;span class="n"&gt;WithValidation&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TRequest&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="n"&gt;RouteHandlerBuilder&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;where&lt;/span&gt; &lt;span class="n"&gt;TRequest&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="err"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="nc"&gt;builder&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddEndpointFilter&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;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;next&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;// Obtenemos el validador registrado para este tipo de Request&lt;/span&gt;
                &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;validator&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;HttpContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RequestServices&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;IValidator&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TRequest&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;();&lt;/span&gt;

                &lt;span class="kt"&gt;var&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;context&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;OfType&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TRequest&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;().&lt;/span&gt;&lt;span class="nf"&gt;First&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;await&lt;/span&gt; &lt;span class="n"&gt;validator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ValidateAsync&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;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HttpContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RequestAborted&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

                &lt;span class="c1"&gt;// Si la validación falla, corta la petición y retorna HTTP 400 Bad Request (ProblemDetails)&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="n"&gt;IsValid&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;next&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="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TypedResults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ValidationProblem&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;ToDictionary&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;ProducesValidationProblem&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Documenta automáticamente el error 400 en OpenAPI&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;En el mapeo de la ruta en &lt;code&gt;Endpoints.cs&lt;/code&gt;, solo agregas una extensión encadenada:&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="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CreateProduct&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;CreateProductHandler&lt;/span&gt; &lt;span class="n"&gt;handler&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;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&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="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="nf"&gt;WithName&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;CreateProduct&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithValidation&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CreateProduct&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;&amp;gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Las grandes ventajas de este enfoque:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;El Handler es territorio sagrado&lt;/strong&gt;: El handler sabe que si el código llegó a él, los datos ya son 100% válidos. No necesita manejar &lt;code&gt;ValidationProblem&lt;/code&gt; ni ensuciarse con validaciones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Respuesta estándar&lt;/strong&gt;: Si falla, el cliente recibe automáticamente un &lt;code&gt;RFC 9457 ProblemDetails&lt;/code&gt; estándar con el desglose de errores por propiedad.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fail fast en desarrollo&lt;/strong&gt;: Si usas &lt;code&gt;.WithValidation&amp;lt;T&amp;gt;()&lt;/code&gt; pero olvidaste escribir la clase &lt;code&gt;Validator&lt;/code&gt;, el contenedor falla inmediatamente con una excepción explícita (&lt;code&gt;GetRequiredService&lt;/code&gt;), impidiendo que despliegues un endpoint con validaciones rotas.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Comunicación entre Slices: Eventos in-process
&lt;/h2&gt;

&lt;p&gt;Nuestra &lt;strong&gt;Regla #1&lt;/strong&gt; de VSA dice:&lt;br&gt;&lt;br&gt;
&lt;em&gt;«Un slice no usa los casos de uso ni los handlers de otro slice».&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Pero surge un problema real: cuando se coloca una orden de compra en el slice &lt;code&gt;Orders&lt;/code&gt;, necesitamos que el slice &lt;code&gt;Notifications&lt;/code&gt; envíe un correo y registre un log. ¿Cómo lo hacemos sin que &lt;code&gt;PlaceOrderHandler&lt;/code&gt; llame directamente a &lt;code&gt;Notifications&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;La respuesta: Eventos de dominio in-process.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Un Dispatcher propio de ~40 líneas
&lt;/h3&gt;

&lt;p&gt;En lugar de instalar librerías pesadas, la plantilla incluye un dispatcher sencillo y directo en &lt;code&gt;Common/Events/EventDispatcher.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;namespace&lt;/span&gt; &lt;span class="nn"&gt;VsaTemplate.Common.Events&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;EventDispatcher&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IServiceProvider&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="n"&gt;IEventDispatcher&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="n"&gt;Publish&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TEvent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;TEvent&lt;/span&gt; &lt;span class="n"&gt;@event&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="k"&gt;default&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;TEvent&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt;
    &lt;span class="err"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Resuelve todos los handlers suscritos a TEvent en el scope actual&lt;/span&gt;
        &lt;span class="nc"&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;handler&lt;/span&gt; &lt;span class="k"&gt;in&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;GetServices&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IEventHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TEvent&lt;/span&gt;&lt;span class="p"&gt;&amp;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="n"&gt;handler&lt;/span&gt;&lt;span class="p"&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;@event&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;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  El contrato público vive en &lt;code&gt;Contracts/&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;El slice emisor (&lt;code&gt;Orders&lt;/code&gt;) define su evento en una subcarpeta pública: &lt;code&gt;Features/Orders/Contracts/OrderPlaced.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;namespace&lt;/span&gt; &lt;span class="nn"&gt;VsaTemplate.Features.Orders.Contracts&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;OrderPlaced&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;OrderId&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;ProductId&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;Quantity&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;Total&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cuando se coloca la orden, &lt;code&gt;PlaceOrderHandler&lt;/code&gt; simplemente publica el evento:&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;await&lt;/span&gt; &lt;span class="n"&gt;eventDispatcher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Publish&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;OrderPlaced&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&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;product&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;quantity&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;ct&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 el slice de notificaciones (&lt;code&gt;Features/Notifications/OrderPlacedNotification.cs&lt;/code&gt;), escuchamos el evento:&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;OrderPlacedNotification&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;OrderPlacedNotification&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;IEventHandler&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OrderPlaced&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;public&lt;/span&gt; &lt;span class="n"&gt;Task&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;OrderPlaced&lt;/span&gt; &lt;span class="n"&gt;@event&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;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;"Nueva orden recibida: {OrderId}. Enviando confirmación..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;@event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderId&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;p&gt;&lt;code&gt;Orders&lt;/code&gt; no tiene la más remota idea de que &lt;code&gt;Notifications&lt;/code&gt; existe. Están 100% desacoplados.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;¿Cuándo migrar a Wolverine o MassTransit?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Este dispatcher in-process es ligero y comparte la misma transacción y memoria del request. Si el proceso del servidor se cae a la mitad, el evento en memoria se pierde.&lt;br&gt;&lt;br&gt;
Cuando necesites &lt;strong&gt;garantías transaccionales duras&lt;/strong&gt; (Outbox Pattern), reintentos automáticos ante caídas o enviar mensajes a brokers externos (RabbitMQ, Kafka, Azure Service Bus), el salto natural y recomendado en el ecosistema .NET es &lt;strong&gt;Wolverine&lt;/strong&gt;. Y lo mejor: como tus handlers ya son métodos limpios, migrar a Wolverine requiere mínimos cambios.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Resumen del flujo de un caso de uso
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  HTTP POST /api/products
            │
            ▼
 ┌──────────────────────────────────────┐
 │       Endpoint Route Builder         │
 └──────────────────┬───────────────────┘
                    │
                    ▼
 ┌──────────────────────────────────────┐
 │ .WithValidation&amp;lt;CreateProduct.Req&amp;gt;() │ ──► ¿Inválido? ──► HTTP 400 ProblemDetails
 └──────────────────┬───────────────────┘
                    │ (Válido)
                    ▼
 ┌──────────────────────────────────────┐
 │        CreateProductHandler          │
 │  • Guarda en DbContext (EF Core)     │
 │  • Publica EventDispatcher           │
 └──────────────────┬───────────────────┘
                    │
                    ▼
          HTTP 201 Created (JSON)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Conclusión y lo que viene en la Parte 3
&lt;/h2&gt;

&lt;p&gt;Hemos visto cómo un slice cobra vida:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Casos de uso compactos y altamente cohesivos.&lt;/li&gt;
&lt;li&gt;Handlers concretos sin interfaces ficticias, registrados automáticamente por convención con Scrutor.&lt;/li&gt;
&lt;li&gt;Persistencia directa con Entity Framework Core.&lt;/li&gt;
&lt;li&gt;Validación automática desacoplada del negocio mediante Endpoint Filters.&lt;/li&gt;
&lt;li&gt;Comunicación limpia entre slices mediante eventos in-process.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pero falta la pieza más crítica de todas: &lt;strong&gt;la protección y el futuro&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;¿Cómo testeamos estos slices sin caer en la pesadilla de mockear 20 llamadas a repositorios?&lt;/li&gt;
&lt;li&gt;¿Cómo evitamos que un desarrollador distraído rompa las reglas y acople los slices entre sí?&lt;/li&gt;
&lt;li&gt;¿Cómo compartimos estos modelos C# con el frontend en Angular/React sin desincronizarnos?&lt;/li&gt;
&lt;li&gt;¿Y cómo escala esta arquitectura si el sistema crece a 50 o 100 slices?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Todo eso lo resolveremos en el cierre de la trilogía: &lt;strong&gt;Parte 3: Blindaje, Frontend y Evolución&lt;/strong&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  Código fuente de referencia
&lt;/h3&gt;

&lt;p&gt;El código de todos los ejemplos proviene directamente de la plantilla:&lt;br&gt;&lt;br&gt;
👉 &lt;strong&gt;&lt;a href="https://github.com/betoramiz/vsa-template" rel="noopener noreferrer"&gt;Repositorio en GitHub: betoramiz/vsa-template&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;¡Déja tu opinión en los comentarios! ¿Qué opinas de eliminar las interfaces en los handlers? ¿Prefieres eventos in-process o usar un mediador tradicional?&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>architecture</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Deconstruyendo Clean Architecture: Por qué nos cansa y cómo Vertical Slice Architecture nos devuelve la paz en .NET (Parte 1)</title>
      <dc:creator>Beto Ramirez</dc:creator>
      <pubDate>Sun, 27 Sep 2026 20:36:38 +0000</pubDate>
      <link>https://dev.to/betoramiz/deconstruyendo-clean-architecture-por-que-nos-cansa-y-como-vertical-slice-architecture-nos-4fn8</link>
      <guid>https://dev.to/betoramiz/deconstruyendo-clean-architecture-por-que-nos-cansa-y-como-vertical-slice-architecture-nos-4fn8</guid>
      <description>&lt;p&gt;¿Alguna vez has tenido esa sensación de "estoy escribiendo demasiada burocracia" al crear un endpoint sencillo en .NET?&lt;/p&gt;

&lt;p&gt;Imagina esta escena cotidiana: tu equipo necesita agregar un endpoint para registrar un producto. Una tabla sencilla con tres columnas: &lt;code&gt;Id&lt;/code&gt;, &lt;code&gt;Name&lt;/code&gt; y &lt;code&gt;Price&lt;/code&gt;. Nada del otro mundo.&lt;/p&gt;

&lt;p&gt;Abres tu solución y el viaje comienza:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Creas la entidad en &lt;code&gt;MiApp.Domain&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Defines una interfaz en &lt;code&gt;MiApp.Application.Contracts&lt;/code&gt; (&lt;code&gt;ICreateProductUseCase&lt;/code&gt; o &lt;code&gt;IProductRepository&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Creas un &lt;code&gt;ProductDto&lt;/code&gt;, un &lt;code&gt;CreateProductCommand&lt;/code&gt; y un mapper (o perfil de AutoMapper).&lt;/li&gt;
&lt;li&gt;Implementas la interfaz en &lt;code&gt;MiApp.Application&lt;/code&gt; o &lt;code&gt;MiApp.Infrastructure&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Implementas el repositorio &lt;code&gt;ProductRepository&lt;/code&gt; con un único método &lt;code&gt;AddAsync&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Creas el controlador en &lt;code&gt;MiApp.Api&lt;/code&gt;, inyectas la interfaz y conectas los cables.&lt;/li&gt;
&lt;li&gt;Registras el servicio en el contenedor de Inyección de Dependencias.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Quince minutos después, tienes &lt;strong&gt;7 archivos modificados o creados en 4 proyectos distintos&lt;/strong&gt; para guardar una fila en una base de datos. Y lo peor de todo: esa interfaz &lt;code&gt;ICreateProductService&lt;/code&gt; solo tiene —y tendrá por siempre— &lt;strong&gt;una sola implementación&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Si te sentiste identificado, bienvenido al club. Esto no es falta de ganas: es &lt;strong&gt;fatiga por arquitectura ceremonial&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;En esta serie de 3 entregas exhaustivas, vamos a desarmar este problema y ver cómo construir una Web API en .NET con &lt;strong&gt;Vertical Slice Architecture (VSA)&lt;/strong&gt; de forma pragmática, tomando como referencia una plantilla lista para producción construida con C# moderno y Minimal APIs.&lt;/p&gt;




&lt;h2&gt;
  
  
  El problema de Clean Architecture: El costo se paga el día uno
&lt;/h2&gt;

&lt;p&gt;Clean Architecture, Onion Architecture o la arquitectura hexagonal son herramientas magníficas... cuando el problema que resuelven justifica su costo. El problema ocurre cuando las adoptamos por defecto como si fueran una ley grabada en piedra.&lt;/p&gt;

&lt;p&gt;En una arquitectura por capas tradicional, el software se organiza por &lt;strong&gt;preocupaciones técnicas&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;[ Capa de Presentación / API ]
             │
             ▼
[ Capa de Aplicación (Casos de uso / DTOs) ]
             │
             ▼
[ Capa de Dominio (Entidades / Lógica pura) ]
             ▲
             │
[ Capa de Infraestructura (EF Core / Repositorios) ]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cada capa vive en un proyecto &lt;code&gt;.csproj&lt;/code&gt; separado. Para cualquier cambio, tu mente y tu editor tienen que saltar de un proyecto a otro. Las cosas que cambian juntas están separadas por la fuerza.&lt;/p&gt;

&lt;p&gt;Pero lo más peligroso es esto:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;En Clean Architecture pagas el costo máximo de complejidad desde el día 1 para todos los casos de uso, incluso para los más triviales.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Un CRUD de lectura de 2 campos paga la misma ceremonia que un motor de pricing financiero hipercomplejo con reglas de negocio cambiantes.&lt;/p&gt;

&lt;p&gt;¿Y si invertimos la fórmula?&lt;/p&gt;




&lt;h2&gt;
  
  
  La idea central: La complejidad se paga por slice
&lt;/h2&gt;

&lt;p&gt;Aquí es donde entra &lt;strong&gt;Vertical Slice Architecture (VSA)&lt;/strong&gt;, popularizada por referentes como Jimmy Bogard (creador de MediatR) y refinada por Derek Comartin (CodeOpinion) y Milan Jovanović.&lt;/p&gt;

&lt;p&gt;En VSA, la estructura del código no refleja la técnica (capas), sino las &lt;strong&gt;capacidades del negocio&lt;/strong&gt; (features):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;       CAPAS TRADICIONALES                   VERTICAL SLICE
     (División por técnica)               (División por negocio)

  ┌─────────────────────────┐          ┌──────────┐ ┌──────────┐
  │      Presentación       │          │          │ │          │
  ├─────────────────────────┤          │ Feature  │ │ Feature  │
  │       Aplicación        │   ──►    │ Products │ │  Orders  │
  ├─────────────────────────┤          │          │ │          │
  │         Dominio         │          │          │ │          │
  ├─────────────────────────┤          │          │ │          │
  │     Infraestructura     │          │          │ │          │
  └─────────────────────────┘          └──────────┘ └──────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;En lugar de rebanar el sistema de forma horizontal, lo cortamos en &lt;strong&gt;rebanadas verticales&lt;/strong&gt;. Un slice atraviesa todo lo necesario para resolver ese caso de uso: desde la ruta HTTP hasta la consulta a la base de datos.&lt;/p&gt;

&lt;p&gt;La premisa de oro es:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;La complejidad se paga por slice, no por proyecto.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;¿Un caso de uso es un simple reporte o un CRUD directo? Se resuelve en un único archivo, usando consultas directas sin ceremonias.&lt;/li&gt;
&lt;li&gt;¿Otro caso de uso es crítico, tiene reglas complejas y requiere validación avanzada y eventos? Ese slice particular tendrá un modelo de dominio rico, validadores dedicados y más pruebas.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Y lo mejor: &lt;strong&gt;la complejidad de un caso de uso difícil no contamina al resto de la aplicación&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Las 5 reglas que sustituyen a la policía de capas
&lt;/h2&gt;

&lt;p&gt;En Clean Architecture, la separación en múltiples proyectos &lt;code&gt;.csproj&lt;/code&gt; existe para que el compilador nos impida hacer trampas (por ejemplo, que el dominio no referencie a la base de datos). &lt;/p&gt;

&lt;p&gt;Al movernos a una arquitectura de vertical slices dentro de un solo proyecto Web API, eliminamos los muros físicos. Pero para que no se convierta en el temido "código espagueti", establecemos &lt;strong&gt;5 reglas de oro explícitas&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Un slice no usa los casos de uso de otro slice.&lt;/strong&gt;
Lo público de un slice es únicamente su modelo de datos (&lt;code&gt;Domain/&lt;/code&gt;, para consultas compartidas) y sus eventos (&lt;code&gt;Contracts/&lt;/code&gt;). La lógica interna de un caso de uso es privada.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nada entra a &lt;code&gt;Common&lt;/code&gt; hasta que se repita al menos 3 veces.&lt;/strong&gt;
La abstracción prematura es la raíz de la complejidad innecesaria. No crees una clase base genérica porque "quizás la usemos luego". Si dos features hacen algo similar, permítete duplicar un poco de código. A la tercera repetición, se refactoriza a &lt;code&gt;Common&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;El archivo de endpoints se mantiene tonto.&lt;/strong&gt;
Solo mapea la ruta HTTP hacia el handler correspondiente. Cero lógica de negocio, cero consultas directas en el mapeo de rutas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;La complejidad es opt-in por slice.&lt;/strong&gt;
Validadores, modelos enriquecidos, manejo transaccional... se agregan únicamente donde el negocio lo exige.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Camino de crecimiento sin saltos dramáticos.&lt;/strong&gt;
Feature pequeño en una carpeta → Feature grande convertido en módulo → Monolito Modular → Microservicio (solo si realmente hace falta escalabilidad independiente).&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Estructura de proyecto: Menos proyectos, más claridad
&lt;/h2&gt;

&lt;p&gt;¿Cómo se ve esto en la vida real? En lugar de tener una solución con 5 proyectos &lt;code&gt;.csproj&lt;/code&gt;, arrancamos con &lt;strong&gt;un solo proyecto Web API&lt;/strong&gt; y su proyecto de tests correspondiente:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Crear solución y proyectos&lt;/span&gt;
dotnet new slnx &lt;span class="nt"&gt;-n&lt;/span&gt; MiApp
dotnet new web &lt;span class="nt"&gt;-n&lt;/span&gt; MiApp &lt;span class="nt"&gt;-o&lt;/span&gt; src/MiApp
dotnet new xunit &lt;span class="nt"&gt;-n&lt;/span&gt; MiApp.Tests &lt;span class="nt"&gt;-o&lt;/span&gt; tests/MiApp.Tests

dotnet sln add src/MiApp tests/MiApp.Tests
dotnet add tests/MiApp.Tests reference src/MiApp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La estructura dentro de &lt;code&gt;src/MiApp&lt;/code&gt; queda limpia, intuitiva y navegable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/MiApp/
├── Program.cs                    ← Composición de la app
├── Common/                       ← Infraestructura compartida real
│   ├── Endpoints/                ← Contratos base y filtros
│   ├── Persistence/              ← DbContext global y migraciones
│   └── ErrorHandling/            ← Manejo de errores RFC 9457
├── Composition/
│   └── UseCaseRegistration.cs    ← Registro automático por convención
└── Features/
    ├── Products/                 ← Slice de Productos
    │   ├── Endpoints.cs          ← Solo enrutamiento de este feature
    │   ├── CreateProduct.cs      ← Un caso de uso = un archivo
    │   ├── GetProducts.cs
    │   ├── GetProductById.cs
    │   ├── DeleteProduct.cs
    │   └── Domain/
    │       ├── Product.cs
    │       └── ProductConfiguration.cs
    └── Orders/                   ← Slice de Órdenes
        ├── Endpoints.cs
        ├── PlaceOrder.cs
        ├── GetOrderById.cs
        ├── Contracts/
        │   └── OrderPlaced.cs    ← Eventos públicos para otros slices
        └── Domain/
            └── Order.cs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nota algo fundamental: cuando te asignen una tarea sobre "Productos", &lt;strong&gt;nunca sales de la carpeta &lt;code&gt;Features/Products&lt;/code&gt;&lt;/strong&gt;. No tienes que abrir 6 carpetas en el explorador de soluciones. Todo lo que cambia junto, vive junto.&lt;/p&gt;




&lt;h2&gt;
  
  
  Enrutamiento con Minimal APIs: Endpoints tontos y limpios
&lt;/h2&gt;

&lt;p&gt;Hay quienes meten toda la definición de rutas, validación y lógica dentro del propio &lt;code&gt;Program.cs&lt;/code&gt;. A los 5 endpoints, &lt;code&gt;Program.cs&lt;/code&gt; se vuelve inleíble.&lt;/p&gt;

&lt;p&gt;Nuestra decisión de diseño fue:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Separar las rutas de la lógica&lt;/strong&gt;: Cada feature tiene un archivo &lt;code&gt;Endpoints.cs&lt;/code&gt;. Funciona como la "tabla de contenidos" del slice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Endpoints tontos&lt;/strong&gt;: El endpoint solo recibe la petición HTTP y se la pasa al handler.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Observa cómo se ve &lt;code&gt;Features/Products/Endpoints.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;namespace&lt;/span&gt; &lt;span class="nn"&gt;VsaTemplate.Features.Products&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;Endpoints&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IFeatureEndpoints&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;Map&lt;/span&gt;&lt;span class="p"&gt;(&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="k"&gt;group&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/products"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WithTags&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="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="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CreateProduct&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;CreateProductHandler&lt;/span&gt; &lt;span class="n"&gt;handler&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;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&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="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="nf"&gt;WithName&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;CreateProduct&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithValidation&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CreateProduct&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;&amp;gt;();&lt;/span&gt;

        &lt;span class="k"&gt;group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapGet&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="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;pageSize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;GetProductsHandler&lt;/span&gt; &lt;span class="n"&gt;handler&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;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&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;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pageSize&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="nf"&gt;WithName&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;GetProducts&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

        &lt;span class="k"&gt;group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapGet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/{id:guid}"&lt;/span&gt;&lt;span class="p"&gt;,&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;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;GetProductByIdHandler&lt;/span&gt; &lt;span class="n"&gt;handler&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;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&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;id&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="nf"&gt;WithName&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;GetProductById&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

        &lt;span class="k"&gt;group&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;"/{id:guid}"&lt;/span&gt;&lt;span class="p"&gt;,&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;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DeleteProductHandler&lt;/span&gt; &lt;span class="n"&gt;handler&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;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="p"&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;id&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="nf"&gt;WithName&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;DeleteProduct&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;¿Qué logramos con esto?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;En 20 líneas de código entiendes todo lo que ofrece el módulo de productos.&lt;/li&gt;
&lt;li&gt;Las rutas están agrupadas con prefijo &lt;code&gt;/api/products&lt;/code&gt; y categorizadas para OpenAPI.&lt;/li&gt;
&lt;li&gt;Cada endpoint delega inmediatamente en su &lt;code&gt;*Handler&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.WithName(...)&lt;/code&gt; le da una identidad única a la operación (clave para el frontend, como veremos en la Parte 3).&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Auto-descubrimiento de Endpoints: Olvídate de tocar Program.cs
&lt;/h2&gt;

&lt;p&gt;Uno de los mayores dolores de cabeza en proyectos grandes es el archivo central de configuración. Cada vez que alguien crea un feature, tiene que tocar &lt;code&gt;Program.cs&lt;/code&gt; para registrarlo. Si 3 programadores hacen esto al mismo tiempo en ramas distintas, prepárate para los conflictos de Git (merge conflicts).&lt;/p&gt;

&lt;p&gt;Para resolver esto, implementamos un patrón de &lt;strong&gt;auto-descubrimiento&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Aprovechando las interfaces con métodos estáticos abstractos (&lt;code&gt;static abstract&lt;/code&gt;) introducidas en C# 11, definimos un contrato muy simple en &lt;code&gt;Common/Endpoints/IFeatureEndpoints.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;namespace&lt;/span&gt; &lt;span class="nn"&gt;VsaTemplate.Common.Endpoints&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;interface&lt;/span&gt; &lt;span class="nc"&gt;IFeatureEndpoints&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;abstract&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;(&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Y luego, en &lt;code&gt;Common/Endpoints/EndpointRegistration.cs&lt;/code&gt;, creamos un método de extensión que escanea el ensamblado al arrancar y mapea automáticamente todas las clases que implementen esta interfaz:&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;EndpointRegistration&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;IEndpointRouteBuilder&lt;/span&gt; &lt;span class="nf"&gt;MapFeatureEndpoints&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;endpointTypes&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;Program&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;Assembly&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetTypes&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;t&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;IsAbstract&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;IsInterface&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="p"&gt;&amp;amp;&amp;amp;&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;IFeatureEndpoints&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;IsAssignableFrom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&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;type&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;endpointTypes&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;mapMethod&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="nf"&gt;GetMethod&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;IFeatureEndpoints&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
                &lt;span class="n"&gt;BindingFlags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Public&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;BindingFlags&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Static&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="n"&gt;mapMethod&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;Invoke&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="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="p"&gt;}&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;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ahora, en tu &lt;code&gt;Program.cs&lt;/code&gt;, solo necesitas una sola línea:&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;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="c1"&gt;// Registra todos los endpoints de todos los features automáticamente&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;MapFeatureEndpoints&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;blockquote&gt;
&lt;p&gt;&lt;strong&gt;El resultado&lt;/strong&gt;: Agregar un feature nuevo es tan simple como crear la carpeta &lt;code&gt;Features/Clientes/&lt;/code&gt;, agregar su &lt;code&gt;Endpoints.cs&lt;/code&gt; implementando &lt;code&gt;IFeatureEndpoints&lt;/code&gt;, y listo. El sistema lo descubre al compilar. &lt;strong&gt;No tocas &lt;code&gt;Program.cs&lt;/code&gt; para nada.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Conclusión y lo que viene
&lt;/h2&gt;

&lt;p&gt;Hemos pasado de una maraña de capas horizontales a un esquema vertical donde:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Organizamos el código por &lt;strong&gt;capacidades de negocio&lt;/strong&gt;, no por conceptos técnicos.&lt;/li&gt;
&lt;li&gt;Eliminamos proyectos &lt;code&gt;.csproj&lt;/code&gt; innecesarios y reducimos la fricción mental.&lt;/li&gt;
&lt;li&gt;Definimos rutas limpias con Minimal APIs y las descubrimos automáticamente sin tocar archivos centrales.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Pero ahora viene la pregunta del millón:&lt;br&gt;&lt;br&gt;
&lt;em&gt;¿Cómo se ve por dentro ese &lt;code&gt;CreateProduct.cs&lt;/code&gt;?&lt;/em&gt;&lt;br&gt;&lt;br&gt;
&lt;em&gt;¿Dónde quedaron las interfaces? ¿Cómo inyectamos las dependencias si no usamos &lt;code&gt;ICreateProductHandler&lt;/code&gt;?&lt;/em&gt;&lt;br&gt;&lt;br&gt;
&lt;em&gt;¿Y qué pasó con MediatR? ¿Cómo interactuamos con Entity Framework Core sin escribir una capa de repositorios?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Todo eso lo veremos en la &lt;strong&gt;Parte 2: Anatomía de un Slice en Producción&lt;/strong&gt;, donde destriparemos el diseño de los Handlers, la validación elegante sin ensuciar el negocio, y la comunicación entre slices mediante eventos ligeros.&lt;/p&gt;




&lt;h3&gt;
  
  
  ¿Quieres explorar el código completo?
&lt;/h3&gt;

&lt;p&gt;Puedes clonar y probar la plantilla directamente desde el repositorio de GitHub:&lt;br&gt;&lt;br&gt;
👉 &lt;strong&gt;&lt;a href="https://github.com/betoramiz/vsa-template" rel="noopener noreferrer"&gt;betoramiz/vsa-template&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Cuéntame en los comentarios: ¿En tu equipo actual usan Clean Architecture tradicional? ¿Cuántos proyectos tiene tu solución .NET habitual? ¡Nos leemos abajo!&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>architecture</category>
      <category>webdev</category>
    </item>
    <item>
      <title>CoreDriven: Un Template de Arquitectura Limpia en .NET para Acelerar tu Desarrollo</title>
      <dc:creator>Beto Ramirez</dc:creator>
      <pubDate>Sun, 10 Aug 2025 22:07:02 +0000</pubDate>
      <link>https://dev.to/betoramiz/coredriven-un-template-de-arquitectura-limpia-en-net-para-acelerar-tu-desarrollo-3691</link>
      <guid>https://dev.to/betoramiz/coredriven-un-template-de-arquitectura-limpia-en-net-para-acelerar-tu-desarrollo-3691</guid>
      <description>&lt;p&gt;Para cualquier desarrollador de .NET, iniciar un nuevo proyecto implica una serie de decisiones cruciales sobre la arquitectura. ¿Cómo aseguramos que la aplicación sea mantenible, escalable y, sobre todo, fácil de probar? La Arquitectura Limpia (Clean Architecture) nos ofrece una solución robusta, y con el template CoreDriven, he querido llevar esa implementación un paso más allá.&lt;/p&gt;

&lt;p&gt;Este post explora las características de &lt;a href="https://www.netmentor.es/entrada/core-driven-architecture" rel="noopener noreferrer"&gt;Core-Driven&lt;/a&gt;, un template de proyecto en .NET Core diseñado para programadores que buscan una base sólida y moderna para sus aplicaciones.&lt;/p&gt;

&lt;h3&gt;
  
  
  El Fundamento: Arquitectura Limpia
&lt;/h3&gt;

&lt;p&gt;CoreDriven se basa en los principios de la Arquitectura Limpia, garantizando una clara separación de responsabilidades. El dominio y la lógica de negocio (los casos de uso) están en el centro, sin dependencias de frameworks externos como Entity Framework o ASP.NET Core.&lt;/p&gt;

&lt;p&gt;Esto se traduce en beneficios directos:&lt;/p&gt;

&lt;p&gt;Independencia: Tu lógica de negocio no sabe nada sobre la base de datos, la UI o cualquier servicio externo.&lt;/p&gt;

&lt;p&gt;Testabilidad: Probar las reglas de negocio se vuelve trivial, ya que no requieren un entorno web o una base de datos activa.&lt;/p&gt;

&lt;p&gt;Mantenibilidad: Cambiar un detalle de infraestructura (como pasar de SQL Server a PostgreSQL) no afecta al corazón de tu aplicación.&lt;/p&gt;

&lt;h3&gt;
  
  
  Casos de Uso, una Sintaxis más Limpia
&lt;/h3&gt;

&lt;p&gt;Una de las decisiones de diseño en CoreDriven es cómo se estructuran los casos de uso (Use Cases). En lugar de usar una sola clasepara multiples funcionalidades, hacemos uso de una clase para solamente una solan funcinalidad, esto hara que el caso de uso tenga una forma lógica y concisa.&lt;/p&gt;

&lt;p&gt;Observa la diferencia. La implementación de un caso de uso se agrupa dentro de un solo archivo, mejorando la cohesión y la legibilidad.&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%2F8x7y35nehi852s9esyu9.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%2F8x7y35nehi852s9esyu9.png" alt="Use-Case" width="578" height="540"&gt;&lt;/a&gt;&lt;br&gt;
Este enfoque no solo reduce el número de archivos, sino que también hace que la navegación y la comprensión del flujo de una feature sean mucho más intuitivas.&lt;/p&gt;

&lt;p&gt;Ahora bien, en la capa de casos de uso podemos agruparlos con un nombre descriptivo que nos diga exactamente que es lo que el caso de uso resulve.&lt;br&gt;
Por ejemplo, en un TodoList, el caso de uso de creacion puede contener los archivos de Creacion del Todo, el Request, Response y las validaciones.&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%2Fdf6f99h6hm22ilkfbi2n.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%2Fdf6f99h6hm22ilkfbi2n.png" alt="Folder-structure" width="191" height="212"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Y asi podemos listar todos los casos de uso para la entidad Todo.&lt;/p&gt;

&lt;p&gt;La agrepacion de todos los casos de uso correspondiente a Todo la haremos con record que contega todos los casos de uso y que ademas implmente la interfaz IUseCaseRepository.&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%2Fz5kinps42tnl4jwdqol4.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%2Fz5kinps42tnl4jwdqol4.png" alt="usecase-repository" width="613" height="70"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Inyección de Dependencias Automática con Scrutor.
&lt;/h3&gt;

&lt;p&gt;Configurar la inyección de dependencias puede ser tedioso. Registrar cada servicio y su interfaz (services.AddScoped()) en proyectos grandes consume tiempo y es propenso a errores.&lt;/p&gt;

&lt;p&gt;Con este enfoque CorDriven camos a hacer uso de Scrutor, una pequeña pero potente librería que automatiza este proceso mediante el escaneo de ensamblados. Con unas pocas líneas de código, todos tus servicios, handlers y validadores de FluentValidation quedan registrados.&lt;/p&gt;

&lt;p&gt;Vamos a definir dos interfaces que solo seran indicadores para que Scrutor sepa donde buscar.&lt;/p&gt;

&lt;h5&gt;
  
  
  La interfaz IUseCase y la interfaz IUseIUseCaseRepository
&lt;/h5&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%2F3re0pyfbaxjz9tsdjmhf.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%2F3re0pyfbaxjz9tsdjmhf.png" alt="interfaces" width="285" height="119"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ahora cada uno de nuestros casos de uso pueden hacer uso de la interfaz IUseCase y el record que agrupa a todos los use cases hara uso de la interfaz IUseCaseRepository.&lt;/p&gt;

&lt;p&gt;Esto hara que la inyeccion de dependencias con Scrutor sea muy simple:&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%2Fjyh9lx2y5w02aj13r53w.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%2Fjyh9lx2y5w02aj13r53w.png" alt="Scrutor" width="661" height="217"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Pruebas Unitarias Simples y Confiables.
&lt;/h3&gt;

&lt;p&gt;Gracias a la arquitectura desacoplada, probar nuestros casos de uso es increíblemente fácil. No necesitamos mocks complejos ni configuraciones aparatosas. La lógica de negocio está aislada y lista para ser validada.&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%2Fvy2oy4i3fl7tm477zedp.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%2Fvy2oy4i3fl7tm477zedp.png" alt="UnitTest" width="800" height="307"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;La prueba es limpia, legible y se enfoca exclusivamente en la lógica del caso de uso, validando su comportamiento de forma aislada.&lt;/p&gt;

&lt;p&gt;Si buscas una base sólida, moderna y bien estructurada para tu próximo proyecto en .NET, te invito a que le des una oportunidad a CoreDriven.&lt;/p&gt;

&lt;p&gt;Visita el repositorio en GitHub: &lt;a href="https://github.com/betoramiz/coredriven" rel="noopener noreferrer"&gt;https://github.com/betoramiz/coredriven&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;¡Clónalo y empieza a construir!&lt;/p&gt;

&lt;p&gt;Cualquier feedback, sugerencia o contribución es más que bienvenida. ¡Espero que este template te ayude a construir software increíble!&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Separando la logica de la capa de negocios en casos de uso.</title>
      <dc:creator>Beto Ramirez</dc:creator>
      <pubDate>Wed, 09 Apr 2025 20:39:35 +0000</pubDate>
      <link>https://dev.to/betoramiz/separando-la-logica-de-la-capa-de-negocios-en-casos-de-uso-48ka</link>
      <guid>https://dev.to/betoramiz/separando-la-logica-de-la-capa-de-negocios-en-casos-de-uso-48ka</guid>
      <description>&lt;p&gt;En este post quiero explicar como se puede lograr una separacion entre a logica de la aplicacion o capa de reglas de negocio del resto de las capas de la arquitectura que estemos utilizando.&lt;/p&gt;

&lt;p&gt;El codigo completo lo puedes encontrar &lt;a href="https://github.com/betoramiz/usecaseDispatcher" rel="noopener noreferrer"&gt;Aqui&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;He tenido la oportunidad de trabajar en distintos tipos de proyectos, algunos muy grandes, otros pequeños y otros de tamaño medio.&lt;/p&gt;

&lt;p&gt;En mucho de ellos he visto la implementacion de la capa de logica de negocios donde se defina una interfaz que contiene la definicion de las acciones de de ese servicio.&lt;/p&gt;

&lt;p&gt;Supongamos que tenemos una aplicacion que tiene la entidad cliente y en la capa donde vive nuestra logica de aplicacion tenemos una interfaz que define todas las acciones del cliente.&lt;/p&gt;

&lt;p&gt;Algo como esto.&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%2Fu1coxo9duijlrcchpzg7.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%2Fu1coxo9duijlrcchpzg7.png" alt="IClientServiceInterface" width="410" height="238"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Dentro de esta interfaz podemos agregar cuantos metodos necesitemos y entre mas metodos agregemos, la clase que implemente dicha interfaz sera un mounstroo de muchisimas lineas de codigo dificil de leer. y de mantener.&lt;/p&gt;

&lt;p&gt;Esto no esta mal perse, ya que tenemos una interfaz que despues podemos inyectar en la capa superior que seria la capa de presentacion, nos permite hacere referencia a una abstracion, nos permitira tener mas facilidad de hacer unit testing y en general seguimos principios solid.&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%2Fxg6tcybev61em7wshw1c.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%2Fxg6tcybev61em7wshw1c.png" alt="Client Controller with IClientService injected" width="529" height="466"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Pero esto tambien es mejorable y aqui es donde viene una propuesta de solucion.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solucion.
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Partiendo la interfaz
&lt;/h3&gt;

&lt;p&gt;En la imagen anterior, la interfaz IClient implementa el principio de Single Resposanbility. &lt;br&gt;
Tenemos una interfaz que tiene muchos metodos de la logica de aplicacion pero aun podemos tener mayor granularidad.&lt;br&gt;
En lugar de tener una unica interfaz, podemos separarla en partes y generar una clase para cada accion, es decir, una clase por cada metodo definido en la interfaz.&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%2F8rxjqt1qyh12vgne5a4a.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%2F8rxjqt1qyh12vgne5a4a.png" alt="interfaces separeted" width="655" height="825"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Cada una de estas clases hacen referencia a un case de uso enconcreto de nuestra capa de negocios y estan perfectamente separadas.&lt;/p&gt;

&lt;p&gt;Ahora, estas clases no implementan ninguna interfaz. Lo cual no esta mal por que aun asi podemos seguid inyectando cada una de estas clases en .NET pero no es lo ideal ya que estariamos dependiendo de una clase concreta y no de una abstraccion.&lt;/p&gt;

&lt;h3&gt;
  
  
  Agregando IUseCase
&lt;/h3&gt;

&lt;p&gt;Para solucionarlo vamos a crear una interfaz que la nombraremos IUseCase y cada una de nuestras clases podras implementar esta interfaz.&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%2Fbs6v8if1b5ozgdef2la9.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%2Fbs6v8if1b5ozgdef2la9.png" alt="IUseCase" width="656" height="299"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ahora si, nuestro caso de uso ya puede ser inyectado de mejor manera en nuestro controlador y obtener el cliente por Id.&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%2Faiikgdwx8r53od1hcga3.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%2Faiikgdwx8r53od1hcga3.png" alt="IUseCase In controller" width="419" height="339"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Convirtiendo el IUseCase en generico.
&lt;/h3&gt;

&lt;p&gt;Bien, ahora con este diseño nos enfretamos a otro "problema". Y es que tenemos que crer una interfaz por cada caso de uso, definir los parametros que recibe y el tipo de la respuesta de cada metodo, ademas de eso el nombre que tendra el metodo que ejecutara la logica de aplicacion podria tener el nombre que sea.&lt;/p&gt;

&lt;p&gt;Para solucionar estos inconvenientes haremos uso de los Generics de C# y vamos a convertir a nuestra interfac IUseCase en una interfaz generica que tenga como entrada un tipo y que tenga una salida.&lt;/p&gt;

&lt;p&gt;Ademsa de todo esto, tambien vamos a definir otra interfaz IRequest que servira para marcar a los datos de entrada de nuesto Caso de uso.&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%2F4q96j64iyc1qqsnsgqxr.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%2F4q96j64iyc1qqsnsgqxr.png" alt="Image description" width="410" height="177"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Con este caso de uso generico ya podemos estandarizar el nombre del metodo que ejecutara la logica de negocio en cada caso de uso, ademas, con la interfaz IRequest definimos que cada caso de uso tiene una entrada y genera una salida.&lt;/p&gt;

&lt;p&gt;La implementacion de el caso de uso de obtener un cliente por Id quedaria de la siguiente manera:&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%2Flp065rf95hqp4xz319ig.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%2Flp065rf95hqp4xz319ig.png" alt="Image description" width="654" height="331"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Con esto, no necesitamos crear una interfaz para cada caso de uso, ahora estamos reutilizando una unica interfaz generica que nos ayudara a implementar la logica de negocio.&lt;/p&gt;

&lt;p&gt;Y para hacer uso del case de uso tendriams que inyectar la interface IUseCase de la siguiente manera:&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%2F6ewtmy5nocakxqcv68rq.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%2F6ewtmy5nocakxqcv68rq.png" alt="Image description" width="583" height="336"&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Hasta aqui tenemos una separacion de la logica de negocios de nuestro controller y ademas cada caso de uso esta en una clase separada lo que hace que queca caso de uso tenga una resposabilidad unica.&lt;/p&gt;

&lt;p&gt;Seguimos manteniendo una inversion de control mediante la inyeccion de dependencias y segumos mantiendo la testeabilidad de la capa de negocios.&lt;/p&gt;

&lt;h2&gt;
  
  
  Despachador de Casos de uso.
&lt;/h2&gt;

&lt;p&gt;Ok, considero que la implementacinon hasta este punto esta bien.&lt;/p&gt;

&lt;p&gt;Si la logica de negocio empieza a crecer en numero de casos de uso, entonces nos tocara injectar un monton de estos en el contolador, no es un lio pero quiza el codigo no se vea tan "limpio" con eso.&lt;/p&gt;

&lt;p&gt;Entonces, que tal que agregamos un despachador de casos de uso, es decir, una capa extra que internamente resuelva cual es el caso de uso que tiene que utilizar dependiendo de el request y el response:&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%2Fwe5wc8rgvpv063wgvs52.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%2Fwe5wc8rgvpv063wgvs52.png" alt="dispatcher" width="800" height="785"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Esta seria la implementacion de nuestro use case dispatcher.&lt;/p&gt;

&lt;p&gt;Internamente hace uso del Service Provider de .net para poder entregarnos el caso de uso correcto.&lt;/p&gt;

&lt;p&gt;Ademas, podemos loguear errores cuando se ejecute el metodo ExecuteAsync de cada caso de uso.&lt;/p&gt;

&lt;p&gt;Y la implementacion en cada Controller seria algo asi:&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%2Fxzdlczchqknfmubba6ea.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%2Fxzdlczchqknfmubba6ea.png" alt="dispatcher in controller" width="693" height="352"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
