AT.BackEnd.Patterns 1.1.0

dotnet add package AT.BackEnd.Patterns --version 1.1.0
                    
NuGet\Install-Package AT.BackEnd.Patterns -Version 1.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="AT.BackEnd.Patterns" Version="1.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="AT.BackEnd.Patterns" Version="1.1.0" />
                    
Directory.Packages.props
<PackageReference Include="AT.BackEnd.Patterns" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add AT.BackEnd.Patterns --version 1.1.0
                    
#r "nuget: AT.BackEnd.Patterns, 1.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package AT.BackEnd.Patterns@1.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=AT.BackEnd.Patterns&version=1.1.0
                    
Install as a Cake Addin
#tool nuget:?package=AT.BackEnd.Patterns&version=1.1.0
                    
Install as a Cake Tool

AT.BackEnd.Patterns

Este nuget expone patrones tácticos transversales para los backends .NET: Strategy, Specification, Chain of Responsibility, Decorate y Cache-aside.

Getting Started

  1. Proceso de instalación
  2. Software dependencies
  3. Cómo usarlo?
  4. API references

⚙️ Proceso de instalación:

Instale el nuget usando el siguiente comando.

.NET Cli
dotnet add package AT.BackEnd.Patterns --version 1.0.0
Nuget
NuGet\Install-Package AT.BackEnd.Patterns -Version 1.0.0
Package reference
<PackageReference Include="AT.BackEnd.Patterns" Version="1.0.0" />

🛠️Dependencias

net10.0

  • Microsoft.Extensions.Caching.Memory [10.0.10]
  • Microsoft.Extensions.DependencyInjection.Abstractions [10.0.10]

✈️ Cómo usarlo?

Cada patrón vive en su propio namespace y se registra de forma independiente: puede usar uno sin arrastrar los demás.

Patrón Namespace Para qué sirve
Strategy AT.BackEnd.Patterns.Strategy Elegir una implementación por nombre en tiempo de ejecución
Specification AT.BackEnd.Patterns.Specifications Reglas de negocio componibles, traducibles a SQL
Chain of Responsibility AT.BackEnd.Patterns.Chains Encadenar handlers que deciden si continúan
Decorate AT.BackEnd.Patterns.Registration Envolver un servicio ya registrado sin tocar su código, opcionalmente bajo condición
Cache-aside AT.BackEnd.Patterns.Caching Cachear métodos concretos de un servicio
Retry AT.BackEnd.Patterns.Resilience Reintentar una operación mientras el fallo sea transitorio

1️⃣ Strategy

Resuelve una implementación por nombre. Sirve cuando la variante se decide en runtime: el formato de exportación que pidió el usuario, el medio de pago, el motor de cálculo según el producto.

Definir las estrategias

Marque cada implementación con StrategyName. El contrato puede ser cualquier interfaz suya, o el IStrategy<TInput, TOutput> que expone el paquete.

using AT.BackEnd.Patterns.Strategy;

public interface IExportador
{
    Task<byte[]> ExportarAsync(int reporteId, CancellationToken cancellationToken = default);
}

[StrategyName("pdf")]
public sealed class ExportadorPdf : IExportador
{
    public Task<byte[]> ExportarAsync(int reporteId, CancellationToken cancellationToken = default) => ...
}

[StrategyName("excel")]
public sealed class ExportadorExcel : IExportador
{
    public Task<byte[]> ExportarAsync(int reporteId, CancellationToken cancellationToken = default) => ...
}
Program.cs

Registrar las estrategias con el metodo AddStrategies. Sin argumentos escanea el ensamblado que llama.

using AT.BackEnd.Patterns.Strategy;
            ⁝
    builder.Services.AddStrategies<IExportador>();

    // indicando ensamblados concretos
    builder.Services.AddStrategies<IExportador>(typeof(ExportadorPdf).Assembly);

    // materializando todas las estrategias por adelantado
    builder.Services.AddStrategies<IExportador>(lazy: false);
            ⁝
Consumir

Inyecte IStrategyResolver<T> y pida la estrategia por su nombre.

