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
<PackageReference Include="AT.BackEnd.Patterns" Version="1.1.0" />
<PackageVersion Include="AT.BackEnd.Patterns" Version="1.1.0" />
<PackageReference Include="AT.BackEnd.Patterns" />
paket add AT.BackEnd.Patterns --version 1.1.0
#r "nuget: AT.BackEnd.Patterns, 1.1.0"
#:package AT.BackEnd.Patterns@1.1.0
#addin nuget:?package=AT.BackEnd.Patterns&version=1.1.0
#tool nuget:?package=AT.BackEnd.Patterns&version=1.1.0
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
- Proceso de instalación
- Software dependencies
- Cómo usarlo?
- 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
continuationy nonextporqueNextes 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
Scopeddeja unScoped. - Se pueden apilar, y el orden importa:
Decorate<A>y luegoDecorate<B>deja a B envolviendo a A. - El contenedor sigue siendo dueño del servicio interno, así que un decorado
IDisposablese 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
AddKeyedScopedy 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:
ActualizarAsyncdevolverí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.
TServicedebe ser una interfaz.- Un método con
[Cacheable]debe devolverTask<TResult>. Si devuelvevoid,Tasko un tipo síncrono, el registro falla. - Los argumentos deben tener identidad de valor fiable: primitivos,
string,decimal,Guid,DateTime,DateTimeOffset,TimeSpan, enums y susNullable<>. Cualquier otro tipo hace fallar el registro, porque la clave usaría identidad por referencia y la caché nunca acertaría. CancellationTokense 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 deIDistributedCachetrabaja conbyte[]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 | Versions 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. |
-
net10.0
- Microsoft.Extensions.Caching.Memory (>= 10.0.10)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.10)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.