Patrones de diseño de .NET

Aprende a reconocer y aplicar patrones de diseño en .NET y ASP.NET Core con ejemplos, trade-offs y criterios prácticos.

Patrones de diseño de .NET

El TL;DR está justo debajo, pero vale la pena leer con calma si estás a cargo de decisiones técnicas o influyes en ellas dentro de .NET.

Los patrones de diseño en .NET son casi como un idioma propio de la plataforma. En ASP.NET Core, Entity Framework, bibliotecas de terceros e incluso en tu propio código, los mismos patrones se repiten: factory, builder, decorator, strategy, repository, entre otros.

Este artículo sirve como guía práctica para líderes técnicos y desarrolladores experimentados que quieren entender profundamente cómo surgen los patrones de diseño de .NET en el día a día. Más que simplemente “aplicar el patrón X”, el objetivo es reconocer estos patrones en el framework y utilizar ese conocimiento para tomar mejores decisiones en arquitectura, revisión de código e integración con bases de código heredadas.

Abordaremos:

  • El problema que resuelve cada patrón.
  • Dónde aparece en .NET / ASP.NET Core.
  • Cuándo tiene sentido —o no— utilizarlo en tu código.
  • Trade-offs reales, sin eslóganes de los libros.

TL;DR

Para quienes tienen prisa:

  • .NET y ASP.NET Core ya incluyen la mayoría de los patrones clásicos del GoF: Factory, Builder, Singleton (mediante DI), Adapter, Decorator, Facade, Observer, Strategy, Command, Template Method, Repository y Unit of Work.
  • En lugar de “forzar el patrón” en tu código, el mayor beneficio está en reconocer los patrones que ya existen, entender los problemas que resuelven y utilizarlos como vocabulario común en el equipo.
  • Para los líderes técnicos, esto afecta a:
  • Arquitectura: saber dónde utilizar DI, dónde encapsular integraciones y dónde aplicar CQRS.
  • Revisión de código: cuestionar decisiones basándose en problemas reales (acoplamiento, capacidad de prueba y claridad), en vez de decir que “faltó utilizar el patrón X”.
  • Onboarding: explicar el código heredado diciendo “aquí hay un Decorator de logging” o “aquí hay un Adapter para el gateway de pagos”, en lugar de “esta clase gigante hace de todo”.
  • Cuidado con el overengineering: utilizar demasiados patrones, demasiado pronto, puede hacer que el código sea más confuso de lo necesario.

Si quieres más detalles, continúa a partir de la sección 1.


1. Por qué los patrones de diseño importan tanto en .NET hoy

Diagrama conceptual del ecosistema .NET: runtime, BCL, ASP.NET Core y código de la empresa, con los puntos donde aparecen los patrones.
Diagrama conceptual del ecosistema .NET: runtime, BCL, ASP.NET Core y código de la empresa, con los puntos donde aparecen los patrones.

En un ecosistema extenso como .NET, rara vez trabajas únicamente con “tu código”. En cualquier aplicación razonable tienes:

  • El runtime (CLR).
  • La BCL (biblioteca de clases base).
  • ASP.NET Core, ORMs, bibliotecas NuGet y SDKs de terceros.
  • Y el código de tu empresa, con sus capas, dominios e integraciones.

Todos esos bloques se construyen con patrones de diseño comunes, por lo que los patrones de diseño en .NET funcionan como un vocabulario compartido:

  • “Este middleware es un Decorator.”
  • “Esta interfaz es una Strategy de serialización.”
  • “Este DbContext funciona como Unit of Work + Repository.”

Cuando todos entienden este vocabulario, las discusiones de diseño se vuelven mucho más objetivas.

Utilizar un patrón frente a reconocer patrones en el framework

Existe una gran diferencia entre:

  • “Voy a aplicar el patrón X aquí”
    (a veces forzando un patrón donde bastaba con un if).

  • “Este método del framework implementa un Template Method”
    (y solo necesitas conectarte a un hook).

El objetivo de este artículo es el segundo: leer y diseñar código .NET/C# reconociendo los patrones que ya existen y solo después decidir si vale la pena reproducir ese estilo en tu código.

Si quieres reforzar al mismo tiempo los fundamentos de las buenas prácticas de .NET, un buen complemento es revisar buenas prácticas de código C# y .NET en materiales como este artículo sobre buenas prácticas en .NET y C#.

Cómo ayuda esto a los líderes técnicos

Para un tech lead, arquitecto o referente técnico, entender los patrones de diseño de .NET cambia la forma de actuar en:

  • Decisiones de arquitectura
  • ¿Vale la pena aislar este gateway de pagos con un Adapter?
  • ¿Este cross-cutting (logging/retry/cache) funciona mejor como Decorator o middleware?
  • ¿Un CQRS sin demasiadas ceremonias o un simple Command Handler resuelve el problema?

  • Revisiones de código más objetivas
    En lugar de “me gusta/no me gusta”, puedes discutir:

  • “Tu UserService se está convirtiendo en un God Object; quizá aquí el papel sea el de una Facade sobre repositorios e integraciones.”
  • “Creaste un Singleton manual con static, pero ya tenemos DI. ¿Qué tal si dejamos que el contenedor se encargue?”

  • Onboarding en bases de código heredadas
    Explicar “aquí tenemos un Observer de un evento de dominio; allí tenemos una Strategy de cálculo de precios” es mucho más eficaz que decir “aquí hay una magia que llama a esto y aquello”.

