use Scrutor to wrap your existing service with a decorator that adds caching, logging, or any cross-cutting concern — without modifying the original service.
why the decorator pattern?
injecting cache logic directly into your service violates the single responsibility principle. with a decorator, your LookupService only handles business logic. caching is a separate concern.
setup with scrutor
register the real service first, then decorate it:
builder.Services.AddScoped<ILookupService, LookupService>();
builder.Services.Decorate<ILookupService, LookupCacheDecorator>();
// stack decorator
// builder.Services.Decorate<ILookupService, LookupLoggingDecorator>();
that's it. every time ILookupService is resolved, you get LookupCacheDecorator wrapping LookupService.
the decorator
public class LookupCacheDecorator(
ILookupService inner,
IHybridCache cache,
ILogger<LookupCacheDecorator> logger) : ILookupService
{
public async ValueTask<IList<LookupModel>> GetCategoriesAsync()
{
var isCacheMissed = false;
var categories = await cache.GetOrCreateAsync("/categories", async (ct) =>
{
isCacheMissed = true;
logger.LogInformation("[CACHE MISS] Fetching from database...");
return await inner.GetCategoriesAsync();
});
if (!isCacheMissed)
{
logger.LogInformation("[CACHE HIT] Returned from cache.");
}
return categories;
}
}
key points
inneris the realLookupService, injected automatically by Scrutor.- the decorator is transparent to callers — they just use
ILookupService. - you can stack multiple decorators (e.g., logging → caching → retry) — the last
.Decoratecall becomes the outermost layer, so it runs first.