BrandGhost
How to Implement Decorator Pattern in C#: Step-by-Step Guide

How to Implement Decorator Pattern in C#: Step-by-Step Guide

How to Implement Decorator Pattern in C#: Step-by-Step Guide

Adding behavior to existing objects without cracking open their source code is one of the most practical skills you can develop as a C# developer. The decorator pattern makes this possible by wrapping objects with layers of functionality -- and the best part is that each layer is independent, testable, and composable. If you want to implement decorator pattern C# style, this guide will walk you through it from scratch.

Whether you're adding logging, validation, or retry logic to a service, the decorator pattern keeps your code aligned with the Open/Closed Principle. You extend behavior by composition rather than inheritance, which avoids the rigid class hierarchies that make codebases brittle over time. If you're new to design patterns in general, take a look at The Big List of Design Patterns for broader context on where the decorator pattern fits.

In this step-by-step guide, we'll build a data processing pipeline by implementing the decorator pattern in C# from scratch. By the end, you'll have a working example that demonstrates interfaces, abstract decorators, concrete decorator implementations, chaining, and dependency injection registration.

Prerequisites

Before diving in, make sure you're comfortable with these fundamentals:

  • C# basics: Classes, interfaces, abstract classes, and constructors. If you need a refresher on interfaces versus abstract classes, the code examples below will reinforce those concepts.
  • Composition over inheritance: The decorator pattern relies heavily on composition -- wrapping one object inside another rather than extending a base class. Understanding this principle will make the pattern feel natural.
  • Dependency injection awareness: The final step covers wiring decorators through IServiceCollection. You don't need to be an expert, but familiarity with registering services helps.
  • .NET 8 or later: The code examples target modern C# syntax. Any recent .NET SDK will work.

Step 1: Define the Component Interface

The first step to implement decorator pattern in C# is defining a contract. This interface defines what both the real component and all decorators must be able to do. Think of it as the agreement that makes the entire pattern work -- because decorators and concrete components share the same interface, client code never needs to know which one it's talking to.

For our example, we'll build an IDataProcessor that processes data records:

public interface IDataProcessor
{
    DataResult Process(DataRecord record);
}

public sealed class DataRecord
{
    public string Id { get; }

    public string Payload { get; }

    public DataRecord(string id, string payload)
    {
        Id = id;
        Payload = payload;
    }
}

public sealed class DataResult
{
    public string RecordId { get; }

    public bool Success { get; }

    public string Output { get; }

    public DataResult(
        string recordId,
        bool success,
        string output)
    {
        RecordId = recordId;
        Success = success;
        Output = output;
    }
}

The interface is deliberately simple -- one method, clear input and output types. Keeping the interface narrow is important because every decorator you write must implement every method on the interface. A focused contract means less boilerplate in each decorator.

Step 2: Create the Concrete Component

The concrete component is the class that does the real work when you implement the decorator pattern in C#. All the decorators we build later will wrap this core implementation. In a real application, this might call a database, invoke an API, or run a transformation pipeline.

public class DefaultDataProcessor : IDataProcessor
{
    public DataResult Process(DataRecord record)
    {
        string transformed = record.Payload.Trim().ToUpperInvariant();

        Console.WriteLine(
            $"[DefaultDataProcessor] Processed record " +
            $"'{record.Id}': {transformed}");

        return new DataResult(
            record.Id,
            success: true,
            output: transformed);
    }
}

Nothing fancy here. The DefaultDataProcessor transforms the payload and returns a result. This is the object we'll wrap with decorators to add cross-cutting concerns without touching this class again.

Step 3: Create the Abstract Decorator Base

The abstract decorator is the backbone when you implement the decorator pattern in C#. It implements the same IDataProcessor interface and holds a reference to the inner component it wraps. By making the method virtual, we allow concrete decorators to override and add their own behavior while still delegating to the wrapped component.

public abstract class DataProcessorDecorator : IDataProcessor
{
    private readonly IDataProcessor _inner;