[TRADE-OFF] Overengineering: cuándo los patrones ayudan y cuándo complican

Los patrones son herramientas, no objetivos. Algunos problemas habituales:

  • Aplicar un patrón demasiado pronto: “por si algún día necesitamos cinco estrategias más” → hoy tienes una sola y siete interfaces que mantener.
  • Patrones en cascada: cada capa utiliza tres patrones diferentes y el flujo se convierte en un laberinto de factories, facades y decorators.

Regla práctica:

Empieza de forma sencilla. Utiliza patrones cuando el problema aparezca con claridad (variedad de comportamientos, múltiples integraciones, responsabilidades transversales, etc.). Antes de eso, el código directo suele ser mejor.


2. Patrones creacionales en .NET: más allá de new

2.1 Factory Method y Abstract Factory en el día a día de C

Los patrones creacionales responden a la pregunta: “¿cómo instanciar esto sin acoplar a todo el mundo a new y a los detalles de configuración?”.

ASP.NET Core Options: fábricas de configuración

El patrón Options de ASP.NET Core es un buen ejemplo de “fábrica de objetos” orientada a la configuración.

csharp public class MyOptions { public string Endpoint { get; set; } = ""; public int TimeoutSeconds { get; set; } = 30; }

// Program.cs / Startup.cs builder.Services.Configure<MyOptions>(builder.Configuration.GetSection("MyService"));

// En cualquier servicio: public class MyService { private readonly HttpClient _client;

public MyService(HttpClient client, IOptions<MyOptions> options)
{
    _client = client;
    _client.BaseAddress = new Uri(options.Value.Endpoint);
    _client.Timeout = TimeSpan.FromSeconds(options.Value.TimeoutSeconds);
}

}

IOptions<MyOptions> funciona como una especie de Factory Method configurable:

  • No instancias MyOptions directamente.
  • El contenedor sabe cómo construir la instancia (a partir de la configuración).
  • Cuando cambias el binding, no necesitas reescribir a quienes consumen MyOptions.

Si utilizas IOptionsMonitor<>, obtienes una “fábrica reactiva”: cada vez que cambia la configuración, el “producto” cambiará en el siguiente acceso.

DbProviderFactory y Abstract Factory

ADO.NET incluye un ejemplo clásico de Abstract Factory con DbProviderFactory:

csharp DbProviderFactory factory = DbProviderFactories.GetFactory("System.Data.SqlClient");

using var connection = factory.CreateConnection(); connection.ConnectionString = connectionString;

using var command = factory.CreateCommand(); command.Connection = connection; command.CommandText = "SELECT * FROM Users";

Aquí, DbProviderFactory es una fábrica abstracta que sabe crear una familia de objetos relacionados:

  • DbConnection, DbCommand, DbDataAdapter, etc.
  • La implementación concreta (SQL Server, MySQL, etc.) queda oculta detrás de la factory.

Cuándo crear tus propias factories

Rara vez necesitas una Abstract Factory “formal” en una aplicación, pero vale la pena utilizar el patrón cuando:

  • Tienes bounded contexts que cambian el “tipo” de las dependencias:
  • Por ejemplo, en el vertical de “Pagos”, cada método (tarjeta, transferencia o PIX) necesita clientes y configuraciones diferentes.
  • Necesitas plugins de integración:
  • Gateways de pagos, servicios antifraude o proveedores diversos de email/SMS.

Un esqueleto sencillo:

csharp public interface IPaymentGateway { Task<PaymentResult> ChargeAsync(PaymentRequest request); }

public interface IPaymentGatewayFactory { IPaymentGateway Create(string method); }

public class PaymentGatewayFactory : IPaymentGatewayFactory { private readonly IServiceProvider _provider;

public PaymentGatewayFactory(IServiceProvider provider)
{
    _provider = provider;
}

public IPaymentGateway Create(string method)
{
    return method switch
    {
        "credit-card" => _provider.GetRequiredService<CreditCardGateway>(),
        "pix"         => _provider.GetRequiredService<PixGateway>(),
        _             => throw new NotSupportedException($"Payment method {method} not supported")
    };
}

}

[PITFALL] Factories que se convierten en God Objects

El peligro clásico:

  • La factory empieza solo eligiendo qué implementación utilizar.
  • Poco a poco comienza a:
  • Validar inputs.
  • Hacer logging.
  • Llamar a integraciones.
  • Disparar eventos.

Cuando te das cuenta, la “factory” se ha convertido en un servicio gigante que mezcla la creación de objetos con las reglas de negocio. Es una señal de que debes separar responsabilidades:

  • Una clase muy pequeña para elegir/crear (la propia factory).
  • Otras clases para las reglas de negocio y la orquestación.