public sealed class ServicioReportes(IStrategyResolver<IExportador> resolver)
{
    public Task<byte[]> GenerarAsync(int reporteId, string formato, CancellationToken cancellationToken)
    {
        var exportador = resolver.ResolveRequired(formato);
        return exportador.ExportarAsync(reporteId, cancellationToken);
    }
}
API
Miembro Descripción
TStrategy? Resolve(string name) Devuelve la estrategia o null. La comparación ignora mayúsculas.
TStrategy ResolveRequired(string name) Lanza KeyNotFoundException listando los nombres disponibles.
bool TryResolve(string name, out TStrategy? strategy) Devuelve false en vez de lanzar.
IReadOnlyCollection<string> Names Todos los nombres registrados. Útil para validar configuración al arranque.

ResolveRequired y TryResolve son métodos de extensión sobre IStrategyResolver<T>, no miembros de la interfaz: así una implementación propia del contrato los obtiene sin cambiar.

Comportamiento que conviene conocer

La resolución es perezosa por defecto. Solo se construye la estrategia que se pide. Importa cuando alguna tiene un constructor costoso (un DbContext, un HttpClient): no se paga si nadie la usa. Los dos modos dejan el mismo grafo registrado; lo único que cambia es cuándo se construyen.

El resolutor se registra como Scoped, de modo que una estrategia puede depender de servicios Scoped sin quedar cautiva del contenedor raíz. Resuélvalo dentro de un ámbito, como cualquier servicio de petición.

Los nombres duplicados fallan al arrancar. Si dos estrategias del mismo contrato declaran el mismo nombre, AddStrategies lanza InvalidOperationException indicando cuáles chocan.

Varias llamadas sobre el mismo contrato acumulan. Un monolito modular puede registrar cada módulo por separado y el resolutor los ve todos:

    builder.Services.AddStrategies<IExportador>(typeof(ModuloFacturacion).Assembly);
    builder.Services.AddStrategies<IExportador>(typeof(ModuloCobranza).Assembly);

Queda un único IStrategyResolver<IExportador> que resuelve las estrategias de ambos módulos. Repetir el mismo ensamblado es idempotente; la validación de nombres duplicados también actúa entre llamadas, así que dos módulos que reclamen el mismo nombre fallan al arrancar. Si las llamadas discrepan en lazy, gana la última — ambos modos dejan el mismo grafo registrado y solo cambia cuándo se construyen las estrategias.

Contratos genéricos cerrados

AddStrategies funciona con cualquier contrato, incluido un IStrategy<,> cerrado.

[StrategyName("duplicar")]
public sealed class DuplicarTexto : IStrategy<int, string>
{
    public Task<string> Execute(int input, CancellationToken cancellationToken = default) => ...
}
            ⁝
    builder.Services.AddStrategies<IStrategy<int, string>>();
            ⁝

AddStrategies es el único punto de entrada del patrón: exige declarar el contrato de forma explícita, de modo que el resolutor que se inyecta y las estrategias que se registran queden siempre a la vista en el Program.cs.


2️⃣ Specification

Encapsula una regla de negocio en un objeto componible. La misma regla sirve para validar en memoria y para filtrar en base de datos, sin duplicar la lógica.

Definir especificaciones

Herede de Specification<T> e implemente ToExpression.

using System.Linq.Expressions;
using AT.BackEnd.Patterns.Specifications;

public sealed class ClienteActivo : Specification<Cliente>
{
    public override Expression<Func<Cliente, bool>> ToExpression() => cliente => cliente.Activo;
}

public sealed class SaldoMayorQue(decimal minimo) : Specification<Cliente>
{
    public override Expression<Func<Cliente, bool>> ToExpression() => cliente => cliente.Saldo > minimo;
}
Componer y usar
var spec = new ClienteActivo().And(new SaldoMayorQue(100m));

// en memoria
if (spec.IsSatisfiedBy(cliente)) { ... }

// contra la base de datos: se traduce a SQL, el filtrado ocurre en el motor
var clientes = await contexto.Clientes
    .Where(spec.ToExpression())
    .ToListAsync(cancellationToken);

Los operadores equivalen a los métodos y suelen leerse mejor al encadenar:

