C# Decorator Pattern

Summary: in this tutorial, you’ll learn how to use extend the behavior of an object dynamically without using inheritance.

Introduction to the C# Decorator pattern

The Decorator pattern is a structural pattern that allows you to extend or modify the behavior of an object without changing the original implementation of the object.

The following UML diagram illustrates the Decorator pattern:

The Decorator pattern consists of the following elements:

  • Component: This is the interface that defines operations an object can perform. The Component can be an interface or an abstract class. The Component defines an object that will be decorated.
  • ConcreteComponent: This is the class that implements the Component interface.
  • Decorator: This is an abstract class that implements the Component interface and contains a reference to the Component object.
  • ConcreteDecorator: This is the class that extends the Decorator class and adds additional behavior to the Component object.

In this diagram, the Decorator class inherits from the Component. But it uses inheritance to achieve type matching only, not to reuse the functionality of the Component.

C# Decorator pattern example

Let’s take an example to understand how the Decorator pattern works.

Developing a program that calls an API

Suppose you need to develop a program that calls the API from the following URL:

https://jsonplaceholder.typicode.com/posts/1Code language: C# (cs)

The API returns a JSON object with the four fields userId, id, title, and body.

{
  "userId": 1,
  "id": 1,
  "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
  "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
}Code language: C# (cs)

The following shows the steps for creating the program that calls the above API endpoint:

First, create a Post class that has the four corresponding fields:

public class Post
{
    public int UserId { get; set; }
    public int Id { get; set;  }
    public string? Title  { get; set; }
    public string? Body  { get; set; }

    public override string ToString() => $"{Id} - {Title}";
}Code language: C# (cs)

In the Post class, the ToString() method returns a string that consists of the Id and Title of the Post.

Second, create an interface called IPostService that has one method GetPost. The GetPost method gets a post by an id and returns a Post object:

public interface IPostService
{
    Task<Post?> GetPost(int postId);
}Code language: C# (cs)

Third, create a PostService class that implements the IPostService interface:

public class PostService : IPostService
{
    private readonly HttpClient client;

    public PostService()
    {
        client = new HttpClient();
    }

    public async Task<Post?> GetPost(int postId)
    {
        var url = $"https://jsonplaceholder.typicode.com/posts/{postId}";
        var response = await client.GetAsync(url);

        if (response.IsSuccessStatusCode)
        {
            var responseBody = await response.Content.ReadAsStringAsync();

            var post = JsonSerializer.Deserialize<Post?>(
                responseBody,
                new JsonSerializerOptions { PropertyNameCaseInsensitive = true }
            );
            return post;
        }
        else
        {
            throw new Exception($"Error: {response.StatusCode}");
        }
    }
}Code language: C# (cs)

In the PostService class, the GetPost method uses the HttpClient to call the API to get the JSON response, and deserializes it to the Post object using the JsonSerializer, and returns the Post. The method raises an exception if an error occurs.

Finally, use the PostService class to call the API and display the Post to the console:

public class Program
{
    public static async Task Main(string[] args)
    {
        var postService = new PostService();
        try
        {
            var post = await postService.GetPost(1);
            Console.WriteLine(post);
        }
        catch (Exception)
        {
            throw;
        }
    }

}Code language: C# (cs)

Putting it all together.

using System.Text.Json;

namespace Decorator;

public class Post
{
    public int UserId { get; set; }
    public int Id { get; set;  }
    public string? Title  { get; set; }
    public string? Body  { get; set; }
    public override string ToString() => $"{Id} - {Title}";
}

public interface IPostService
{
    Task<Post?> GetPost(int postId);
}

public class PostService : IPostService
{
    private readonly HttpClient client;

    public PostService()
    {
        client = new HttpClient();
    }

    public async Task<Post?> GetPost(int postId)
    {
        var url = $"https://jsonplaceholder.typicode.com/posts/{postId}";
        var response = await client.GetAsync(url);

        if (response.IsSuccessStatusCode)
        {
            var responseBody = await response.Content.ReadAsStringAsync();

            var post = JsonSerializer.Deserialize<Post?>(
                responseBody,
                new JsonSerializerOptions { PropertyNameCaseInsensitive = true }
            );
            return post;
        }
        else
        {
            throw new Exception($"Error: {response.StatusCode}");
        }
    }
}

public class Program
{
    public static async Task Main(string[] args)
    {
        var postService = new PostService();
        try
        {
            var post = await postService.GetPost(1);
            Console.WriteLine(post);
        }
        catch (Exception)
        {
            throw;
        }
    }

}Code language: C# (cs)