2.2 Builder: leer y escribir código fluido en C

La mayoría de las APIs fluidas en C# son variaciones del patrón Builder: vas componiendo llamadas que acumulan estado hasta “finalizar” la construcción o configuración de algo.

Ejemplos: HttpClient, IServiceCollection, IApplicationBuilder

ASP.NET Core es Builder de principio a fin:

csharp var builder = WebApplication.CreateBuilder(args);

builder.Services .AddControllers() .AddJsonOptions(o => { o.JsonSerializerOptions.PropertyNamingPolicy = null; });

builder.Services.AddHttpClient("GitHub", client => { client.BaseAddress = new Uri("https://api.github.com/"); client.DefaultRequestHeaders.UserAgent.ParseAdd("MyApp"); });

var app = builder.Build();

app.UseRouting(); app.UseAuthentication(); app.UseAuthorization();

app.MapControllers();

app.Run();

Aquí:

  • WebApplicationBuilder es un Builder de la aplicación.
  • El pipeline app.UseXxx() es un Builder de middlewares.
  • IServiceCollection construye el grafo de dependencias mediante una API fluida.

Como líder, esto es útil porque:

  • Las APIs internas de tu empresa, utilizadas por varios squads, pueden seguir el mismo estilo:
  • services.AddBusinessModuleX();
  • builder.AddTenantSupport().AddMultiRegion();
  • Builder evita constructores gigantes con diez parámetros opcionales; en su lugar, tienes una API de configuración paso a paso.

[TRADE-OFF] Builder frente a múltiples constructores y objetos de request

  • Múltiples constructores:
  • Son buenos para tipos sencillos, con pocas variaciones.
  • Se vuelven problemáticos cuando hay muchas combinaciones de parámetros opcionales.

  • Objeto de request:

  • Funciona bien cuando tienes un “comando” claro.
  • Puede convertirse en un conjunto de propiedades sin una expresión clara si no lo cuidas.

  • Builder:

  • Destaca cuando:
    • Hay muchos parámetros configurables.
    • Quieres una API expresiva que refleje el flujo de configuración.
  • Coste:
    • Más tipos y código de conexión.
    • Puede ocultar dependencias implícitas (por ejemplo, el orden de las llamadas importa).

Regla pragmática:

Si el uso de tu tipo se está volviendo confuso (“¿qué constructor tenía que utilizar?”), considera un Builder o una API fluida.


2.3 Singleton bien hecho (y mal hecho) en .NET moderno

En el .NET moderno, rara vez deberías implementar el Singleton clásico con static. Casi siempre conviene dejar que el contenedor de DI gestione el ciclo de vida.

Servicio singleton en ASP.NET Core frente al patrón clásico

csharp builder.Services.AddSingleton<ISystemClock, SystemClock>();

Aquí:

  • ISystemClock tendrá una única instancia por contenedor (normalmente, por aplicación).
  • Quien la necesite la solicita en el constructor; no necesitas un GetInstance() estático.

Esto resuelve:

  • Concurrencia (thread-safe, controlada por el contenedor).
  • Pruebas (puedes cambiar la implementación en la configuración de DI).
  • Orden de inicialización (el contenedor lo controla).

[PITFALL] Crear un Singleton manual con static

csharp public class ConfigManager { private static readonly ConfigManager _instance = new(); public static ConfigManager Instance => _instance;

private ConfigManager() { /* carga la configuración */ }

public string GetValue(string key) { ... }

}

Problemas:

  • Dificulta las pruebas (no puedes sustituir ConfigManager).
  • Introduce dependencias globales (acoplamiento oculto).
  • Puede crear problemas de orden de inicialización si hay elementos static complejos.

En la mayoría de los casos, prefiere:

csharp public interface IConfigManager { string GetValue(string key); }

public class ConfigManager : IConfigManager { // ... }

// Program.cs builder.Services.AddSingleton<IConfigManager, ConfigManager>();

Uso aceptable de singletons

Buen uso del lifetime singleton mediante DI:

  • Configuraciones inmutables: tipos de options cargados una sola vez.
  • Clientes compartidos:
  • HttpClient mediante IHttpClientFactory.
  • Clientes de colas, caches y otros recursos costosos.

3. Patrones estructurales que encuentras en todo código .NET

Diagrama por capas de una aplicación .NET, con el dominio, la infraestructura y las integraciones externas, y los patrones destacados.
Diagrama por capas de una aplicación .NET, con el dominio, la infraestructura y las integraciones externas, y los patrones destacados.

3.1 Adapter: hacer compatibles las APIs en las integraciones

Adapter es el patrón de “hablar con el mundo externo sin ensuciar tu dominio”. Expones una interfaz que tiene sentido para ti y adaptas el SDK externo a esa interfaz.

Ejemplo en la BCL: Stream y wrappers

Stream es una abstracción. Implementaciones como CryptoStream y GZipStream adaptan un stream base para añadir comportamiento:

csharp using var file = File.OpenRead("data.txt"); using var gzip = new GZipStream(file, CompressionMode.Decompress); using var reader = new StreamReader(gzip);

string content = reader.ReadToEnd();

