The Decorator Pattern for C# Developers

The Decorator pattern is one of those patterns that sounds fancy but is actually pretty simple once you see it in action. It lets you add behavior to an object without changing the object itself. You wrap it, and the wrapper adds something extra.

Let’s Define the Pattern Clearly

A decorator is a class that wraps another class and implements the same interface. It forwards calls to the wrapped object, but it can do something before or after (or both). The key idea is that the wrapped object does not know it’s being wrapped, and the caller does not know it’s talking to a wrapper.

What a decorator is not: it’s not inheritance. You’re not creating a subclass that overrides behavior. You’re creating a separate class that holds a reference to the original and delegates to it.

The mental model is wrapping. Think of it like putting a gift in a box, then putting that box in another box. Each box can add something (a ribbon, a tag, padding) without changing what’s inside.

The Problem It Solves

Imagine you have a service that sends notifications. It works fine. Then someone asks for logging. Then someone else wants retry logic. Then you need to add metrics. If you keep adding these features directly to the class, you end up with a mess of flags and conditionals.

public class NotificationService : INotificationService
{
    public void Send(string message)
    {
        if (_enableLogging)
            _logger.Log("Sending notification...");
        
        if (_enableRetry)
        {
            // retry logic here
        }
        
        if (_enableMetrics)
            _metrics.Record("notification.sent");
        
        // actual send logic
    }
}

This is what happens when cross-cutting concerns leak into core logic. The class becomes harder to read, harder to test, and harder to change. Every new feature means touching the same class.

Inheritance doesn’t help much either. You’d end up with LoggingNotificationService, RetryingNotificationService, LoggingRetryingNotificationService, and so on. The combinations explode.

Core Structure and Roles

The decorator pattern has a few moving parts:

  • Component interface: The contract that both the real implementation and decorators implement.
  • Concrete component: The actual implementation that does the real work.
  • Decorator base (optional): A base class for decorators that forwards all calls to the wrapped object. This reduces boilerplate.
  • Concrete decorators: The wrappers that add specific behavior.

The interface is the most important piece. It’s what makes the pattern work. Because both the real implementation and the decorators implement the same interface, they’re interchangeable. You can wrap a decorator with another decorator, and the caller never knows.

Here’s the component interface and concrete implementation:

public interface INotificationService
{
    void Send(string message);
}

public class NotificationService : INotificationService
{
    public void Send(string message)
    {
        // actual send logic
        Console.WriteLine($"Sending: {message}");
    }
}

And here’s a decorator that adds logging:

public class LoggingNotificationDecorator : INotificationService
{
    private readonly INotificationService _inner;
    private readonly ILogger _logger;

    public LoggingNotificationDecorator(INotificationService inner, ILogger logger)
    {
        _inner = inner;
        _logger = logger;
    }

    public void Send(string message)
    {
        _logger.Log("Sending notification...");
        _inner.Send(message);
        _logger.Log("Notification sent.");
    }
}

The decorator takes the real service in its constructor, implements the same interface, and adds logging around the call. The real service stays clean. You can use it with or without the decorator.

Composition and Ordering

You can stack decorators. Each one wraps the previous:

INotificationService service = new NotificationService();
service = new LoggingNotificationDecorator(service, logger);
service = new RetryNotificationDecorator(service, retryPolicy);
service = new MetricsNotificationDecorator(service, metrics);

Order matters. In this example, metrics wraps retry, which wraps logging, which wraps the real service. When you call Send, the call flows through metrics first, then retry, then logging, then the actual service.

If you want logging to capture retries, put logging outside retry. If you want logging to only log the final attempt, put it inside. Think about what each decorator should see and order accordingly.

Make the order explicit. Don’t rely on container registration order if you can help it. When someone reads the code, they should be able to see the chain.

Decorator vs Similar Concepts

Decorator vs inheritance: Inheritance is “is-a”. A decorator is “has-a”. With inheritance, you’re locked into a class hierarchy. With decorators, you compose behavior at runtime.

Decorator vs proxy: A proxy controls access to an object (lazy loading, remote calls, access control). A decorator adds behavior. In practice, the line can blur, but the intent is different.

Decorator vs middleware/pipelines: Middleware is a chain of handlers where each can short-circuit or pass to the next. Decorators always delegate to the wrapped object. ASP.NET middleware and MediatR pipelines are similar in spirit but have different mechanics.