    protected DataProcessorDecorator(IDataProcessor inner)
    {
        _inner = inner
            ?? throw new ArgumentNullException(nameof(inner));
    }

    public virtual DataResult Process(DataRecord record)
    {
        return _inner.Process(record);
    }
}

A few things to notice here:

  • The constructor requires a non-null IDataProcessor. This is the object being wrapped.
  • The Process method is virtual, so subclasses can override it to inject behavior before, after, or around the call to the inner component.
  • The default implementation simply delegates to the inner component -- decorators only need to override what they care about.

This abstract base eliminates repetitive delegation code. Without it, every decorator would need to manually store and call the inner component, which is error-prone and noisy. This approach is similar to how inheritance lets you share behavior across related classes -- but here, we're combining inheritance (for the decorator hierarchy) with composition (for wrapping the inner component).

Step 4: Build Your First Decorator -- Logging

Now let's create a concrete decorator that adds logging around the processing call. This is one of the most common use cases when developers implement decorator pattern in C# -- adding observability without polluting your core business logic.

using System.Diagnostics;

public class LoggingDataProcessorDecorator
    : DataProcessorDecorator
{
    public LoggingDataProcessorDecorator(
        IDataProcessor inner)
        : base(inner)
    {
    }

    public override DataResult Process(DataRecord record)
    {
        Console.WriteLine(
            $"[LOG] Starting processing for " +
            $"record '{record.Id}'...");

        var stopwatch = Stopwatch.StartNew();
        DataResult result = base.Process(record);
        stopwatch.Stop();

        Console.WriteLine(
            $"[LOG] Finished record '{record.Id}' " +
            $"in {stopwatch.ElapsedMilliseconds}ms. " +
            $"Success: {result.Success}");

        return result;
    }
}

The key line is base.Process(record). This delegates to the abstract decorator's Process method, which in turn calls the inner component. The logging decorator wraps that call with timing and status output. The concrete component doesn't know it's being logged, and the logging decorator doesn't know what the inner component actually does. That separation is what makes the pattern powerful.

Step 5: Add More Decorators -- Validation and Retry

One decorator is useful, but the real value shows up when you implement decorator pattern in C# with multiple decorators that each handle a single concern. Let's add two more.

Validation Decorator

This decorator checks that the incoming data record is valid before allowing it to pass through:

public class ValidationDataProcessorDecorator
    : DataProcessorDecorator
{
    public ValidationDataProcessorDecorator(
        IDataProcessor inner)
        : base(inner)
    {
    }

    public override DataResult Process(DataRecord record)
    {
        if (string.IsNullOrWhiteSpace(record.Id))
        {
            throw new ArgumentException(
                "Record ID cannot be empty.",
                nameof(record));
        }

        if (string.IsNullOrWhiteSpace(record.Payload))
        {
            return new DataResult(
                record.Id,
                success: false,
                output: "Validation failed: empty payload.");
        }

        return base.Process(record);
    }
}

Notice that validation can short-circuit the pipeline. If the payload is empty, it returns a failure result without ever calling the inner component. This is a powerful pattern -- decorators can guard behavior, not just augment it.

Retry Decorator

This decorator wraps the inner call with retry logic for transient failures:

public class RetryDataProcessorDecorator
    : DataProcessorDecorator
{
    private readonly int _maxRetries;

    public RetryDataProcessorDecorator(
        IDataProcessor inner,
        int maxRetries = 3)
        : base(inner)
    {
        _maxRetries = maxRetries;
    }

    public override DataResult Process(DataRecord record)
    {
        for (int attempt = 1; attempt <= _maxRetries; attempt++)
        {
            try
            {
                return base.Process(record);
            }
            catch (Exception ex) when (attempt < _maxRetries)
            {
                Console.WriteLine(
                    $"[RETRY] Attempt {attempt} failed " +
                    $"for record '{record.Id}': " +
                    $"{ex.Message}. Retrying...");
            }
        }

        return new DataResult(
            record.Id,
            success: false,
            output: "All retry attempts exhausted.");
    }
}