Output:

1 - sunt aut facere repellat provident occaecati excepturi optio reprehenderitCode language: C# (cs)

The program works as expected.

Now, you receive a new requirement that you need to log the API call. To do that you can modify the PostService class.

But, if you do so, you’ll violate the single responsibility principle. The PostService class should be responsible for calling the API and returning a Post. And it should not handle the logging functionality.

To meet the new requirements without changing the PostService class, you can use the Decorator pattern by extending the PostService object dynamically.

Adding a decorator object

We’ll define two new classes:

  • PostServiceDecorator class which serves as the Decorator class
  • PostServiceLoggingDecorator class which acts as the ConcreteDecorator class:

Here’s the new UML diagram:

C# decorator pattern - logging

Note that we don’t include the Post class in the diagram to focus more on the Decorator pattern.

First, define a new PostServiceDecorator abstract class that implements the IPostService interface and has an IPostService instance:

public abstract class PostServiceDecorator : IPostService
{
    protected readonly IPostService postService;

    public PostServiceDecorator(IPostService postService)
    {
        this.postService = postService;
    }

    public abstract Task<Post?> GetPost(int postId);

}Code language: C# (cs)

Second, define the PostLoggingDecorator class that implements the PostServiceDecorator class:

public class PostServiceLoggingDecorator : PostServiceDecorator
{
    public PostServiceLoggingDecorator(IPostService postService)
        : base(postService) { }

    public async override Task<Post?> GetPost(int postId)
    {
        Console.WriteLine($"Calling the API to get the post with ID: {postId}");
        var stopwatch = Stopwatch.StartNew();

        try
        {
            var post = await postService.GetPost(postId);
            Console.WriteLine($"It took {stopwatch.ElapsedMilliseconds} ms to call the API");
            return post;
        }
        catch (Exception ex)
        {
            Console.WriteLine($"GetPostAsync threw exception: {ex.Message}");
            throw;
        }
        finally
        {
            stopwatch.Stop();
        }

    }
}Code language: C# (cs)

The GetPost() method of PostServiceLoggingDecorator calls the GetPost() method of the IPostService instance. It also uses the StopWatch object to measure the time for calling the API and logs some information to the console.

In other words, the GetPost() method of the PostServiceLoggingDecorator class adds the logging functionality to the GetPost() method of the IPostService object.

Third, modify the Program class to use the PostLoggingDecorator class:

public class Program
{
    public static async Task Main(string[] args)
    {
        IPostService postService = new PostService();
        var postServiceLogging = new PostServiceLoggingDecorator(postService);
        try
        {
            var post = await postServiceLogging.GetPost(1);
            Console.WriteLine(post);
        }
        catch (Exception)
        {
            throw;
        }
    }

}Code language: C# (cs)

In the Main() method, we create an instance of the PostService and pass it to the constructor of the PostServiceLoggingDecorator.

The PostLoggingDecorator acts as a decorator for the PostService object by adding the logging functionality to the PostService object.

Put it all together.

using System.Diagnostics;
using System.Text.Json;

namespace Decorator;

public class Post
{
    public int UserId { get; set; }
    public int Id { get; set;  }
    public string? Title  { get; set; }
    public string? Body  { get; set; }
    public override string ToString() => $"{Id} - {Title}";
}

public interface IPostService
{
    Task<Post?> GetPost(int postId);
}


public class PostService : IPostService
{
    private readonly HttpClient client;

    public PostService()
    {
        client = new HttpClient();
    }

    public async Task<Post?> GetPost(int postId)
    {
        var url = $"https://jsonplaceholder.typicode.com/posts/{postId}";
        var response = await client.GetAsync(url);

        if (response.IsSuccessStatusCode)
        {
            var responseBody = await response.Content.ReadAsStringAsync();

            var post = JsonSerializer.Deserialize<Post?>(
                responseBody,
                new JsonSerializerOptions { PropertyNameCaseInsensitive = true }
            );
            return post;
        }
        else
        {
            throw new Exception($"Error: {response.StatusCode}");
        }
    }
}

public abstract class PostServiceDecorator : IPostService
{
    protected readonly IPostService postService;

    public PostServiceDecorator(IPostService postService)
    {
        this.postService = postService;
    }

    public abstract Task<Post?> GetPost(int postId);

}

public class PostServiceLoggingDecorator : PostServiceDecorator
{
    public PostServiceLoggingDecorator(IPostService postService)
        : base(postService) { }

