# Regla: Patrones de Diseño — C# / .NET

Patrones establecidos en el ecosistema .NET moderno para código mantenible,
testeable y con separación clara de responsabilidades. Estos patrones están
soportados por las herramientas del stack — no reinventar lo que ya existe.

---

## CQRS con MediatR

Separar operaciones de lectura (queries) de escritura (commands):

```csharp
// Command — modifica estado, no retorna datos de negocio
public record CrearFacturaCommand(string ClienteEmail, decimal Total)
    : IRequest<Guid>;

public class CrearFacturaHandler : IRequestHandler<CrearFacturaCommand, Guid>
{
    public async Task<Guid> Handle(CrearFacturaCommand cmd, CancellationToken ct)
    {
        var factura = new Factura(cmd.ClienteEmail, cmd.Total);
        await _repo.AgregarAsync(factura, ct);
        return factura.Id;
    }
}

// Query — solo lectura, no modifica estado
public record ObtenerFacturasQuery(string ClienteEmail)
    : IRequest<IReadOnlyList<FacturaDto>>;

// En el endpoint
app.MapPost("/facturas", async (CrearFacturaCommand cmd, IMediator mediator) =>
    Results.Created($"/facturas/{await mediator.Send(cmd)}", null));
```

- Cada Command y Query es un record inmutable con los parámetros necesarios.
- Los Handlers tienen una sola responsabilidad.
- MediatR permite agregar behaviors (pipeline) para logging, validación y cache
  sin modificar los handlers.

---

## Repository pattern con EF Core

```csharp
// Interfaz — define el contrato, independiente de EF
public interface IFacturaRepository
{
    Task<Factura?> ObtenerPorIdAsync(Guid id, CancellationToken ct = default);
    Task<IReadOnlyList<Factura>> ListarPorClienteAsync(string email, CancellationToken ct = default);
    Task AgregarAsync(Factura factura, CancellationToken ct = default);
}

// Implementación con EF Core
public class FacturaRepository : IFacturaRepository
{
    private readonly AppDbContext _db;

    public async Task<Factura?> ObtenerPorIdAsync(Guid id, CancellationToken ct)
        => await _db.Facturas.FindAsync(new object[] { id }, ct);

    public async Task AgregarAsync(Factura factura, CancellationToken ct)
    {
        await _db.Facturas.AddAsync(factura, ct);
        // No llama SaveChangesAsync — eso es responsabilidad del servicio/handler
    }
}
```

- Los repositorios NUNCA llaman `SaveChangesAsync` — eso es del Handler o Service.
- Inyectar la interfaz (`IFacturaRepository`), no la implementación concreta.
- `Unit of Work` via `AppDbContext.SaveChangesAsync()` en el Handler, después de
  todas las operaciones de la transacción.

---

## Options pattern para configuración

```csharp
// Clase de configuración fuertemente tipada
public class SmtpOptions
{
    public const string SectionName = "Smtp";

    public string Host { get; init; } = string.Empty;
    public int Puerto { get; init; } = 587;
    public bool UsarTls { get; init; } = true;
}

// Registro en Program.cs
builder.Services.Configure<SmtpOptions>(
    builder.Configuration.GetSection(SmtpOptions.SectionName));

// Uso en servicios — IOptions<T> para configuración estática
public class EmailService(IOptions<SmtpOptions> opts)
{
    private readonly SmtpOptions _config = opts.Value;
}

// IOptionsSnapshot<T> para configuración recargable por request
// IOptionsMonitor<T> para configuración recargable en singletons
```

- NUNCA inyectar `IConfiguration` directamente en servicios — usar Options pattern.
- Validar la configuración al inicio con `ValidateDataAnnotations()` o `Validate()`.
- `init` (no `set`) para propiedades de configuración — son inmutables después de cargar.

---

## Dependency injection nativo de .NET

```csharp
// Program.cs — registro de servicios
builder.Services.AddScoped<IFacturaRepository, FacturaRepository>();
builder.Services.AddScoped<IEmailService, SmtpEmailService>();
builder.Services.AddSingleton<ICacheService, RedisCacheService>();

// Lifetimes:
// Scoped    — una instancia por request HTTP (la más común para servicios)
// Transient — nueva instancia cada vez (para servicios sin estado)
// Singleton — una instancia por toda la vida de la app (caches, configs)
```