Each decorator is responsible for exactly one cross-cutting concern. Logging logs. Validation validates. Retry retries. This keeps each class small, testable, and easy to reason about. If you've worked with the strategy pattern, you'll notice a similar emphasis on single-responsibility -- but where strategy swaps entire algorithms, the decorator pattern layers behavior around an existing one.

Step 6: Chain Decorators Together

This is where the real power becomes clear when you implement the decorator pattern in C#. You compose multiple decorators by wrapping them in sequence. Each decorator wraps the previous one, forming a pipeline that processes calls from the outside in.

using System;
using System.Diagnostics;

// Build the decorator chain
IDataProcessor processor =
    new DefaultDataProcessor();

processor = new ValidationDataProcessorDecorator(processor);
processor = new RetryDataProcessorDecorator(processor, maxRetries: 3);
processor = new LoggingDataProcessorDecorator(processor);

// Use it -- client code only sees IDataProcessor
var record = new DataRecord("REC-001", "  hello world  ");
DataResult result = processor.Process(record);

Console.WriteLine(
    $"Final result: {result.RecordId} -> " +
    $"{result.Output} (Success: {result.Success})");

Running this produces output like:

[LOG] Starting processing for record 'REC-001'...
[DefaultDataProcessor] Processed record 'REC-001': HELLO WORLD
[LOG] Finished record 'REC-001' in 2ms. Success: True
Final result: REC-001 -> HELLO WORLD (Success: True)

The order of wrapping matters. In this chain, the outermost decorator is LoggingDataProcessorDecorator, which executes first. It then calls into RetryDataProcessorDecorator, which calls ValidationDataProcessorDecorator, which finally calls DefaultDataProcessor. This is conceptually similar to how middleware pipelines work in ASP.NET Core -- each layer wraps the next.

If you placed validation on the outside instead, invalid records would be rejected before logging could capture the attempt. Think about what makes sense for your use case and order accordingly.

Step 7: Register with Dependency Injection

In production .NET applications, you'll wire up decorators through the DI container rather than constructing them manually. This is the final step to implement the decorator pattern in C# for real-world use. It integrates with the inversion of control principle and keeps your composition root clean.

Manual Registration with IServiceCollection

You can use factory delegates to build the chain inside your DI registration:

using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection();

services.AddSingleton<DefaultDataProcessor>();

services.AddSingleton<IDataProcessor>(sp =>
{
    IDataProcessor inner =
        sp.GetRequiredService<DefaultDataProcessor>();

    inner = new ValidationDataProcessorDecorator(inner);
    inner = new RetryDataProcessorDecorator(inner);
    inner = new LoggingDataProcessorDecorator(inner);

    return inner;
});

var provider = services.BuildServiceProvider();
var processor = provider
    .GetRequiredService<IDataProcessor>();

var record = new DataRecord("REC-042", "test payload");
DataResult result = processor.Process(record);

Console.WriteLine(
    $"DI Result: {result.Output} " +
    $"(Success: {result.Success})");

This works, but the factory delegate can get verbose with many decorators. For a cleaner approach, consider using Scrutor, which adds a Decorate extension method to IServiceCollection:

using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection();

services.AddSingleton<IDataProcessor, DefaultDataProcessor>();

services.Decorate<IDataProcessor,
    ValidationDataProcessorDecorator>();
services.Decorate<IDataProcessor,
    RetryDataProcessorDecorator>();
services.Decorate<IDataProcessor,
    LoggingDataProcessorDecorator>();

var provider = services.BuildServiceProvider();
var processor = provider
    .GetRequiredService<IDataProcessor>();

Each Decorate call wraps the previous registration, so the last decorator registered becomes the outermost layer. This is declarative, readable, and scales well as your application grows.

Common Mistakes to Avoid

Even though the decorator pattern is straightforward, there are several mistakes that trip up developers when they first implement the decorator pattern in C#.