    public async override Task<Post?> GetPost(int postId)
    {
        Console.WriteLine($"Calling the API to get the post with ID: {postId}");
        var stopwatch = Stopwatch.StartNew();

        try
        {

            var post = await postService.GetPost(postId);
            Console.WriteLine($"It took {stopwatch.ElapsedMilliseconds} ms to call the API");
            return post;
        }
        catch (Exception ex)
        {
            Console.WriteLine($"GetPostAsync threw exception: {ex.Message}");
            throw;
        }
        finally
        {
            stopwatch.Stop();
        }

    }
}

public class Program
{
    public static async Task Main(string[] args)
    {
        IPostService postService = new PostService();
        var postServiceLogging = new PostServiceLoggingDecorator(postService);
        try
        {
            var post = await postServiceLogging.GetPost(1);
            Console.WriteLine(post);
        }
        catch (Exception)
        {
            throw;
        }
    }

}Code language: C# (cs)

In this example, we start with the PostService object:

And decorate it with the PostServiceLoggingDecorator object by adding the logging functionality:

Adding more decorator object

Suppose, you want to improve the performance of the PostService class by caching the result of the API call.

For example, if a post with id 1 is requested for the first time, you can directly call the API and save it into a cache. But if the same post is requested again, you can retrieve the Post with id 1 from the cache instead of making a call to the API.

To do this, you can add a new decorator class called PostServiceCache that perform caching:

C# decorator pattern - cache

First, define the PostServiceCacheDecorator that extends the PostServiceDecorator:

public class PostServiceCacheDecorator : PostServiceDecorator
{
    private readonly Dictionary<int, Post> cache;

    public PostServiceCacheDecorator(IPostService postService) : base(postService)
    {
        cache = new Dictionary<int, Post>();
    }

    public async override Task<Post?> GetPost(int postId)
    {
        // get post from the cache
        if (cache.TryGetValue(postId, out var post))
        {
            // demo purpose 
            Console.WriteLine($"Getting the post with id {postId} from the cache");
            return post;
        }

        // otherwise call the API
        post = await postService.GetPost(postId);
        if (post != null)
        {
            cache[postId] = post;
        }
        return post;

    }
}Code language: C# (cs)

The PostServiceCacheDecorator class has a member cache that serves as a cache. The type of the cache is Dictionary<int, Post> which allows you to look up Post by id.

The GetPost() method of the PostServiceCacheDecorator class checks the cache for the requested id and returns the Post if it is already in the cache. Otherwise, it uses the GetPost() method of the PostService object to call the API and adds the result into the cache.

Second, modify the Program to use the PostServiceCacheDecorator that caches the result of the API call if the user requests the same post again:

public class Program
{
    public static async Task Main(string[] args)
    {
        IPostService postService = new PostService();
        var postServiceLogging = new PostServiceLoggingDecorator(postService);
        var postServiceCache = new PostServiceCacheDecorator(postServiceLogging);
        try
        {
            var post = await postServiceCache.GetPost(1);
            Console.WriteLine(post);

            // request the same post second time
            Console.WriteLine("Getting the same post again:");
            post = await postServiceCache.GetPost(1);
            Console.WriteLine(post);
        }
        catch (Exception)
        {
            throw;
        }
    }

}Code language: C# (cs)

Output:

Calling the API to get the post with ID: 1
It took 1698 ms to call the API
1 - sunt aut facere repellat provident occaecati excepturi optio reprehenderit
Getting the same post again:
Getting the post with id 1 from the cache
1 - sunt aut facere repellat provident occaecati excepturi optio reprehenderitCode language: C# (cs)

The output shows that the second call to the GetPost method does not call the API but gets the result from the cache.

In this example, the PostServiceLoggingDecorator decorates the PostService and the PotServiceCacheDecorator decorates the PostServiceLoggingDecorator class.

C# decorator pattern - PostServiceCacheDecorator

If you want to, you can add more decorator classes and they decorate each other to add more functionality to the decorated objects.

Also, you can mix and match the decorator based on the requirements. This makes your code more flexible.

Decorator pattern in .NET

.NET uses the decorator pattern in some libraries. For example, the BufferedStream, GZipStream, and CryptoStream classes are the decorators of the Stream class. The FileStream, MemoryStream, and NetworkStream are concrete Stream classes.

Note that the Stream and FileStream classes have more methods that the ones listed on the diagram. We put out the Read and Write method only for simplification purposes.

Summary

  • Use the C# decorator pattern to extend the behavior of an object dynamically at runtime without using inheritance.
Was this tutorial helpful ?