var spec = (new ClienteActivo() | new SaldoMayorQue(800m)) & !new EnListaNegra();
API
Miembro Descripción
Expression<Func<T, bool>> ToExpression() La regla como árbol de expresión. Es lo que se pasa a EF Core.
bool IsSatisfiedBy(T candidate) Evalúa en memoria. La expresión se compila una sola vez y se reutiliza.
And(ISpecification<T>) / operator & Exige ambas.
Or(ISpecification<T>) / operator \| Basta con una.
Not() / operator ! Niega.
Specification.All<T>() Siempre verdadera. Útil como neutro al construir filtros dinámicos.
Specification.None<T>() Siempre falsa.
Comportamiento que conviene conocer

Las especificaciones compuestas se traducen a SQL de verdad. Al combinar dos reglas, cada una trae su propio parámetro de expresión; el paquete los reunifica antes de construir el árbol. Sin eso EF Core no podría traducir la consulta y la degradaría a evaluación en cliente: traer la tabla entera a memoria y filtrar ahí.

Las lambdas anidadas sobreviven a la composición. Una regla como p => p.Lineas.Any(l => l.Activa) se puede combinar con And/Or sin que el parámetro interno se vea afectado.


3️⃣ Chain of Responsibility

Encadena handlers sobre un contexto compartido. Cada uno decide si continúa, y puede ejecutar lógica antes y después del resto de la cadena.

Definir los handlers
using AT.BackEnd.Patterns.Chains;

public sealed class SolicitudContext
{
    public Solicitud Solicitud { get; init; } = null!;
    public List<string> Errores { get; } = [];
}

[ChainOrder(1)]
public sealed class ValidarDatos : IChainHandler<SolicitudContext>
{
    public async Task HandleAsync(
        SolicitudContext context,
        ChainNext<SolicitudContext> continuation,
        CancellationToken cancellationToken)
    {
        if (context.Solicitud.Monto <= 0)
        {
            context.Errores.Add("El monto debe ser mayor que cero.");
            return;                                          // cortar: no invocar continuation
        }

        await continuation(context, cancellationToken);
    }
}

[ChainOrder(2)]
public sealed class ConsultarBuro : IChainHandler<SolicitudContext>
{
    public async Task HandleAsync(
        SolicitudContext context,
        ChainNext<SolicitudContext> continuation,
        CancellationToken cancellationToken)
    {
        var inicio = Stopwatch.GetTimestamp();
        await continuation(context, cancellationToken);      // lógica antes y después
        RegistrarDuracion(Stopwatch.GetElapsedTime(inicio));
    }
}
Program.cs

Registrar la cadena con el metodo AddChain. Descubre los handlers y da de alta IChain<TContext>.

using AT.BackEnd.Patterns.Chains;
            ⁝
    builder.Services.AddChain<SolicitudContext>();
            ⁝
Consumir
public sealed class ServicioSolicitudes(IChain<SolicitudContext> chain)
{
    public async Task<bool> ProcesarAsync(Solicitud solicitud, CancellationToken cancellationToken)
    {
        var context = new SolicitudContext { Solicitud = solicitud };
        await chain.ExecuteAsync(context, cancellationToken);
        return context.Errores.Count == 0;
    }
}
API
Miembro Descripción
Task HandleAsync(TContext, ChainNext<TContext>, CancellationToken) El eslabón. Invocar continuation sigue la cadena; no invocarlo la corta.
Task ExecuteAsync(TContext, CancellationToken) Ejecuta la cadena completa.
[ChainOrder(n)] Fija la posición. Los valores menores se ejecutan antes.
AddChain<TContext>(params Assembly[]) Descubre los handlers y registra IChain<TContext>.

Los handlers sin ChainOrder se ejecutan al final, en orden de descubrimiento.

El parámetro del delegado se llama continuation y no next porque Next es palabra reservada en Visual Basic y el analizador CA1716 lo rechaza en miembros de interfaz pública.


4️⃣ Decorate

Envuelve un servicio ya registrado en el contenedor sin modificar su código. Es la vía para añadir trazas, reintentos, validación o caché a algo que no se quiere tocar.

Definir el decorador

Recibe el servicio interno por constructor.

using AT.BackEnd.Patterns.Registration;

public sealed class TarifasConTrazas(ITarifas inner, ILogger<TarifasConTrazas> logger) : ITarifas
{
    public async Task<Tarifa> ObtenerAsync(int id, CancellationToken cancellationToken = default)
    {
        logger.LogInformation("Consultando tarifa {Id}", id);
        return await inner.ObtenerAsync(id, cancellationToken);
    }
}
Program.cs
using AT.BackEnd.Patterns.Registration;
            ⁝
    builder.Services.AddScoped<ITarifas, Tarifas>();
    builder.Services.Decorate<ITarifas, TarifasConTrazas>();
            ⁝