  • GZipStream adapta un Stream para gestionar la compresión.
  • La API pública sigue siendo “un Stream”.

Integraciones habituales: SDKs de proveedores

Con DDD / Ports & Adapters:

  1. Defines un puerto (interfaz) alineado con el dominio.
  2. Creas Adapters para cada proveedor.

csharp public interface IPaymentProvider { Task<PaymentResult> ChargeAsync(PaymentRequest request); }

public class AcmePaymentAdapter : IPaymentProvider { private readonly AcmeSdkClient _client;

public AcmePaymentAdapter(AcmeSdkClient client)
{
    _client = client;
}

public async Task<PaymentResult> ChargeAsync(PaymentRequest request)
{
    var acmeRequest = new AcmeChargeRequest
    {
        Amount = request.Amount,
        CardToken = request.CardToken
    };

    var response = await _client.ChargeAsync(acmeRequest);

    return new PaymentResult
    {
        Success = response.Status == "OK",
        TransactionId = response.Id
    };
}

}

[PITFALL] Llevar tipos del proveedor al dominio

Un error habitual:

  • Los modelos del SDK (AcmeChargeRequest, AcmeChargeResponse) aparecen en el dominio, en las entidades y en los servicios de negocio.

Consecuencias:

  • El dominio queda acoplado a un proveedor específico.
  • Cambiar de proveedor se convierte en una cirugía mayor.

Regla: el SDK externo no entra en el dominio. Utiliza un Adapter como zona de contención.


3.2 Decorator: middleware, pipelines y cross-cutting concerns

Flujo horizontal de una petición HTTP que atraviesa varios middlewares, cada uno de los cuales añade comportamiento.
Flujo horizontal de una petición HTTP que atraviesa varios middlewares, cada uno de los cuales añade comportamiento.

Decorator es el patrón de “envolver” un servicio con otro para añadir comportamiento.

ASP.NET Core Middleware como Decorator en cadena

El pipeline app.UseXxx() es literalmente una cadena de Decorators:

csharp app.Use(async (context, next) => { // Antes await next(); // Después });

Cada middleware:

  • Recibe un RequestDelegate (next).
  • Hace algo antes o después de llamar al siguiente.

Esto es Decorator puro: un objeto que implementa la misma “firma” y añade comportamiento alrededor del otro.

DelegatingHandler en HttpClient

Otro ejemplo directo:

csharp public class LoggingHandler : DelegatingHandler { protected override async Task<HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken ct) { Console.WriteLine($"Request: {request.Method} {request.RequestUri}");

    var response = await base.SendAsync(request, ct);

    Console.WriteLine($"Response: {response.StatusCode}");

    return response;
}

}

Puedes encadenar varios DelegatingHandler mediante IHttpClientFactory, formando un pipeline de Decorators.

[EJEMPLO] Decorar un servicio de dominio con logging

csharp public interface IOrderService { Task<Order> GetByIdAsync(Guid id); }

public class OrderService : IOrderService { public Task<Order> GetByIdAsync(Guid id) { // Buscar en el repositorio, aplicar reglas de negocio, etc. } }

public class LoggingOrderServiceDecorator : IOrderService { private readonly IOrderService _inner; private readonly ILogger<LoggingOrderServiceDecorator> _logger;

public LoggingOrderServiceDecorator(
    IOrderService inner,
    ILogger<LoggingOrderServiceDecorator> logger)
{
    _inner = inner;
    _logger = logger;
}

public async Task<Order> GetByIdAsync(Guid id)
{
    _logger.LogInformation("Fetching order {OrderId}", id);
    var order = await _inner.GetByIdAsync(id);
    _logger.LogInformation("Fetched order {OrderId}", id);
    return order;
}

}

En contenedores que soportan Decorator, registrarías algo como:

csharp builder.Services.AddScoped<IOrderService, OrderService>(); // builder.Services.Decorate<IOrderService, LoggingOrderServiceDecorator>();

[TRADE-OFF] Decorator frente a AOP y filtros
  • Decorator:
  • Funciona bien cuando controlas DI y los contratos.
  • Es transparente y explícito (los decoradores se ven en la composición).

  • AOP (Aspect-Oriented Programming):

  • Puede ser potente para cross-cutting (log, auditoría y retry).
  • Con frecuencia es “mágico” y difícil de rastrear durante el debug.

  • Filtros MVC / filtros de pipeline:

  • Son buenos para preocupaciones relacionadas con HTTP (autorización, validación y formateo de respuestas).
  • Son menos adecuados para lógica puramente de dominio.

Regla pragmática:

Si el cross-cutting trata sobre peticiones HTTP, utiliza middleware o filtros.
Si trata sobre servicios de dominio, prefiere Decorator.


3.3 Facade: estabilizar fronteras entre contextos

Facade es el patrón de una “puerta de entrada sencilla a un subsistema complejo”.

“Services” y “Managers” como Facade

No todo UserService es un anti-pattern. Algunos realmente:

  • Orquestan varios repositorios.
  • Llaman a integraciones.
  • Coordinan transacciones.

Ejemplo:

csharp public class CheckoutFacade { private readonly ICartRepository _cartRepository; private readonly IPaymentProvider _paymentProvider; private readonly IOrderRepository _orderRepository;

public CheckoutFacade(
    ICartRepository cartRepository,
    IPaymentProvider paymentProvider,
    IOrderRepository orderRepository)
{
    _cartRepository = cartRepository;
    _paymentProvider = paymentProvider;
    _orderRepository = orderRepository;
}

public async Task<CheckoutResult> CheckoutAsync(Guid cartId)
{
    var cart = await _cartRepository.GetByIdAsync(cartId);

    var paymentResult = await _paymentProvider.ChargeAsync(new PaymentRequest
    {
        Amount = cart.Total
    });

    if (!paymentResult.Success)
        return CheckoutResult.Failed("Payment failed");

    var order = Order.CreateFromCart(cart, paymentResult.TransactionId);

    await _orderRepository.SaveAsync(order);

    return CheckoutResult.Success(order.Id);
}

}

La aplicación web se comunica solo con la Facade, sin tener que conocer todos los detalles internos.

System.IO.File como Facade

System.IO.File encapsula varias APIs de IO:

csharp var text = File.ReadAllText("file.txt"); File.WriteAllText("file.txt", "content");

Es una Facade sobre:

  • Streams.
  • Buffers.
  • Acceso al sistema de archivos.
[PITFALL] Un servicio gigante que no es una Facade, sino falta de diseño

Señales de alerta:

  • UserService con miles de líneas.
  • Métodos sin una relación clara: CreateUser, ExportToCsv, SendWelcomeEmail, RebuildIndexes
  • Mezcla de:
  • Reglas de negocio.
  • Infraestructura.
  • Detalles de UI.

En esos casos:

  • Divide en:
  • Facades más pequeñas por caso de uso (por ejemplo, UserRegistrationFacade).
  • Servicios de dominio enfocados.
  • Adapters para las integraciones.

4. Patrones de comportamiento en .NET: eventos, comandos y pipelines

4.1 Observer y el modelo de eventos de .NET

Observer es el patrón de “alguien cambia y varios observadores reciben una notificación”.

Eventos (event, EventHandler)

csharp public class Stock { private int _quantity; public event EventHandler<int>? QuantityChanged;

public void Change(int value)
{
    _quantity += value;
    QuantityChanged?.Invoke(this, _quantity);
}

}

Quien esté interesado en el evento se registra:

csharp var stock = new Stock(); stock.QuantityChanged += (sender, newQuantity) => { Console.WriteLine($"New quantity: {newQuantity}"); };

Esto es Observer clásico: Stock es el Subject y los listeners son los Observers.

INotifyPropertyChanged en WPF/MAUI

En las UIs .NET, INotifyPropertyChanged es una implementación estandarizada de Observer:

csharp public class PersonViewModel : INotifyPropertyChanged { private string _name = "";

public string Name
{
    get => _name;
    set
    {
        if (_name == value) return;
        _name = value;
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
    }
}

public event PropertyChangedEventHandler? PropertyChanged;

}

La UI observa el ViewModel y actualiza la pantalla cuando cambian las propiedades.

[PITFALL] Memory leaks con eventos

En eventos basados en delegates:

  • Si el “observer” se suscribe a un objeto de larga duración y no se desuscribe, el GC no puede recolectar el observer, lo que provoca una fuga de memoria.

Regla: en objetos de larga duración (singletons y servicios globales), planifica siempre el unsubscribe.

Eventos frente a mensajes asíncronos

  • Eventos .NET (Observer):
  • En memoria y síncronos de forma predeterminada.
  • Buenos para la coordinación local dentro del proceso.

  • Mensajes asíncronos (cola, bus):

  • Entre procesos y resistentes a fallos.
  • Buenos para integraciones entre servicios y workflows largos.

Trade-off:

Si la reacción al evento es local y rápida, utiliza Observer.
Si ocurre entre servicios o necesita sobrevivir a la caída de procesos, utiliza mensajería.


4.2 Strategy: configurar el comportamiento mediante DI

Comparación entre un bloque de código con muchos if/else y varias estrategias conectables.
Comparación entre un bloque de código con muchos if/else y varias estrategias conectables.

Strategy es el patrón de “intercambiar el algoritmo mediante composición”.

Interfaces y políticas inyectadas

ASP.NET Core utiliza Strategy en varios puntos:

  • IPasswordHasher<> en Identity.
  • IOutputFormatter en MVC.
  • Formas de cache, cifrado y serialización, entre otras.

Ejemplo simplificado:

csharp public interface IPriceStrategy { decimal Calculate(decimal basePrice); }

public class DefaultPriceStrategy : IPriceStrategy { public decimal Calculate(decimal basePrice) => basePrice; }

public class BlackFridayPriceStrategy : IPriceStrategy { public decimal Calculate(decimal basePrice) => basePrice * 0.7m; }

public class PricingService { private readonly IPriceStrategy _strategy;

public PricingService(IPriceStrategy strategy)
{
    _strategy = strategy;
}

public decimal GetFinalPrice(decimal basePrice)
    => _strategy.Calculate(basePrice);

}

Registro:

csharp builder.Services.AddScoped<IPriceStrategy, DefaultPriceStrategy>(); // en otro entorno o escenario, sustituir por BlackFridayPriceStrategy

Cambiar el comportamiento mediante configuración

Puedes decidir la Strategy mediante:

  • Feature flags.
  • Región o tenant.
  • Tipo de cliente.

csharp public class TenantPriceStrategySelector : IPriceStrategy { private readonly IHttpContextAccessor _httpContextAccessor; private readonly IPriceStrategy _default; private readonly IPriceStrategy _premium;

public TenantPriceStrategySelector(
    IHttpContextAccessor httpContextAccessor,
    DefaultPriceStrategy @default,
    PremiumPriceStrategy premium)
{
    _httpContextAccessor = httpContextAccessor;
    _default = @default;
    _premium = premium;
}

public decimal Calculate(decimal basePrice)
{
    var tenant = _httpContextAccessor.HttpContext?.Request.Headers["X-Tenant"];

    return tenant == "premium"
        ? _premium.Calculate(basePrice)
        : _default.Calculate(basePrice);
}

}

[TRADE-OFF] Strategy frente a if-else extensos
  • Pocas variaciones y poco acoplamiento: un switch directo lo resuelve y es más sencillo.
  • Cuando las reglas crecen, se dispersan por el código y cada nueva variación exige modificar muchos if/else, es hora de utilizar Strategy.
[PITFALL] Demasiadas Strategies

Señal de overdesign:

  • Creas una Strategy para todo:
  • ILoggingStrategy, ISaveStrategy, IEmailStrategy
  • El flujo de negocio queda fragmentado en decenas de clases.

Utiliza Strategy cuando exista una variación real de comportamiento que quieras cambiar sin reescribir al cliente.


4.3 Command, CQRS y pipelines en .NET

Command es el patrón de representar una intención como un objeto.

Commands como objetos de intención

csharp public record CreateOrderCommand(Guid CustomerId, IReadOnlyList<Guid> Items);

public interface ICommandHandler<TCommand> { Task HandleAsync(TCommand command, CancellationToken cancellationToken = default); }

public class CreateOrderCommandHandler : ICommandHandler<CreateOrderCommand> { public Task HandleAsync(CreateOrderCommand command, CancellationToken cancellationToken = default) { // crear el pedido, guardarlo en el repositorio, etc. } }

Obtienes:

  • Separación clara entre la intención (“crear pedido”) y la ejecución.
  • Un punto único para validar, autorizar y procesar.

Patrón Command en workers y colas

Puedes encontrar Command en:

  • APIs de UI (botones que disparan comandos).
  • Colas de trabajo: cada job es un “comando” que debe ejecutar un worker.

csharp public class ProcessOrdersBackgroundService : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { var command = await DequeueAsync(stoppingToken); await HandleCommandAsync(command, stoppingToken); } }

private Task<CreateOrderCommand> DequeueAsync(CancellationToken ct)
{
    // leer de la cola, deserializar, etc.
}

private Task HandleCommandAsync(CreateOrderCommand command, CancellationToken ct)
{
    // delegar en ICommandHandler<CreateOrderCommand>
}

}

Cada mensaje de la cola es un Command.

CQRS en escenarios de alta complejidad

CQRS (Command and Query Responsibility Segregation) es una evolución natural:

  • Commands: modifican el estado y no devuelven datos complejos.
  • Queries: solo leen y no tienen efectos colaterales.

Puedes implementarlo de forma ligera:

csharp public interface IQuery<TResult> { }

public interface IQueryHandler<TQuery, TResult> where TQuery : IQuery<TResult> { Task<TResult> HandleAsync(TQuery query, CancellationToken cancellationToken = default); }

[TRADE-OFF] CQRS completo frente a un “CQRS con sentido común”
  • CQRS completo (con event sourcing, read models separados, etc.) tiene sentido cuando:
  • Tienes alta complejidad de lectura y escritura.
  • Existen requisitos fuertes de auditoría, histórico o reconstrucción del estado.

  • CQRS con sentido común:

  • Separar handlers de comandos y consultas.
  • Evitar métodos que hagan todo (lectura, escritura y side effects) al mismo tiempo.

Regla:

Empieza con un CQRS ligero (comandos y queries separados).
Adopta event sourcing y modelos separados solo cuando el problema lo justifique, no porque “esté en el libro”.


5. Patrones específicos del ecosistema .NET (implícitos, pero reales)

5.1 Dependency Injection como patrón de primera clase

DI, como patrón, es central en ASP.NET Core.

Contenedor de DI e IServiceProvider como Service Locator “oculto”

csharp var builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<IUserRepository, UserRepository>(); builder.Services.AddScoped<IUserService, UserService>();

var app = builder.Build();

  • El Composition Root es Program.cs: allí defines el grafo de objetos.
  • El framework inyecta lo que necesitas en los constructores de controllers, services y handlers.

IServiceProvider es, técnicamente, un Service Locator. Sin embargo:

  • Está confinado al runtime.
  • En la mayoría de los casos, no necesitas llamarlo directamente.
[PITFALL] Dispersar IServiceProvider.GetService por el código

csharp public class SomeClass { private readonly IServiceProvider _provider;

public SomeClass(IServiceProvider provider)
{
    _provider = provider;
}

public void DoSomething()
{
    var repo = _provider.GetService<IUserRepository>();
    // ...
}

}

Esto se convierte en:

  • Un Service Locator explícito.
  • Dependencias ocultas (nadie sabe que SomeClass necesita IUserRepository).
  • Pruebas más difíciles.

Regla: prefiere la inyección mediante constructor; utiliza IServiceProvider directamente solo en puntos muy específicos (factories, integración con APIs heredadas, etc.).

[TRADE-OFF] DI minimalista frente a frameworks de DI con muchas funcionalidades

  • DI minimalista de ASP.NET Core:
  • Cubre la mayoría de los casos sin complicaciones.
  • No incluye todos los recursos (auto-decorators, child containers avanzados).

  • Frameworks de DI más complejos:

  • Pueden ofrecer funcionalidades útiles (escaneo automático, decorators e interception).
  • Añaden complejidad de configuración y debug.

Como líder, piensa siempre en el coste-beneficio:

  • ¿El equipo entiende las funcionalidades adicionales?
  • ¿La ganancia real compensa la curva de aprendizaje y el coste de mantenimiento?

5.2 Template Method en frameworks basados en herencia

Template Method es el patrón de “la clase base controla el flujo y las subclases completan los detalles”.

Controllers, handlers y frameworks de UI

Puedes ver Template Method en:

  • Controllers MVC:
  • El framework controla el pipeline de la request; tú implementas acciones y, a veces, métodos como OnActionExecuting y OnActionExecuted.
  • Handlers:
  • Métodos ExecuteAsync y HandleAsync que llama la infraestructura.
  • UI (WPF, MAUI):
  • Métodos OnLoad, OnAppearing y OnInitialized.

Pseudocódigo:

csharp public abstract class BackgroundJob { public async Task RunAsync() { await BeforeRunAsync(); await ExecuteAsync(); await AfterRunAsync(); }

protected virtual Task BeforeRunAsync() => Task.CompletedTask;
protected abstract Task ExecuteAsync();
protected virtual Task AfterRunAsync() => Task.CompletedTask;

}

El “template” (RunAsync) es fijo; las subclases solo implementan los “hooks”.

[TRADE-OFF] Template Method frente a Strategy con composición

  • Template Method:
  • Es sencillo de entender (herencia y override).
  • Pero ata la jerarquía: solo puedes heredar de una clase base.

  • Strategy + composición:

  • Es más flexible (puedes combinar estrategias diferentes).
  • Implica más tipos e indirecciones.

Como regla orientativa:

Si el framework ya define el flujo y te ofrece hooks, utiliza Template Method sin reparos.
Si estás diseñando algo nuevo y quieres componer comportamientos libremente, prefiere Strategy mediante DI.


5.3 Repository, Unit of Work y “lo peor y lo mejor” de DDD en C

Repository y Unit of Work aparecen por todas partes en .NET, sobre todo mediante ORMs.

ORMs que combinan Repository + Unit of Work

Un DbContext típico funciona como:

  • Unit of Work:
  • Rastrea cambios.
  • Ejecuta SaveChanges() en lote.

  • Repository genérico:

  • Expone colecciones (DbSet<T>) para CRUD.

csharp public class OrdersController : ControllerBase { private readonly AppDbContext _db;

public OrdersController(AppDbContext db)
{
    _db = db;
}

[HttpPost]
public async Task<IActionResult> Create([FromBody] CreateOrderDto dto)
{
    var order = new Order(...);

    _db.Orders.Add(order);
    await _db.SaveChangesAsync();

    return CreatedAtAction(nameof(GetById), new { id = order.Id }, order);
}

}

En escenarios sencillos, esto es suficiente.

Cuándo tiene sentido crear repositorios explícitos

Crea repositorios explícitos cuando quieras:

  • Proteger el dominio de los detalles de persistencia.
  • Expresar consultas de alto nivel alineadas con el negocio.

csharp public interface IOrderRepository { Task<Order?> GetByIdAsync(Guid id); Task<IReadOnlyList\<Order>> GetPendingOrdersAsync(); Task AddAsync(Order order); Task SaveChangesAsync(); }

En su interior utilizas DbContext, pero el dominio solo conoce la interfaz.

[PITFALL] Repositorio anémico que solo reenvía llamadas

csharp public class OrderRepository : IOrderRepository { private readonly AppDbContext _db;

public Task<Order?> GetByIdAsync(Guid id) 
    => _db.Orders.FindAsync(id).AsTask();

}

Si el repositorio solo delega llamadas, sin aportar nada más:

  • Ganas muy poco aparte de código adicional.
  • La complejidad no está justificada.

Idealmente, los repositorios añaden:

  • Consultas específicas.
  • Semántica de dominio (“pedidos vencidos”, “pedidos pendientes”).
  • Encapsulación de los detalles de mapeo.

Orientaciones para el liderazgo

  • En contextos sencillos, utilizar directamente DbContext en handlers o controllers es aceptable y práctico.
  • En dominios complejos:
  • Utiliza repositorios explícitos para obtener claridad y capacidad de prueba.
  • Trata DbContext como un detalle de infraestructura.
  • Evita el DDD “ceremonial”: más capas sin un beneficio claro solo dificultan el trabajo del equipo.

6. Cómo liderar equipos para utilizar patrones de diseño .NET con madurez

Del “nombre del patrón” al “problema que resuelve”

Los patrones son atajos para conversar, no medallas. En lugar de:

  • “Vamos a utilizar Decorator aquí porque es bonito.”

Prefiere:

  • “Tenemos varios cross-cuttings en este servicio (log, retry y cache). Decorator nos ayuda a componerlos de forma concisa.”

Una heurística útil es mapear:

  • Problema → posibles patrones.

Ejemplos rápidos:

  • Muchas variaciones de comportamiento → Strategy.
  • Cross-cutting alrededor de un servicio → Decorator / middleware.
  • Integración con un proveedor externo → Adapter + Facade.
  • Complejidad de configuración o instanciación → Builder / Factory.
  • Flujo controlado por el framework → Template Method.

Prácticas para tech leads

Revisiones de código centradas en la claridad de la intención

En una revisión de código, en lugar de decir “faltó utilizar el patrón X”, lleva la conversación al problema:

  • “Hoy tenemos cinco if/else diferentes para calcular el precio. ¿Qué tal si aislamos eso en una Strategy configurable?”
  • “¿Este UserService está orquestando subsistemas (Facade) o simplemente acumulando responsabilidades?”

Las preguntas directas ayudan:

  • “¿Qué es difícil de probar aquí?”
  • “¿Qué sería más fácil de cambiar si lo aislamos en un patrón?”

Sesiones de diseño comparando alternativas

Antes de decidirte por un patrón, pon sobre la mesa:

  • Una solución sin un patrón dedicado (if/else o un método sencillo).
  • Uno o dos patrones candidatos.
  • El impacto en:
  • Complejidad de lectura.
  • Pruebas.
  • Extensibilidad real (no hipotética).

Esto orienta la discusión hacia “qué problema estamos resolviendo” en lugar de “qué patrón es más bonito”.

[TRADE-OFF] Estandarizar demasiado frente a permitir una variación guiada

Guidelines estrictas como:

  • “Utiliza siempre Repository, incluso para una pantalla CRUD sencilla.”
  • “Toda regla de negocio debe estar en una Strategy.”

Tienden a crear arquitecturas ceremoniales.

Un enfoque mejor:

  • Guidelines ligeras, con motivos claros:
  • “Utilizamos Adapter para cualquier integración externa expuesta al dominio.”
  • “Los Decorators son el patrón para los cross-cuttings de dominio (log, retry y cache).”
  • Deja espacio para excepciones bien justificadas.

Cómo entrenar la mirada para reconocer patrones

  • En código .NET heredado:
  • Pide a los desarrolladores que identifiquen: “¿Dónde hay una Strategy? ¿Dónde hay un Template Method? ¿Dónde hay un Singleton disfrazado?”
  • Utiliza esto como punto de partida para refactors, no como una caza de brujas.

  • En frameworks y SDKs:

  • Al leer la documentación de ASP.NET Core, intenta responder:
    • “¿Qué patrón hay detrás de este middleware?”
    • “¿Esta API de configuración es un Builder?”

Con el tiempo, el equipo comenzará a hablar de “Decorator”, “Adapter” y “Strategy” con un significado concreto, no solo como términos de un libro.


Próximos pasos

Si has llegado hasta aquí, probablemente ya tengas una base para:

  1. Elegir uno o dos patrones para “verlos” en el código existente
  2. Abre un proyecto de ASP.NET Core y marca:

    • Dónde hay Decorator (middlewares, handlers y DelegatingHandler).
    • Dónde hay Template Method (controllers y background services).
  3. Seleccionar un caso real de tu equipo para aplicar conscientemente un patrón

  4. Por ejemplo, extraer una Strategy de un método lleno de if o crear un Adapter para aislar un SDK.

  5. Reforzar los fundamentos de las buenas prácticas de .NET

  6. Revisa un material de referencia como el artículo sobre buenas prácticas en .NET y C# y relaciona las buenas prácticas con los patrones tratados aquí.

  7. Llevar el tema al equipo

  8. Propón una sesión de lectura de código en la que cada persona identifique patrones en una parte de la solución.
  9. Analicen si cada patrón está ayudando o simplemente haciendo que el flujo sea más oscuro.

  10. Conectar con quienes viven esto en el día a día

  11. Participa en la comunidad SCCB — https://instagram.com/software_craftsmanship
  12. Consulta los próximos eventos — https://instagram.com/software_craftsmanship

Te gusto el articulo?

Compartelo con tus amigos y ayuda a difundir conocimiento!