DEV Community

Cover image for The Decorator Pattern in Modern C#
Gael Fraiteur for PostSharp Technologies

Posted on • Originally published at blog.postsharp.net

The Decorator Pattern in Modern C#

The Decorator design pattern allows software developers to extend the functionalities of a component without altering its code. This article explores the primary techniques for implementing the decorator pattern in modern .NET while adhering to the Single Responsibility Principle (SRP) and avoiding boilerplate code.

When to use a Decorator pattern in C

The Decorator pattern is useful when you want to add behavior to an existing component but either cannot or do not want to modify the source code. This is typically done to adhere to the single responsibility principle (SRP) to keep our code clean, readable, and maintainable.

Some real-world use cases for the decorator design pattern include:

  • Execution policies, such as exception handling, retrying, or caching, which help improve the performance and reliability of your apps.
  • Observability, for instance, by adding logging to all calls to an external component.
  • User Interface, like adding a scrollbar to a large textbox. Another example is the Adorner concept in WPF.
  • Streams, with features such as buffering, encryption, or compression.

What is the Decorator pattern

A Decorator is essentially a wrapper that implements the same contract as the entity it’s wrapping. We are intentionally using the vague term contract. As we will see in this article, it can mean two things: a C# interface if we implement a type decorator, or a method signature if we implement a method decorator. In both cases, the caller does not need to know that it is talking to a decorator rather than the final implementation. The pattern is recursive: we can add a decorator to a decorator, creating a chain of responsibility.

For instance, instead of just calling an unreliable service, we may want to retry a couple of times upon transient failure, and finally assign a unique ID to each exception, log it, and wrap the exception. We can represent the chain as follows:

Control flow diagram

In this article, we will explore two kinds of decorators: type decorators and method decorators.

The classic Type Decorator pattern

The classic type decorator pattern is a purely object-oriented variant of the decorator pattern that relies on type interfaces.

To illustrate the idea, let’s say we want to build a simple messaging app. We’d need a component that handles sending and receiving messages. This component implements the IMessenger interface and is implemented in a third-party library.

public interface IMessenger
{
    void Send( Message message );

    public Message Receive();
}
Enter fullscreen mode Exit fullscreen mode

We are using the IMessenger service from a client class:

public class Client( IMessenger messenger )
{
    public void Greet()
    {
        messenger.Send( new Message( "Hello, world" ) );
        Console.WriteLine( "--> " + messenger.Receive().Text );
    }
}
Enter fullscreen mode Exit fullscreen mode

We are instantiating the Client class from Program.cs:

var messenger = new Messenger();
var client = new Client( messenger );
client.Greet();
Enter fullscreen mode Exit fullscreen mode

That all works nicely on our development environment. However, as soon as we move things to production, we realize that the messenger service is unreliable and occasionally causes our app to crash. Since we don’t own the source code of the IMessenger implementation, we cannot simply add the logic we need to each method.

How does the Decorator pattern help us tackle this problem?

Take a look at our design using the type decorator pattern in the class diagram below. Along with the decorators for error handling and retrying, we’ve introduced the MessengerDecorator abstract class that holds the wrapped IMessenger object, making it easier to implement individual decorators.

Class diagram of the classic decorator pattern

Here is the implementation of the ExceptionReportingMessenger class:

public class ExceptionReportingMessenger : MessengerDecorator
{
    private readonly IExceptionReportingService _reportingService;

    public ExceptionReportingMessenger( 
      IMessenger underlying, 
      IExceptionReportingService reportingService ) :
      base( underlying )
    {
        this._reportingService = reportingService;
    }

    public override void Send( Message message )
    {
        try
        {
            this.Underlying.Send( message );
        }
        catch ( Exception e )
        {
            this._reportingService.ReportException( 
                                           "Failed to send message", e );

            throw;
        }
    }

    public override Message Receive()
    {
        try
        {
            return this.Underlying.Receive();
        }
        catch ( Exception e )
        {
            this._reportingService.ReportException( 
"Failed to receive message", e );

            throw;
        }
    }

}
Enter fullscreen mode Exit fullscreen mode