API
Miembro Descripción
Decorate<TService, TDecorator>() Envuelve el último registro del contrato.
Decorate(Type serviceType, Type decoratorType) Igual, para tipos resueltos en tiempo de ejecución.
Comportamiento que conviene conocer
  • El lifetime original se conserva. Decorar un Scoped deja un Scoped.
  • Se pueden apilar, y el orden importa: Decorate<A> y luego Decorate<B> deja a B envolviendo a A.
  • El contenedor sigue siendo dueño del servicio interno, así que un decorado IDisposable se libera con normalidad al cerrar el ámbito.
  • Falla al arrancar si no hay registro previo del contrato, en lugar de aplicar el decorador en silencio a nada.
  • Solo contempla registros sin clave. Los servicios registrados con AddKeyedScoped y similares quedan fuera de alcance.

5️⃣ Cache-aside

Dos piezas: un decorador por atributo para el caso común, y un helper explícito para control fino.

Opción A — decorador por atributo

Marque solo los métodos que quiere cachear. Los demás pasan intactos al servicio real.

using AT.BackEnd.Patterns.Caching;

public interface ITarifas
{
    [Cacheable(Minutes = 10)]
    Task<Tarifa> ObtenerAsync(int id, CancellationToken cancellationToken = default);

    Task ActualizarAsync(Tarifa tarifa);              // sin atributo: nunca se cachea
}
Program.cs
using AT.BackEnd.Patterns.Caching;
            ⁝
    builder.Services.AddScoped<ITarifas, Tarifas>();
    builder.Services.DecorateWithCache<ITarifas>();
            ⁝

DecorateWithCache registra por su cuenta IMemoryCache y ICacheAside si no lo estaban, así que esa única línea deja el módulo operativo.

El opt-in no es una preferencia de estilo, es un requisito de corrección. Un decorador que cachease la interfaz entera se comería también los métodos de escritura: ActualizarAsync devolvería un resultado cacheado en lugar de ejecutar la actualización. Con opt-in, olvidar el atributo cuesta como mucho rendimiento; nunca datos.

Opción B — helper explícito

Cuando necesite claves compuestas, invalidación o TTL dinámico, inyecte ICacheAside.

using AT.BackEnd.Patterns.Caching;

public sealed class ServicioTarifas(ITarifas inner, ICacheAside cache)
{
    public Task<Tarifa> ObtenerAsync(int id, string moneda, CancellationToken cancellationToken) =>
        cache.GetOrCreateAsync(
            $"tarifa:{id}:{moneda}",
            () => inner.ObtenerAsync(id, cancellationToken),
            TimeSpan.FromMinutes(10),
            cancellationToken);

    public Task InvalidarAsync(int id, string moneda, CancellationToken cancellationToken) =>
        cache.RemoveAsync($"tarifa:{id}:{moneda}", cancellationToken);
}

Requiere builder.Services.AddMemoryCache(); y el registro de ICacheAside, que DecorateWithCache ya hace por usted si usa la opción A.

API
Miembro Descripción
[Cacheable(Minutes = , Seconds = )] Marca un método como cacheable. Ambas propiedades se suman.
DecorateWithCache<TService>() Envuelve el contrato con el proxy de caché.
Task<T> GetOrCreateAsync<T>(string key, Func<Task<T>> factory, TimeSpan ttl, CancellationToken) Devuelve lo cacheado, o lo calcula y lo guarda.
Task RemoveAsync(string key, CancellationToken) Invalida una entrada.
Reglas del contrato, validadas al registrar

DecorateWithCache valida el contrato completo en el momento del registro, no al instanciar el proxy. Así un contrato mal marcado hace fallar el arranque de la aplicación, y no la primera petición que lo toque en producción tras horas de uptime.

  • TService debe ser una interfaz.
  • Un método con [Cacheable] debe devolver Task<TResult>. Si devuelve void, Task o un tipo síncrono, el registro falla.
  • Los argumentos deben tener identidad de valor fiable: primitivos, string, decimal, Guid, DateTime, DateTimeOffset, TimeSpan, enums y sus Nullable<>. Cualquier otro tipo hace fallar el registro, porque la clave usaría identidad por referencia y la caché nunca acertaría.
  • CancellationToken se excluye de la clave.

