BrandGhost
Decorator Design Pattern in C#: Complete Guide with Examples

Decorator Design Pattern in C#: Complete Guide with Examples

Decorator Design Pattern in C#: Complete Guide with Examples

When you need to add behavior to an object without changing its class, the decorator design pattern in C# is one of the most powerful tools at your disposal. Unlike subclassing, which locks you into a rigid hierarchy at compile time, decorators let you compose behavior at runtime by wrapping objects with new functionality. This makes your code more flexible, testable, and aligned with the Open/Closed Principle.

In this complete guide, we'll walk through everything you need to know about the decorator pattern -- from the core structure and basic implementation to chaining multiple decorators together, integrating with dependency injection in .NET, and avoiding common pitfalls. By the end, you'll have practical C# examples you can adapt to your own projects and a clear understanding of when this structural pattern is the right choice.

What Is the Decorator Design Pattern?

The decorator design pattern is a structural design pattern from the Gang of Four (GoF) catalog that allows you to attach additional responsibilities to an object dynamically. It provides a flexible alternative to subclassing for extending functionality, and it does so without modifying the original object's code.

The pattern works through composition rather than inheritance. A decorator class wraps the original component and implements the same interface. It delegates calls to the wrapped object while adding its own behavior before, after, or around the delegated call. Because the decorator and the component share the same interface, client code doesn't need to know whether it's working with a plain object or a decorated one.

Structurally, the pattern involves four participants. First, there's the Component -- an interface or abstract class that defines the contract both the real object and decorators will follow. Second, the Concrete Component is the original object whose behavior you want to extend. Third, the Decorator base class implements the component interface and holds a reference to a wrapped component. Finally, Concrete Decorators extend the decorator base to add specific behaviors like logging, caching, or validation.

The key insight is that because decorators conform to the same interface as the object they wrap, they are interchangeable with the original component. This means you can stack multiple decorators on top of each other, building up complex behavior from small, focused pieces. Each decorator is responsible for exactly one concern, which keeps individual classes simple and easy to test.

How the Decorator Pattern Works in C#

Let's build a practical example step by step. We'll create a notification service that can be extended with logging and retry behavior using decorators. This is a common real-world scenario that demonstrates the pattern clearly.

Defining the Component Interface

The first step is defining the interface that both the concrete component and all decorators will implement:

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

Creating the Concrete Component

Next, we implement the core functionality. This class handles the actual work of sending a notification:

public class EmailNotificationService : INotificationService
{
    public void Send(string recipient, string message)
    {
        Console.WriteLine(
            $"Sending email to {recipient}: {message}");
    }
}

Building the Abstract Decorator

The abstract decorator implements the same interface and holds a reference to the wrapped component. This base class ensures all decorators follow a consistent delegation pattern:

public abstract class NotificationDecorator
    : INotificationService
{
    private readonly INotificationService _inner;

    protected NotificationDecorator(
        INotificationService inner)
    {
        _inner = inner
            ?? throw new ArgumentNullException(nameof(inner));
    }

    public virtual void Send(string recipient, string message)
    {
        _inner.Send(recipient, message);
    }
}

Notice that the Send method is virtual, so concrete decorators can override it to inject their own behavior while still calling the base implementation to delegate to the wrapped component.

Adding Concrete Decorators

Now we can create decorators that add specific behaviors. Here's a logging decorator that records when notifications are sent:

public class LoggingNotificationDecorator
    : NotificationDecorator
{
    public LoggingNotificationDecorator(
        INotificationService inner)
        : base(inner)
    {
    }

    public override void Send(string recipient, string message)
    {
        Console.WriteLine(
            $"[LOG] Sending notification to {recipient}...");

        base.Send(recipient, message);

        Console.WriteLine(
            $"[LOG] Notification sent to {recipient}.");
    }
}

And here's a retry decorator that attempts the operation multiple times if it fails:

public class RetryNotificationDecorator
    : NotificationDecorator
{
    private readonly int _maxRetries;

    public RetryNotificationDecorator(
        INotificationService inner,
        int maxRetries = 3)
        : base(inner)
    {
        _maxRetries = maxRetries;
    }

    public override void Send(string recipient, string message)
    {
        for (int attempt = 1; attempt <= _maxRetries; attempt++)
        {
            try
            {
                base.Send(recipient, message);
                return;
            }
            catch (Exception ex) when (attempt < _maxRetries)
            {
                Console.WriteLine(
                    $"[RETRY] Attempt {attempt} failed: " +
                    $"{ex.Message}. Retrying...");
            }
        }
    }
}

Putting It All Together

The client code can work with the base interface without knowing about the decorators:

INotificationService service =
    new EmailNotificationService();

service = new LoggingNotificationDecorator(service);
service = new RetryNotificationDecorator(service);

service.Send("[email protected]", "Welcome aboard!");

The variable service still has the type INotificationService, but it now has logging and retry behavior layered on top of the original email sending logic. This is the power of the decorator pattern -- you build complex behavior by composing simple, focused pieces.

Building Decorator Chains

One of the most practical aspects of the decorator pattern is the ability to chain multiple decorators together. Each decorator wraps the previous one, forming a pipeline of behavior. The order in which you apply decorators matters because each one executes in sequence.

Consider this example where we add a validation decorator to our chain:

public class ValidationNotificationDecorator
    : NotificationDecorator
{
    public ValidationNotificationDecorator(
        INotificationService inner)
        : base(inner)
    {
    }

    public override void Send(string recipient, string message)
    {
        if (string.IsNullOrWhiteSpace(recipient))
        {
            throw new ArgumentException(
                "Recipient cannot be empty.",
                nameof(recipient));
        }

        if (string.IsNullOrWhiteSpace(message))
        {
            throw new ArgumentException(
                "Message cannot be empty.",
                nameof(message));
        }

        base.Send(recipient, message);
    }
}

// Building the chain with order in mind
INotificationService service =
    new EmailNotificationService();

service = new ValidationNotificationDecorator(service);
service = new RetryNotificationDecorator(service);
service = new LoggingNotificationDecorator(service);

service.Send("[email protected]", "Your order shipped!");

In this chain, logging happens first (outermost), then retry logic wraps the validation, and validation runs before the actual send. Think of it like layers of an onion -- the outermost decorator executes first, then passes control inward until the core component handles the actual work. The execution then unwinds back through the decorators in reverse order.

The order matters in practical ways. If you placed logging inside the retry decorator, you'd get log entries for each retry attempt rather than a single log entry wrapping all attempts. Similarly, validation should generally run before retry logic -- there's no point retrying a request that has an invalid recipient.

This composability is what sets the decorator pattern apart from inheritance-based approaches. With inheritance, you'd need a class for every combination of behaviors -- LoggingRetryValidatingEmailService, RetryLoggingEmailService, and so on. With decorators, you mix and match freely.

Decorator Pattern with Dependency Injection in .NET

In real-world .NET applications, you'll often want to wire up decorators through the dependency injection container. This is where the pattern truly shines, because it integrates naturally with inversion of control.

Manual Registration with IServiceCollection

You can register decorators manually using the built-in IServiceCollection by wrapping factory delegates:

using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection();

services.AddSingleton<EmailNotificationService>();

services.AddSingleton<INotificationService>(sp =>
{
    INotificationService inner =
        sp.GetRequiredService<EmailNotificationService>();

    inner = new ValidationNotificationDecorator(inner);
    inner = new RetryNotificationDecorator(inner);
    inner = new LoggingNotificationDecorator(inner);

    return inner;
});

var provider = services.BuildServiceProvider();
var service = provider
    .GetRequiredService<INotificationService>();

service.Send("[email protected]", "DI works!");

This approach works but can become verbose when you have many decorators or need to register them across different services.

Simplified Registration with Scrutor

Scrutor is a popular library that adds decorator support directly to IServiceCollection through its Decorate<TInterface, TDecorator>() extension method. This makes registration much cleaner:

using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection();

services.AddSingleton<INotificationService,
    EmailNotificationService>();

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

var provider = services.BuildServiceProvider();
var service = provider
    .GetRequiredService<INotificationService>();

service.Send("[email protected]", "Scrutor makes it easy!");

Each call to Decorate wraps the previous registration, so the order of Decorate calls determines the decorator chain. The last decorator registered becomes the outermost layer. This approach is clean, declarative, and scales well across large applications.

Common Use Cases for the Decorator Pattern

The decorator design pattern in C# appears in many real-world scenarios. Understanding these common use cases helps you recognize opportunities to apply the pattern in your own codebase.

Logging and auditing is perhaps the most common decorator use case. By wrapping a service with a logging decorator, you can capture method calls, parameters, and results without polluting the core business logic. This keeps your service classes focused on their primary responsibility while still providing observability.

Caching is another natural fit. A caching decorator can check a cache before delegating to the wrapped component, returning cached results when available and storing new results for future calls. This pattern is especially useful for expensive database queries or API calls where the underlying data doesn't change frequently.

Validation and authorization decorators run checks before allowing the request to pass through to the inner component. This is conceptually similar to how middleware works in ASP.NET Core -- each layer in the pipeline can inspect, modify, or reject the request.

Retry and circuit breaker logic wraps unreliable operations with fault tolerance. Instead of embedding retry loops inside every service class, a single retry decorator handles this concern across any service that implements the same interface.

Performance monitoring decorators measure execution time and report metrics without modifying the underlying service. You can add or remove timing instrumentation simply by including or excluding the decorator from the chain.

These use cases all share a common theme: the decorator adds a cross-cutting concern without modifying the core component. This is what makes the pattern so valuable for building maintainable systems -- each concern lives in its own class, and you compose them as needed.

Best Practices and Pitfalls

The decorator pattern is straightforward in concept, but there are several best practices and common pitfalls to keep in mind as you apply it in production code.

Keep each decorator focused on a single concern. A decorator that handles logging, caching, and validation all at once defeats the purpose of the pattern. The whole point is to separate cross-cutting concerns into composable units. If your decorator is growing large, it's a sign you should split it into multiple decorators.

Be mindful of decorator chain depth. While the pattern supports arbitrary nesting, deep chains can make debugging harder. When an exception occurs five decorators deep, tracing the call stack becomes tedious. Keep chains reasonable -- typically three to five decorators is practical for most scenarios.

Consider thread safety. If decorators maintain state, like a counter or a cache, ensure that state is thread-safe. A caching decorator used in a web application will be accessed by multiple threads simultaneously. Use ConcurrentDictionary or other thread-safe collections when needed.

Follow interface segregation. The decorator pattern works best with focused, narrow interfaces. If your interface has twenty methods, every decorator must implement all twenty, even if the decorator only cares about one. Prefer small interfaces, and if you find yourself writing pass-through code for most methods, that's a signal the interface is too broad.

Avoid modifying the wrapped component's behavior unexpectedly. A decorator should enhance or guard behavior, not silently change it. For example, a "caching" decorator that silently returns stale data without any indication could lead to subtle bugs. Be transparent about what each decorator does.

Use the strategy pattern for algorithm variation instead. The decorator pattern is for adding behavior around an operation. If you need to swap out the core algorithm entirely, the strategy pattern is a better fit. The two patterns complement each other but solve different problems.

Frequently Asked Questions

What is the difference between the decorator pattern and inheritance?

Inheritance extends behavior at compile time by creating subclass hierarchies. The decorator pattern extends behavior at runtime through composition. With inheritance, you need a new class for every combination of features, which leads to class explosion. Decorators let you mix and match behaviors dynamically without creating a subclass for each combination. Additionally, C# only supports single inheritance, so you can't inherit from multiple base classes -- but you can stack as many decorators as you need.

When should I use the decorator pattern instead of middleware?