- Usar el DI nativo de .NET — no agregar Autofac, Ninject u otros contenedores
  sin una razón técnica específica documentada.
- NUNCA usar `ServiceLocator` o `IServiceProvider` directamente en servicios
  de negocio — es un anti-patrón que oculta dependencias.
- Registrar la interfaz, no la implementación: `AddScoped<IServicio, Implementacion>()`.

---

## Result pattern para errores sin excepciones

```csharp
// MAL — excepciones para flujo de control normal
public async Task<Factura> ObtenerAsync(Guid id)
{
    var factura = await _repo.ObtenerPorIdAsync(id);
    if (factura is null)
        throw new FacturaNotFoundException(id); // excepción como flujo normal
    return factura;
}

// BIEN — Result pattern expresa el error en el tipo de retorno
public async Task<Result<Factura>> ObtenerAsync(Guid id, CancellationToken ct)
{
    var factura = await _repo.ObtenerPorIdAsync(id, ct);
    return factura is not null
        ? Result.Ok(factura)
        : Result.Fail<Factura>($"Factura {id} no encontrada");
}

// En el handler/endpoint — manejo explícito
var resultado = await servicio.ObtenerAsync(id, ct);
if (resultado.IsFailed)
    return Results.NotFound(resultado.Errors);
```

- Usar `FluentResults` o `ErrorOr` para el Result pattern.
- Las excepciones son para condiciones verdaderamente excepcionales (corrupción de datos,
  fallas de infraestructura) — no para flujos de negocio esperados.
- Los endpoints mapean `Result<T>` a respuestas HTTP apropiadas.

---

## Channel<T> para productor/consumidor

```csharp
// Para procesar eventos de forma asíncrona sin bloquear el request
public class ColaProcesamiento<T>
{
    private readonly Channel<T> _canal = Channel.CreateBounded<T>(
        new BoundedChannelOptions(1000) { FullMode = BoundedChannelFullMode.Wait });

    public async ValueTask EnqueueAsync(T item, CancellationToken ct)
        => await _canal.Writer.WriteAsync(item, ct);

    public async Task ProcesarAsync(
        Func<T, CancellationToken, Task> procesador,
        CancellationToken ct)
    {
        await foreach (var item in _canal.Reader.ReadAllAsync(ct))
            await procesador(item, ct);
    }
}
```

- `Channel<T>` para comunicación asíncrona entre productores y consumidores.
- `BoundedChannel` con límite de capacidad — evita consumo ilimitado de memoria.
- Usar `BackgroundService` de .NET para el consumidor.

---

## Minimal APIs vs Controllers: criterio de decisión

| Situación | Usar | Razón |
|-----------|------|-------|
| APIs simples, pocos endpoints | Minimal APIs | Menos boilerplate, más rápido |
| APIs grandes con muchos filtros y convenciones | Controllers | Mejor organización con attributes |
| Versionado de API complejo | Controllers | ApiExplorer integrado más maduro |
| Alta performance, bajo overhead | Minimal APIs | Menor overhead por request |
| Equipo familiarizado con MVC | Controllers | Menor curva de aprendizaje |

```csharp
// Minimal API — para servicios simples o microservicios
app.MapGet("/facturas/{id:guid}", async (Guid id, IMediator mediator, CancellationToken ct)
    => await mediator.Send(new ObtenerFacturaQuery(id), ct) is { } factura
        ? Results.Ok(factura)
        : Results.NotFound())
    .WithName("ObtenerFactura")
    .RequireAuthorization();
```

---

## Checklist de patrones antes de hacer merge

- [ ] Commands y Queries son records inmutables que implementan `IRequest<T>`
- [ ] Handlers tienen exactamente una responsabilidad
- [ ] Repositorios no llaman `SaveChangesAsync` — eso es del handler
- [ ] Configuración accedida via `IOptions<T>`, no `IConfiguration` directamente
- [ ] Sin uso de `ServiceLocator` en servicios de negocio
- [ ] Errores de flujo normal manejados con Result pattern, no excepciones
- [ ] `CancellationToken` propagado en todas las operaciones async
- [ ] DI nativo de .NET — sin contenedores externos sin justificación