La clave incluye el tipo declarante y la firma completa del método, de modo que dos sobrecargas no colisionan. Las fechas se serializan en formato round-trip ("O"), conservando milisegundos y Kind.

Se apoya en IMemoryCache (caché en proceso) a propósito: la API de IDistributedCache trabaja con byte[] y obligaría a este paquete a imponer un serializador a todos sus consumidores.


6️⃣ Retry

Reintenta una operación mientras el fallo sea transitorio, con retardo exponencial y jitter. La clasificación de qué es transitorio la aporta usted: el paquete no puede saber si una excepción concreta merece reintento.

Program.cs
using AT.BackEnd.Patterns.Resilience;
            ⁝
    builder.Services.AddRetryPolicy(
        isTransient: ex => ex is TimeoutException or SocketException,
        configure: options =>
        {
            options.MaxAttempts = 4;
            options.BaseDelay = TimeSpan.FromMilliseconds(500);
            options.MaxDelay = TimeSpan.FromSeconds(10);
            options.JitterFactor = 0.2;
        });

AddRetryPolicy requiere que TimeProvider esté registrado. En una aplicación con host lo está por defecto. En pruebas, baje BaseDelay a 1 ms para que la espera sea despreciable, o registre un reloj falso si necesita control exacto del tiempo.

Uso
public sealed class EnviadorConReintentos(IEnviador inner, IRetryPolicy retry) : IEnviador
{
    public Task EnviarAsync(Mensaje mensaje, CancellationToken cancellationToken) =>
        retry.ExecuteAsync(ct => inner.EnviarAsync(mensaje, ct), cancellationToken);
}

El retardo del intento n es BaseDelay * 2^(n-1), topado por MaxDelay y variado por JitterFactor. La cancelación nunca se reintenta: se propaga tal cual.


7️⃣ DecorateWhen

Aplica un decorador solo si se cumple una condición, evaluada al resolver el servicio. Sirve para comportamiento que depende del entorno o de la configuración.

using AT.BackEnd.Patterns.Registration;
            ⁝
    builder.Services.AddScoped<IEnviador, EnviadorSmtp>();
    builder.Services.DecorateWhen<IEnviador, EnviadorEnSeco>(
        provider => provider.GetRequiredService<IHostEnvironment>().IsDevelopment());

Si el predicado devuelve false, el contenedor entrega el servicio interno sin envolver.

El predicado se evalúa al resolver y no al registrar: evaluarlo al registrar no necesitaría este método, bastaría con envolver la llamada a Decorate en un if.

📌 API references

Namespace Tipos públicos
AT.BackEnd.Patterns.Strategy IStrategy<TInput,TOutput>, IStrategyResolver<TStrategy>, StrategyResolver<TStrategy>, StrategyNameAttribute, StrategyResolverExtensions, StrategyServiceCollectionExtensions
AT.BackEnd.Patterns.Specifications ISpecification<T>, Specification<T>, Specification
AT.BackEnd.Patterns.Chains IChain<TContext>, IChainHandler<TContext>, ChainNext<TContext>, Chain<TContext>, ChainOrderAttribute, ChainServiceCollectionExtensions
AT.BackEnd.Patterns.Registration DecoratorServiceCollectionExtensions
AT.BackEnd.Patterns.Caching ICacheAside, MemoryCacheAside, CacheableAttribute, CachingServiceCollectionExtensions

El paquete incluye documentación XML, así que las firmas y observaciones aparecen en IntelliSense.

🎓 Créditos

Nombre del Paquete: AT.BackEnd.Patterns Versión: 1.0.0

Autor

  • Nombre: Dayser José Granados Pineda
  • Correo Electrónico: djpgranados@gmail.com | daysergranados@hotmail.com

Licencia

Este paquete está bajo la licencia MIT License

Copyright (c) 2026 Dayser José Granados Pineda

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE..

Contacto

Si tienes comentarios, problemas o solicitudes, ¡no dudes en ponerte en contacto conmigo!

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.1.0 90 8/17/2026
1.0.0 99 8/4/2026