Middleware in ASP.NET Core operates at the HTTP request pipeline level and is specific to web request processing. The decorator pattern works at the service or component level and can be applied anywhere in your application, not just in web contexts. Use middleware for HTTP-level concerns like authentication, CORS, and request logging. Use decorators for service-level cross-cutting concerns like caching, retry logic, and validation that apply regardless of how the service is invoked.

How does the decorator pattern relate to other structural patterns?

The decorator pattern is one of several structural patterns in the GoF catalog. It's closely related to the composite pattern, which also uses composition and shared interfaces, but the composite pattern is about building tree structures rather than adding behavior. The facade pattern simplifies complex subsystems behind a unified interface, while the decorator pattern extends a single component's behavior. You can explore all of these in the complete list of design patterns.

Can I use the decorator pattern with Scrutor in .NET?

Yes! Scrutor provides the Decorate<TInterface, TDecorator>() extension method that integrates directly with IServiceCollection. This makes registering decorator chains clean and declarative. Each Decorate call wraps the previous registration, so the order of calls determines the chain structure. This is the most streamlined approach for wiring up decorators in .NET applications that use the built-in DI container.

How do I test classes that use the decorator pattern?

Testing is one of the decorator pattern's strengths. Because each decorator wraps an interface, you can test each decorator in isolation by passing in a mock or stub as the inner component. This lets you verify that the logging decorator logs correctly, the retry decorator retries on failure, and the validation decorator rejects invalid input -- all independently. You don't need the full chain to test any individual decorator, which keeps your tests fast and focused.

Is the decorator pattern the same as the wrapper pattern?

The terms are sometimes used interchangeably, but "wrapper" is a more general concept. The decorator pattern is a specific form of wrapping where the wrapper and the wrapped object share the same interface, allowing transparent substitution. Not all wrappers follow this constraint -- an adapter, for example, wraps an object to change its interface rather than add behavior. The shared interface is what makes the decorator pattern uniquely composable.

What are alternatives to the decorator pattern in C#?

Several alternatives exist depending on your needs. For plugin-based extensibility, where you want to load behavior dynamically at runtime, a plugin architecture may be more appropriate. The strategy pattern is better when you need to swap entire algorithms rather than layer behavior. Aspect-oriented programming (AOP) with tools like PostSharp or source generators can handle cross-cutting concerns at the IL level. Finally, the builder pattern is useful when you need to construct complex objects step by step rather than wrap existing ones with additional behavior.

Wrapping Up the Decorator Design Pattern in C#

The decorator design pattern in C# is a fundamental structural pattern that every C# developer should have in their toolkit. By wrapping objects with focused, composable layers of behavior, you can extend functionality without modifying existing classes -- keeping your code aligned with the Open/Closed Principle and making it easier to maintain as requirements evolve.

The pattern's real power shows up when combined with dependency injection. Whether you're manually composing decorator chains or using libraries like Scrutor, the decorator pattern integrates naturally with how modern .NET applications are structured. Start by identifying cross-cutting concerns in your codebase -- logging, caching, validation, retry logic -- and consider whether a decorator would let you separate those concerns from your core business logic.

Keep your decorators small, focused, and independently testable. Stack them in a deliberate order that matches your application's needs. And remember that the decorator pattern is just one tool among many -- reach for it when you need to layer behavior onto existing services, and consider the strategy pattern or other approaches when the problem calls for something different.

Decorator Pattern in C# with Needlr: Adding Cross-Cutting Concerns

Learn how to implement the decorator pattern in C# using Needlr's automatic decorator discovery, including the DecoratorFor attribute and manual decorator wiring.

Decorator Pattern - How To Master It In C# Using Autofac

Want to know how the decorator pattern works? Let's check out an Autofac example in C# where we can get the decorator pattern with Autofac working!

How To Implement The Decorator Pattern With Autofac

Whether it's an ASP.NET Core or console app, we can use the decorator pattern with Autofac for powerful results! Let's explore an Autofac example for each!

An error has occurred. This application may no longer respond until reloaded. Reload