Decorator Pattern in ASP.NET Core: A Friendly Guide
Learn the Decorator pattern in ASP.NET Core with simple examples, Scrutor, caching and logging decorators, and clear diagrams for beginners.
A gift box you can wrap again and again
Think about a birthday gift. First you have the toy. Then you put it in a box. Then you wrap the box in shiny paper. Then you tie a ribbon on top. Then you add a small card.
The toy never changed. It is still the same toy. But each layer added something new: protection, colour, a ribbon, a message. You can also remove a layer and the toy is still fine.
The Decorator pattern works exactly like this in code. You take a working object and wrap it in another object that adds one small extra job, like logging or caching. The inside object does not know it is wrapped. The code that uses it does not know either. Everyone is happy.
In ASP.NET Core this is a very common and friendly trick. Let us learn it step by step.
What is the Decorator pattern?
The Decorator pattern is one of the classic Gang of Four design patterns. The idea is simple:
- You have an interface, for example
IGreetingService. - You have a real class that does the work, for example
GreetingService. - You write a wrapper class that also implements
IGreetingService. - The wrapper holds a reference to another
IGreetingServiceand calls it inside. - Around that call, the wrapper adds something extra.
Because the wrapper has the same interface as the real class, you can slide it in anywhere the real class was used. Nobody has to change their code.
The arrow from the decorator back to the real service is the heart of the pattern. The decorator does not replace the service. It stands in front of it and passes the call along.
A real, tiny example
Let us start with a plain service that says hello.
public interface IGreetingService
{
string Greet(string name);
}
public class GreetingService : IGreetingService
{
public string Greet(string name) => $"Hello, {name}!";
}Now suppose we want to log every greeting. We do not want to put logging code inside GreetingService. That class has one job: making greetings. Mixing logging into it would make it messy and harder to test.
So we write a decorator instead.
public class LoggingGreetingService : IGreetingService
{
private readonly IGreetingService _inner;
private readonly ILogger<LoggingGreetingService> _logger;
public LoggingGreetingService(
IGreetingService inner,
ILogger<LoggingGreetingService> logger)
{
_inner = inner;
_logger = logger;
}
public string Greet(string name)
{
_logger.LogInformation("Greeting {Name}", name);
var result = _inner.Greet(name);
_logger.LogInformation("Greeted {Name}", name);
return result;
}
}Look closely. LoggingGreetingService implements the same IGreetingService. It takes an inner service in its constructor. It logs, calls _inner.Greet(name), logs again, then returns the result. The real greeting logic still lives only in GreetingService.
This is the whole pattern. One class wraps another and adds a small extra step around it.
Why not just edit the original class?
A fair question. Why wrap when you could just add the logging line inside GreetingService?
The reason is the Single Responsibility Principle: a class should have one reason to change. If GreetingService only makes greetings, it changes only when greeting rules change. The moment you add logging, caching, retries, and timing inside it, it now has five reasons to change. That class becomes a tangle.
Decorators keep each job in its own small class. You can stack them like the gift wrapping. You can turn one off without touching the others.
One job per layer
Steps
Core
Does the real work
Logging
Records what happened
Caching
Saves repeat answers
Timing
Measures how long it took
Wiring it up by hand in ASP.NET Core
ASP.NET Core has a built-in dependency injection (DI) container. It is great, but out of the box it does not have a one-line way to say "decorate this service". So first we do it the manual way, to understand what really happens.
var builder = WebApplication.CreateBuilder(args);
// Register the real service as itself.
builder.Services.AddScoped<GreetingService>();
// Register the interface using a factory that builds the decorator.
builder.Services.AddScoped<IGreetingService>(provider =>
{
var inner = provider.GetRequiredService<GreetingService>();
var logger = provider.GetRequiredService<ILogger<LoggingGreetingService>>();
return new LoggingGreetingService(inner, logger);
});
var app = builder.Build();Here we register the real GreetingService by its concrete type. Then we register the interface IGreetingService with a factory. The factory asks the container for the real service and the logger, then hands back a LoggingGreetingService that wraps them.
Now whenever any controller asks for IGreetingService, the container gives it the logging decorator. The decorator quietly uses the real service inside. The controller has no idea.
This works, but you can see it is a bit wordy. If you add two or three decorators, the factory grows ugly fast. That is where Scrutor comes in.
The easy way: Scrutor
Scrutor is a small, popular, free open-source NuGet package. It adds helpful methods to the DI container, including Decorate. With Scrutor the wiring becomes one clean line.
builder.Services.AddScoped<IGreetingService, GreetingService>();
// Wrap the registered service with a decorator.
builder.Services.Decorate<IGreetingService, LoggingGreetingService>();That is it. First you register the real service against the interface as normal. Then you call Decorate. Scrutor finds the existing registration and wraps it for you. It even fills in the extra constructor dependencies (like the logger) automatically.
The biggest win is that adding a new dependency to a decorator's constructor does not force you to edit your registration code. Scrutor figures it out. That keeps your Program.cs tidy.
Add Scrutor to your project with one command:
// In a terminal, from your project folder:
// dotnet add package ScrutorStacking many decorators
You can call Decorate more than once. Each call wraps whatever is currently registered. This lets you build the gift wrapping layer by layer.
builder.Services.AddScoped<IGreetingService, GreetingService>();
builder.Services.Decorate<IGreetingService, CachingGreetingService>();
builder.Services.Decorate<IGreetingService, LoggingGreetingService>();Order matters and it is worth slowing down here. The last Decorate call ends up on the outside. So a request flows from logging, then caching, then finally the real service.
Read the diagram twice. The call travels right into the centre, the real service answers, then the answer travels back out through each layer. Each layer can do something on the way in and something on the way out.
A more useful example: caching
Logging is nice, but caching shows the real power of decorators. Imagine a service that fetches a product from a database. The database call is slow. We want to remember answers so we do not ask twice for the same product.
public class CachingProductService : IProductService
{
private readonly IProductService _inner;
private readonly IMemoryCache _cache;
public CachingProductService(IProductService inner, IMemoryCache cache)
{
_inner = inner;
_cache = cache;
}
public async Task<Product?> GetByIdAsync(int id)
{
var key = $"product:{id}";
if (_cache.TryGetValue(key, out Product? cached))
{
return cached;
}
var product = await _inner.GetByIdAsync(id);
_cache.Set(key, product, TimeSpan.FromMinutes(5));
return product;
}
}The first call goes all the way to the database through _inner. The answer is stored in memory for five minutes. The next call for the same id returns instantly from the cache and never touches the database. The real ProductService never learned a single thing about caching. It still has one clean job.
This is a perfect decorator job because caching is a cross-cutting concern. Many services may need it, and none of them should be polluted with cache code.
How a request flows through the layers
Let us trace one real request through a stack of decorators so the inside-out movement is crystal clear.
A cached, logged request
Steps
Controller
Asks for product 7
Logging
Notes the request
Caching
Cache miss, ask inner
Database
Returns product 7
Here is the same journey as a sequence so you can see who talks to whom and when.
On the second request for product 7, the caching layer answers right away and the message never reaches the database. The logging layer still records both requests. Each layer minds its own business.
When should you use the Decorator pattern?
Decorators shine when you want to add the same kind of extra behaviour to one or many services without rewriting them. Here are the most common honest use cases.
| Use case | What the decorator adds | Why a decorator fits |
|---|---|---|
| Logging | Records inputs and outputs | Keeps logging out of business code |
| Caching | Stores repeat answers | Speeds things up transparently |
| Retry | Tries again on failure | Adds resilience around a flaky call |
| Validation | Checks inputs first | Guards the real service cleanly |
| Timing | Measures how long calls take | Helps you find slow methods |
And here is how decorators compare to two patterns people often mix them up with.
| Pattern | Wraps what | Shares interface? | Typical use |
|---|---|---|---|
| Decorator | A single service object | Yes | Logging, caching, retry |
| Middleware | The whole HTTP pipeline | No (uses RequestDelegate) | Auth, CORS, error handling |
| Proxy | A single service object | Yes | Lazy loading, access control |
The Decorator and Proxy patterns look almost the same in code. The difference is intent. A decorator adds behaviour. A proxy controls access to the real object, for example by loading it late or checking permissions first.
Decorators versus middleware
ASP.NET Core middleware also layers behaviour, so beginners often ask if they are the same thing. The idea is shared, but the scope is different.
Middleware wraps the entire HTTP request. It runs before your controller and after it, for every request. It is great for things that affect the whole pipeline: authentication, compression, error pages.
A decorator wraps one service deep inside your code. It is great for behaviour that belongs to a specific job, like caching a single repository or retrying a single API client.
Use middleware for the request as a whole. Use decorators for the services inside.
Common mistakes to avoid
The pattern is simple, but a few traps catch new developers. Keep these in mind.
- Forgetting the same interface. A decorator must implement the exact same interface as the thing it wraps. If it does not, the container cannot swap it in.
- Wrong order. With Scrutor, the last
Decoratecall is outermost. If your logging shows up in the wrong place, check the order. - Hiding errors. A retry or caching decorator can accidentally swallow exceptions. Always think about what happens on failure.
- Too many layers. Three or four decorators are fine. Twelve layers become hard to follow. Keep the stack short and clear.
- Doing real work in the decorator. A decorator should add a side concern, then call inner. If it starts doing the main job itself, it is no longer a decorator.
A quick note on related tools
You may have heard of MediatR and MassTransit, which use a similar layering idea called pipeline behaviours. Both libraries moved to a commercial license in their newer versions, so check the terms before using them in a paid product. The plain Decorator pattern with the built-in container or with Scrutor stays free and is often all you need for adding logging or caching around a service.
This guide reflects the current .NET landscape: .NET 10 is the LTS release, C# 14 has shipped, and Scrutor remains a free, open-source way to register decorators.
Putting it all together
Here is a tidy Program.cs that registers a real service and stacks two decorators with Scrutor.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddMemoryCache();
builder.Services.AddScoped<IProductService, ProductService>();
builder.Services.Decorate<IProductService, CachingProductService>();
builder.Services.Decorate<IProductService, LoggingProductService>();
var app = builder.Build();
app.MapGet("/products/{id:int}", async (int id, IProductService service) =>
{
var product = await service.GetByIdAsync(id);
return product is null ? Results.NotFound() : Results.Ok(product);
});
app.Run();The endpoint asks for a plain IProductService. It has no idea there is logging and caching wrapped around it. When you want to change behaviour, you change the registration, not the endpoint. That is the freedom decorators give you.
Quick recap
- The Decorator pattern wraps an object to add extra behaviour without changing the original class.
- The wrapper shares the same interface as the thing it wraps and holds an inner reference to it.
- It keeps each concern, like logging or caching, in its own small clean class.
- In ASP.NET Core you can register decorators by hand with a factory, or far more cleanly with the free Scrutor package and its
Decoratemethod. - With Scrutor, the last
Decoratecall sits on the outside, so order matters. - Caching and logging are the classic real-world uses, because they are concerns that should not live inside business code.
- Decorators wrap a single service; middleware wraps the whole HTTP pipeline. Pick the right scope for the job.
References and further reading
- Microsoft Learn: Dependency injection in .NET
- Scrutor on GitHub
- Andrew Lock: Adding decorated classes to the ASP.NET Core DI container using Scrutor
- Tim Deschryver: The decorator pattern using .NET's dependency injection
- Milan Jovanovic: Decorator Pattern In ASP.NET Core
Related Patterns
The Repository Pattern in .NET: A Friendly, Complete Guide
Learn the Repository Pattern in .NET with simple real-life examples, EF Core code, diagrams, and honest advice on when to use it and when to skip it.
CQRS Pattern with MediatR in .NET: A Friendly Guide
Learn the CQRS pattern with MediatR in .NET using simple words, clear diagrams, and real C# code. Beginner friendly, with pitfalls and licensing notes.
3 Ways To Create Middleware In ASP.NET Core (Beginner Guide)
Learn the 3 ways to create middleware in ASP.NET Core: inline request delegates, convention-based classes, and factory-based IMiddleware, with simple diagrams and code.
CQRS Pattern in .NET: The Way It Should Have Been From the Start
A friendly, hands-on guide to the CQRS pattern in .NET 10. Learn commands, queries, handlers, diagrams, and when to actually use it.
Specification Pattern in EF Core: Flexible Data Access Without Repositories
Learn the Specification pattern in EF Core to build reusable, testable, composable queries without piling up repository methods or hiding IQueryable.
Stop Conflating CQRS and MediatR: They Are Not the Same Thing
CQRS and MediatR are two different ideas. Learn what each one really does, why people mix them up, and how to use CQRS in .NET with or without MediatR.