Forgetting to call base.Process(): The most common mistake. If a concrete decorator overrides the method but forgets to call base.Process(record), the inner component never executes. The decorator silently swallows the call, and you get no output or a confusing bug. Always ensure your override delegates to the base unless you're intentionally short-circuiting (like the validation example above).

Breaking the interface contract: A decorator should honor the same contract as the component it wraps. If the original Process method never returns null, your decorator shouldn't introduce null returns. Consumers of IDataProcessor rely on the contract being consistent regardless of which decorators are in the chain.

Making the interface too broad: If your component interface has ten methods, every decorator has to implement all ten. This creates boilerplate and increases the chance of bugs. Keep interfaces small and focused. If you find yourself writing pass-through methods, that's a signal the interface needs narrowing.

Ignoring decorator order: The order you stack decorators matters. Logging outside of retry captures one log entry per request. Logging inside of retry captures a log entry per attempt. Neither is wrong, but you need to be intentional about what you want.

Mixing decorator and strategy pattern responsibilities: Decorators add behavior around an operation. If you need to swap the core algorithm entirely, use the strategy pattern instead. The two patterns complement each other, but confusing them leads to decorators that do too much.

Frequently Asked Questions

What is the decorator pattern in C# and why should I implement it?

The decorator pattern is a structural design pattern that lets you attach additional behavior to an object at runtime by wrapping it in a decorator class. You should implement the decorator pattern in C# when you need to add cross-cutting concerns like logging, validation, or caching without modifying the original class. It follows the Open/Closed Principle -- your classes are open for extension but closed for modification.

How is the decorator pattern different from using inheritance to extend behavior?

Inheritance locks you into a fixed class hierarchy at compile time. If you need logging and retry, you'd need a LoggingRetryDataProcessor subclass. Add caching and you need even more subclasses for every combination. When you implement decorator pattern in C#, you avoid this explosion by composing behaviors at runtime -- stack any combination of decorators without creating new subclasses.

Do I always need an abstract decorator base class?

Not strictly. You can have each concrete decorator implement the interface directly and hold its own reference to the inner component. However, the abstract base eliminates repetitive delegation code and reduces the chance of forgetting to delegate a method call. For anything beyond a single decorator, the abstract base pays for itself quickly.

How do I decide the order of decorators in the chain?

Think about what should execute first. Validation typically goes closest to the core component so invalid data is caught early. Retry wraps around operations that might fail transiently. Logging usually goes on the outside so you capture a single entry per external call. The outermost decorator executes first and the innermost executes last before hitting the core component.

Can I use the decorator pattern with ASP.NET Core middleware?

The concepts are similar -- both are pipeline patterns where each layer wraps the next. However, ASP.NET Core middleware operates at the HTTP request level, while the decorator pattern works at the service or component level. You can use decorators inside middleware-based applications to add behavior to individual services that the middleware pipeline invokes.

How do I test a decorator in isolation?

Because each decorator depends on the IDataProcessor interface, you can pass a mock or stub as the inner component. This lets you verify that your logging decorator logs correctly, your validation decorator rejects bad input, and your retry decorator retries on failure -- all independently, without needing the full chain. This isolation is one of the decorator pattern's biggest strengths for testability.

What are alternatives to the decorator pattern for adding cross-cutting concerns?

Aspect-oriented programming (AOP) tools can inject behavior at the IL level without explicit wrapper classes. The facade pattern simplifies complex subsystems but doesn't add layered behavior. Middleware handles HTTP-level concerns. Source generators can add behavior at compile time. Each has trade-offs -- decorators give you explicit, debuggable, composable behavior without magic, which is why they remain one of the most popular approaches.

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 Abstract Factory Pattern in C#: Step-by-Step Guide

Learn how to implement Abstract Factory pattern in C# with a complete step-by-step guide. Includes code examples, best practices, and common pitfalls to avoid.

Decorator Design Pattern in C#: Complete Guide with Examples

Master the decorator design pattern in C# with practical code examples, best practices, and real-world use cases for flexible object extension.

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