The RetryingMessenger messenger is very similar.

Now, instead of passing the original Messenger component to the Client class, we are wrapping the Messenger into RetryingMessenger, then into an ExceptionReportingMessenger. This is finally the ExceptionReportingMessenger that we pass to the Client

var originalMessenger = new Messenger();

var retryingMessenger = new ExceptionReportingMessenger(
    new RetryingMessenger( originalMessenger ),
    new ExceptionReportingService() );

var clientUsingDecorator = new Client( retryingMessenger );
clientUsingDecorator.Greet();
Enter fullscreen mode Exit fullscreen mode

When the program calls Client.Greet, the control flow is the following:

Control flow diagram

Using type decorators with dependency injection

Obviously, in any modern C# application, you would not instantiate the components manually as in the examples above, but you would let dependency injection do the job.

If you are using .NET Core’s IServiceCollection, there is a nice library called Scrutor that can easily wrap a service with a decorator.

For example, this is how to apply the decorators by using Scrutor. Note the calls to the Decorate methods: they are defined by Scrutor.

var services = new ServiceCollection()
    .AddSingleton<IExceptionReportingService, ExceptionReportingService>()
    .AddSingleton<IMessenger, Messenger>()
    .AddSingleton<Client>()
    .Decorate<IMessenger, RetryingMessenger>()
    .Decorate<IMessenger, ExceptionReportingMessenger>()
    .BuildServiceProvider();

var client = services.GetRequiredService<Client>();
client.Greet();
Enter fullscreen mode Exit fullscreen mode

Many dependency injection frameworks have built-in support for decorators. See for instance how Autofac handles this problem,

The Abstract Type Decorator pattern

At first glance, our solution design seems perfect. But when we dig into the implementation of the decorators, we notice that the exception handling would be duplicated across other methods. Here, we’re violating the Don’t Repeat Yourself principle. The code is now harder to maintain than before because any change to the error handling must be made in each method of ExceptionReportingMessenger and any decorator of another type decorator.

We will now see how to improve the Type Decorator pattern to make the decorator logic more reusable.

Let’s use the word policy to designate the logic that we wrap a method call with. Policies can be abstracted away and encapsulated in a reusable way. In the following diagram, we have represented policies as an interface.

Class diagram of the abstract decorator pattern

Here is the exception-reporting policy:

public class ReportExceptionPolicy( 
   IExceptionReportingService reportingService ) : IPolicy
{
    public T Invoke<T>( Func<T> func )
    {
        try
        {
            return func();
        }
        catch ( Exception e )
        {
            reportingService.ReportException( 
                                "Failed to send message", e );

            throw;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

We then define an AbstractDecorator, an abstract class that can be used as a base for any decorator:

public abstract class AbstractDecorator( IPolicy policy )
{
    protected T Invoke<T>( Func<T> func ) => policy.Invoke( func );

    protected void Invoke( Action action )
        => policy.Invoke<object?>(
            () =>
            {
                action();

                return null!;
            } );
}
Enter fullscreen mode Exit fullscreen mode

In practice, you’ll also need to implement async versions of the Invoke methods in both IPolicy and AbstractDecorators.

With this setup, all the MessengerDecorator has to do is wrap method implementations with calls to the Invoke methods of AbstractDecorator:

public class MessengerDecorator( IMessenger underlying, IPolicy policy ) :
    AbstractDecorator( policy ), IMessenger
{
    public void Send( Message message ) =>
 this.Invoke( () => underlying.Send( message ) );

    public Message Receive() => this.Invoke( underlying.Receive );
}
Enter fullscreen mode Exit fullscreen mode

Note that this decorator is now abstracted from any policy. The only repetitive code is now in calling the Invoke method.

Finally, we wire the service collection using Scrutor’s Decorate method by supplying one of the Policies to the MessengerDecorator class:

var services = new ServiceCollection()
    .AddSingleton<IExceptionReportingService, ExceptionReportingService>()
    .AddSingleton<IMessenger, Messenger>()
    .Decorate<IMessenger>(
        ( inner, _ ) => new MessengerDecorator(
            inner,
            new RetryPolicy() ) )
    .Decorate<IMessenger>(
        ( inner, serviceProvider ) => new MessengerDecorator(
            inner,
            new ReportExceptionPolicy( serviceProvider.GetRequiredService<IExceptionReportingService>() ) ) )
    .BuildServiceProvider();

var client = services.GetRequiredService<Client>();
client.Greet();
Enter fullscreen mode Exit fullscreen mode

We’ve now consolidated error-handling logic in one place.

The control flow now becomes:

Control flow diagram

Generating Type Decorators automatically

There’s still repetitive code in the MessengerDecorator class. Arguably, MessengerDecorator is purely boilerplate and should ideally be removed from your codebase. There are two ways to generate this class:

  • At run time, using an approach known as dynamic proxies, or
  • At compile time, using source generators.

In this article, we will only explore the first solution.

The principle behind dynamic proxies is to generate the decorator class at run time, when the application initializes. Among the few libraries that implement this feature, the most popular is Castle DynamicProxy. The concept of policy developed earlier maps to Castle’s IInterceptor interface. Here is the implementation of the retry policy as a Castle interceptor. Notice the similarity to the RetryPolicy class in the example above.

internal class RetryInterceptor( 
                     int retryAttempts = 3, double retryDelay = 1000 ) 
 : IInterceptor
{
    public void Intercept( IInvocation invocation )
    {
        for ( var i = 0;; i++ )
        {
            try
            {
                invocation.Proceed();
            }
            catch ( Exception ) when ( i < retryAttempts )
            {
                var delay = retryDelay * Math.Pow( 2, i );

                Console.WriteLine(
                    "Failed to receive message. " +
                    $"Retrying in {delay / 1000} seconds... " +
                    $"({i + 1}/{retryAttempts})" );

                Thread.Sleep( (int) delay );
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

As promised, there’s no longer a need for any decorator code since Castle has implemented it.

We can now proceed to the startup sequence of our application. We will need a ProxyGenerator:

var proxyGenerator = new ProxyGenerator();
Enter fullscreen mode Exit fullscreen mode

We can now use the proxyGenerator.CreateInterfaceProxyWithTarget method to create the proxy class, and supply the two interceptors implementing our policies:

var services = new ServiceCollection()
    .AddSingleton<IExceptionReportingService, ExceptionReportingService>()
    .AddSingleton<IMessenger, Messenger>()
    .Decorate<IMessenger>(
        ( inner, _ ) => new MessengerDecorator(
            inner,
            new RetryPolicy() ) )
    .Decorate<IMessenger>(
        ( inner, serviceProvider ) => new MessengerDecorator(
            inner,
            new ReportExceptionPolicy( serviceProvider.GetRequiredService<IExceptionReportingService>() ) ) )
    .BuildServiceProvider();

var client = services.GetRequiredService<Client>();
client.Greet();
Enter fullscreen mode Exit fullscreen mode

The Method Decorator Pattern

So far, we’ve discussed techniques that help replace a type with another type implementing the same interface, but providing additional services. The principal benefits of this approach are:

  • It’s purely object-oriented,
  • It works with code you don’t own,
  • It’s composable at run time.

However, there’s a significant drawback: it only works if you can inject yourself into the communication between the caller and the service – typically through an interface, although the same could be achieved using abstract or virtual methods. If you appreciate the benefits of being able to decorate a method with any behavior, it’s a pity to have to limit yourself so much. Even worse: you may be tempted to split your application into more smaller components to benefit from decorators. This is a case of framework dictatorship and it should be avoided.

An alternative to the type decorator pattern is the method decorator. As its name suggests, method decorators target a single method and not the entire type. Method decorators are commonly used in dynamic languages such as Python. They aren’t directly supported by C#, but some toolkits like Metalama make it possible.

The idea of C# method decorators is to move the logic of the policy to a special kind of custom attribute called an aspect, which could be compared to code templates. Unlike other custom attributes, aspects are applied to the code during compilation. Since this approach is compile-time, we’re not limited to the limitations of the .NET runtime, namely we’re not limited to virtual or interface methods, but we can intercept anything (including static private fields, if you ask).

Here is the Metalama version of the retry policy:

internal class RetryAttribute : OverrideMethodAspect
{
    public int Attempts { get; set; } = 3;

    public double Delay { get; set; } = 1000;

    public override dynamic? OverrideMethod()
    {
        for ( var i = 0;; i++ )
        {
            try
            {
                return meta.Proceed();
            }
            catch ( Exception e ) when ( i < this.Attempts )
            {
                var delay = this.Delay * Math.Pow( 2, i + 1 );

                Console.WriteLine(
$"Method {meta.Target.Method.DeclaringType.Name}.{meta.Target.Method} " +
$"has failed on {e.GetType().Name}. Retrying in {delay / 1000} seconds. " + ({i + 1}/{this.Attempts})" );

                Thread.Sleep( (int) delay );
            }
        }
    }

    // TODO: Implement OverrideMethodAsync and call Task.Delay instead of Thread.Sleep.
}
Enter fullscreen mode Exit fullscreen mode

To add the policy to a method, apply it as a custom attribute:

public partial class Messenger
{
    private int _receiveCount;
    private int _sendCount;

    [Retry]
    [ReportExceptions]
    public void Send( Message message )
    {
        Console.WriteLine( "Sending message..." );

        // Simulate unreliable message sending
        if ( ++this._sendCount % 3 == 0 )
        {
            Console.WriteLine( "Message sent successfully." );
        }
        else
        {
            throw new IOException( "Failed to send message." );
        }
    }

    [Retry]
    [ReportExceptions]
    public Message Receive()
    {
        Console.WriteLine( "Receiving message..." );

        // Simulate unreliable message receiving
        if ( ++this._receiveCount % 3 == 0 )
        {
            Console.WriteLine( "Message received successfully." );

            return new Message( "Hi!" );
        }

        throw new IOException( "Failed to receive message." );
    }
}
Enter fullscreen mode Exit fullscreen mode

To further improve maintainability, toolkits like Metalama facilitate the bulk application of aspects, eliminating the need for developers to manually specify where each aspect should be used. For instance, we can stipulate that all public methods within a specific namespace should have exception reporting. Consequently, when new methods are added to this namespace, the exception-reporting aspect is automatically applied. This approach not only enhances the readability and maintainability of the codebase but also simplifies scalability. In Metalama, this is achieved using fabrics. The following example demonstrates how to add exception reporting to all public methods in a project:

internal class AddExceptionReportingToPublicMethodsFabric : ProjectFabric
{
    public override void AmendProject( IProjectAmender amender )
    {
        amender.Outbound.SelectMany( t => t.AllTypes )
            .SelectMany( t => t.Methods )
            .Where( m => m.Accessibility == Accessibility.Public )
            .AddAspectIfEligible<ReportExceptionsAttribute>();
    }
}
Enter fullscreen mode Exit fullscreen mode

Summary

Decorators are an effective way to maintain clean code and uphold the single responsibility principle. Opt for type decorators when you don’t own the code that you wish to enhance with new behaviors, or when you need to dynamically add behaviors at runtime. Use method decorators (aspect-oriented) when you own the source code and aim to adhere to the Single Responsibility Principle.


Discover Metalama, the leading code generation and validation toolkit for C#

  • Write and maintain less code by eliminating boilerplate, generating it dynamically during compilation, typically reducing code lines and bugs by 15%.
  • Validate your codebase against your own rules in real-time to enforce adherence to your architecture, patterns, and conventions. No need to wait for code reviews.
  • Excel with large, complex, or old codebases. Metalama does not require you to change your architecture. Beyond getting started, it's at scale that it really shines.

Top comments (0)