People sometimes call any wrapper a “decorator”. That’s fine in casual conversation, but if you’re discussing design, be precise about whether you mean the pattern or just “a class that wraps another class”.

Integration with Dependency Injection

Decorators and DI work well together. You register the real implementation, then wrap it with decorators during registration.

With the built-in .NET container, you have to do this manually:

services.AddScoped<NotificationService>();
services.AddScoped<INotificationService>(sp =>
{
    var inner = sp.GetRequiredService<NotificationService>();
    var logger = sp.GetRequiredService<ILogger>();
    return new LoggingNotificationDecorator(inner, logger);
});

Libraries like Scrutor make this cleaner:

services.AddScoped<INotificationService, NotificationService>();
services.Decorate<INotificationService, LoggingNotificationDecorator>();
services.Decorate<INotificationService, RetryNotificationDecorator>();

DI makes decorators practical at scale because you don’t have to manually wire up every chain. But be careful. If decorators are registered deep in configuration code, it can be hard to know what’s actually running. When debugging, you might not realize there are three wrappers between your controller and the real service.

Common Use Cases in Real Systems

Decorators shine for cross-cutting concerns that don’t belong in core business logic:

  • Logging: Log before and after calls without polluting the service.
  • Validation: Validate inputs before passing to the real implementation.
  • Caching: Check a cache before calling the real service, store results after.
  • Retry / resiliency: Wrap calls with retry logic using Polly or similar.
  • Authorization: Check permissions before allowing the call to proceed.

These concerns are important, but they’re not what the service is about. Keeping them in decorators means the core service stays focused on its actual job.

Common Pitfalls and Code Smells

Excessive decorator chains: If you have seven decorators on a service, something is off. Either the service is doing too much, or you’re over-engineering.

Decorators that mutate input/output: A decorator should add behavior, not change the data flowing through. If a decorator modifies the message before passing it along, that’s surprising and hard to debug.

Leaky abstractions: If a decorator needs to know implementation details of the wrapped service, the abstraction is too thin. Decorators should work with the interface, not peek inside.

Hidden behavior: When decorators are wired up in configuration, it can be hard to trace what’s happening. If a call fails and you don’t realize there’s a retry decorator swallowing exceptions, you’ll waste time debugging.

When Not to Use Decorators

Decorators are not always the right tool.

When behavior is fundamental: If logging is essential to how the service works (audit logging for compliance, for example), maybe it belongs in the service itself.

When a pipeline fits better: If you have a sequence of steps that can short-circuit or branch, a pipeline or chain-of-responsibility might be clearer than nested decorators.

When complexity outweighs flexibility: If you only have one implementation and no plans to add cross-cutting concerns, a decorator adds indirection for no benefit.

Testing Decorated Systems

One of the benefits of decorators is that you can test each piece independently.

Test the core service: Create an instance of the real service and test its behavior directly. No decorators involved.

[Fact]
public void Send_WritesToConsole()
{
    var service = new NotificationService();
    service.Send("Hello");
    // assert on output
}

Test decorators in isolation: Pass a fake or mock as the inner service and verify the decorator does its job.

[Fact]
public void LoggingDecorator_LogsBeforeAndAfter()
{
    var fakeInner = new FakeNotificationService();
    var fakeLogger = new FakeLogger();
    var decorator = new LoggingNotificationDecorator(fakeInner, fakeLogger);

    decorator.Send("Hello");

    Assert.Equal(2, fakeLogger.LogCount);
    Assert.True(fakeInner.WasCalled);
}

Full-chain tests: Only test the full chain when ordering matters or you need to verify integration. These tests are slower and more brittle, so keep them focused.

Practical Guidelines

A few things to keep in mind:

  • Keep decorators small and single-purpose. One decorator, one concern. A LoggingRetryingCachingDecorator is doing too much.
  • Prefer explicit composition over implicit resolution. If someone reads your DI setup, they should be able to see the decorator chain.
  • Make ordering visible. Document or structure your registration so the order is obvious.
  • Treat decorators as infrastructure, not domain logic. They handle plumbing. The real service handles business rules.

The decorator pattern is a solid tool for keeping cross-cutting concerns out of your core code. It’s not complicated, but it does require discipline to use well. Start simple, add decorators when you have a real need, and keep the chains short.

Avatar
Alan P. Barber
Software Developer, Computer Scientist, Scrum Master, & Crohn’s Disease Fighter

I specialize in Software Development with a focus on Architecture and Design.

comments powered by Disqus
Previous

Related