Passa al contenuto principale

Decorator in C#

Questa pagina mostra come applicare il Decorator in ASP.NET Core, sia in modo manuale che tramite la libreria Scrutor.


1. Decorator manuale con DI nativa​

Scenario​

Un servizio di catalogo viene interrogato spesso con gli stessi parametri. Si vuole aggiungere caching senza toccare il servizio.

Interfaccia e implementazione​

public interface ICatalogService
{
Task<Product?> GetByIdAsync(Guid id, CancellationToken ct);
}

public class CatalogService : ICatalogService
{
private readonly AppDbContext _db;
public CatalogService(AppDbContext db) => _db = db;

public Task<Product?> GetByIdAsync(Guid id, CancellationToken ct) =>
_db.Products.FirstOrDefaultAsync(p => p.Id == id, ct);
}

Decorator di caching​

public class CachingCatalogService : ICatalogService
{
private readonly ICatalogService _inner;
private readonly IMemoryCache _cache;

public CachingCatalogService(ICatalogService inner, IMemoryCache cache)
{
_inner = inner;
_cache = cache;
}

public Task<Product?> GetByIdAsync(Guid id, CancellationToken ct) =>
_cache.GetOrCreateAsync($"product:{id}", entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
return _inner.GetByIdAsync(id, ct);
});
}

Decorator di logging​

public class LoggingCatalogService : ICatalogService
{
private readonly ICatalogService _inner;
private readonly ILogger<LoggingCatalogService> _logger;

public LoggingCatalogService(ICatalogService inner, ILogger<LoggingCatalogService> logger)
{
_inner = inner;
_logger = logger;
}

public async Task<Product?> GetByIdAsync(Guid id, CancellationToken ct)
{
using var scope = _logger.BeginScope("CatalogService.GetById {ProductId}", id);
var sw = Stopwatch.StartNew();
var result = await _inner.GetByIdAsync(id, ct);
_logger.LogInformation("Completed in {Elapsed} ms (found={Found})",
sw.ElapsedMilliseconds, result is not null);
return result;
}
}

Registrazione manuale della catena​

builder.Services.AddScoped<CatalogService>();
builder.Services.AddScoped<ICatalogService>(sp =>
{
var real = sp.GetRequiredService<CatalogService>();
var cached = new CachingCatalogService(real, sp.GetRequiredService<IMemoryCache>());
return new LoggingCatalogService(cached, sp.GetRequiredService<ILogger<LoggingCatalogService>>());
});

L'ordine conta: LoggingCatalogService è all'esterno, quindi registra anche le chiamate servite dalla cache. Invertendo l'ordine, si loggherebbe solo ciò che arriva al servizio reale, utile in altri scenari.


2. Decorator con Scrutor​

Scrutor estende la DI nativa con un metodo Decorate che evita la registrazione manuale.

builder.Services.AddScoped<ICatalogService, CatalogService>();
builder.Services.Decorate<ICatalogService, CachingCatalogService>();
builder.Services.Decorate<ICatalogService, LoggingCatalogService>();

L'ordine di chiamata di Decorate determina la composizione: l'ultimo registrato è il più esterno. La catena risultante è la stessa dell'esempio manuale: Logging → Caching → Catalog.


3. Decorator come pipeline di MediatR​

Quando si usa MediatR, i IPipelineBehavior<TRequest, TResponse> sono decorator applicati a tutti i command/query. Sono l'equivalente dei middleware HTTP, ma applicati a livello di handler. Vedi la pagina sul Chain of Responsibility per i dettagli.


4. Composizione: quando l'ordine conta​

ComposizioneEffetto
Logging → Caching → RealLogga tutte le chiamate, inclusi i cache hit
Caching → Logging → RealLogga solo i cache miss
Retry → Logging → RealLogga ogni tentativo
Logging → Retry → RealLogga solo l'esito finale dopo eventuali retry

L'ordine va deciso esplicitamente in base a cosa si vuole osservare.


5. Quando non usare Decorator​

  • Per modificare la pipeline HTTP: usa middleware, non decorator.
  • Per intercettare azioni MVC: usa action filter, authorization filter o exception filter.
  • Per cross-cutting su tutti i command: usa pipeline behavior di MediatR.
  • Per un solo metodo, su un solo servizio: spesso una modifica diretta al servizio è più chiara.

Il decorator è la risposta giusta quando si ha un'interfaccia condivisa da più consumatori e si vuole aggiungere comportamento componibile.


Anti-pattern: decorator passivo​

public class UselessDecorator : ICatalogService
{
private readonly ICatalogService _inner;
public UselessDecorator(ICatalogService inner) => _inner = inner;
public Task<Product?> GetByIdAsync(Guid id, CancellationToken ct) =>
_inner.GetByIdAsync(id, ct);
}

Un decorator che si limita a inoltrare non aggiunge nulla. Se ne nasce uno così «in vista» di un comportamento futuro, va rimosso: si aggiungerà quando servirà davvero.