the quick notes

fast, quirky, and occasionally buggy: where tech meets sticky notes!

decorator with Scrutor in .NET

🇬🇧
techc#dotnetdesign patterns

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

  • inner is the real LookupService, 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 .Decorate call becomes the outermost layer, so it runs first.