C# Design Patterns: The Decorator Pattern

Kenji Elzerman
5 min readApr 13, 2023

Developers love patterns. There are many patterns we can use or follow. A few well-known patterns are the strategy pattern, observer pattern, and builder pattern. There are many more and each has its own pros and cons. This time I want to show you the decorator pattern. The idea behind this pattern is that you can add behavior to an existing object without affecting other objects of the same class. Sounds complicated? I think so. To make it easier to understand I am going to extend an existing project from the basic caching tutorial with this decorator pattern.

Before we continue

If you want to skip all my hard work in the next chapters and have no idea what I am about to tell, you can simply download the end product from my GitHub repository.

https://github.com/KensLearningCurve/DecoratePatternWithCSharp

If you want to follow all the things I am showing make sure you open the branch “StartProject”. The branch “EndProject” contains all the code I will be adding in this tutorial.

Start project

My start project is pretty straightforward. There are two projects; CachingDemo.Business and CachingDemo.API. The API is a minimal API that is just here to show some of the UI. The business is the one that needs some changing and I will be focusing on this one the most. Especially the class MovieService.cs.

If you open this class you will find the method GetAll(). It looks like this:

public IEnumerable<Movie> GetAll()
{
string key = "allmovies";

Console.ForegroundColor = ConsoleColor.Red;

if (!memoryCache.TryGetValue(key, out List<Movie>? movies))
{
Console.WriteLine("Key is not in cache.");
movies = _dbContext.Set<Movie>().ToList();

var cacheOptions = new MemoryCacheEntryOptions()
.SetSlidingExpiration(TimeSpan.FromSeconds(10))
.SetAbsoluteExpiration(TimeSpan.FromSeconds(30));

memoryCache.Set(key, movies, cacheOptions);
}
else
{
Console.WriteLine("Already in cache.");
}

Console.ResetColor();

return movies ?? new List<Movie>();
}

It actually does two things: Handle cache data and the actual data if it doesn’t exist in the cache. And this is what the decorate pattern could solve. Let the MovieService.cs do what it needs to do and let another class handle the cache.

Caching Service

The first thing we need to do is create a service for the cache. Each service gets its own cache service. I have one service, MovieService, and I create another class and call it MovieService_Cache.cs. The reason for this name is simple: It will be placed directly under the original MovieService file.

I reuse the same interface I am using for the MovieService.

public class MovieService_Cache : IMovieService
{
public void Create(Movie movie)
{
throw new NotImplementedException();
}

public void Delete(int id)
{
throw new NotImplementedException();
}

public Movie? Get(int id)
{
throw new NotImplementedException();
}

public IEnumerable<Movie> GetAll()
{
throw new NotImplementedException();
}
}

Dependency Injection

I am using the IMemoryCache to inject the caching mechanism into the MovieService class, so I am doing the same in the cached version.

And here is the trick part 1: I inject the IMovieService in this class too. The IMovieService is connected to the MovieService, not the MovieService_Cache, so it’s safe to do. The cache class looks like this after the changes:

public class MovieService_Cache : IMovieService
{
private readonly IMemoryCache memoryCache;
private readonly IMovieService movieService;

public MovieService_Cache(IMemoryCache memoryCache, IMovieService movieService)
{
this.memoryCache = memoryCache;
this.movieService = movieService;
}

public void Create(Movie movie)
{
throw new NotImplementedException();
}

public void Delete(int id)
{
throw new NotImplementedException();
}

public Movie? Get(int id)
{
throw new NotImplementedException();
}

public IEnumerable<Movie> GetAll()
{
throw new NotImplementedException();
}
}

Making the methods work

Time to add some code to the methods. From the top to bottom:

The Create method doesn’t need caching. All it does is send data to the database and done. So I will be returning the result of the injected MovieService instance.

Delete is the same idea: no caching, so just reuse the original MovieService instance.

The Get(int id) could use cache. Here I am using the caching mechanism. The code is below. But, as soon as the key is not in the cache (the item doesn’t exist in the cache) I need to retrieve it from the database. This is something that the original MovieService does, not the cached version. See how I create and use the single responsibility principle?

I will do the exact same thing with the GetAll() method.

And here is the code:

public class MovieService_Cache : IMovieService
{
private readonly IMemoryCache memoryCache;
private readonly IMovieService movieService;

public MovieService_Cache(IMemoryCache memoryCache, IMovieService movieService)
{
this.memoryCache = memoryCache;
this.movieService = movieService;
}

public void Create(Movie movie)
{
movieService.Create(movie);
}

public void Delete(int id)
{
movieService.Delete(id);
}

public Movie? Get(int id)
{
string key = $"movie_{id}";

if (memoryCache.TryGetValue(key, out Movie? movie))
return movie;

movie = movieService.Get(id);

var cacheOptions = new MemoryCacheEntryOptions()
.SetSlidingExpiration(TimeSpan.FromSeconds(10))
.SetAbsoluteExpiration(TimeSpan.FromSeconds(30));

memoryCache.Set(key, movie, cacheOptions);

return movie;
}

public IEnumerable<Movie> GetAll()
{
string key = $"movies";

if (memoryCache.TryGetValue(key, out List<Movie>? movies))
return movies ?? new List<Movie>();

movies = movieService.GetAll().ToList();

var cacheOptions = new MemoryCacheEntryOptions()
.SetSlidingExpiration(TimeSpan.FromSeconds(10))
.SetAbsoluteExpiration(TimeSpan.FromSeconds(30));

memoryCache.Set(key, movies, cacheOptions);

return movies;
}
}

If you look at the Get(int id) method you see nothing special. Except for with movie = movieService.Get(id);. If the key does not exist in the cache it will get the movie from the original MovieService, puts that in the cache, and returns the result.

The same thing happens in the GetAll().

One thing: I removed all the caching from the MovieService.cs, including the injection of IMemoryCache. MovieService.cs now looks like this:

public class MovieService : IMovieService
{
private readonly DataContext _dbContext;

public MovieService(DataContext dbContext)
{
_dbContext = dbContext;
}

public void Create(Movie movie)
{
_dbContext.Set<Movie>().Add(movie);
_dbContext.SaveChanges();
}

public void Delete(int id)
{
Movie? toDelete = Get(id);

if (toDelete == null)
return;

_dbContext.Remove(toDelete);
_dbContext.SaveChanges();
}

public Movie? Get(int id) => _dbContext.Set<Movie>().FirstOrDefault(x => x.Id == id);

public IEnumerable<Movie> GetAll() => _dbContext.Set<Movie>().ToList();
}

Decorating the original service

If you start the API now, it won’t use any of the caching methods. It will just get all the movies from the database over and over again. We need to decorate the MovieService class. This is a dependency injection configuration.

To realize this I install the package Scrutor. This package contains the extension method Decorate, which helps us decorate the MovieService with the MovieService_Cache.

So, first, install the package:

Install-Package Scrutor

Then we head over to the API and open the Program.cs. Find the line where the IMovieService is connected to the MovieService. Hint; it’s on line 12. Add the following line of code under that:

builder.Services.Decorate<IMovieService, MovieService_Cache>();

That’s all, folks!

That’s it. Nothing more to do. The decorate pattern is ready to do its job.

To test it I recommend setting a breakpoint on a method in the MovieService_Cache, starting up the API, and executing that method with the breakpoint.

What happens is that the API will call the MovieService, but since it’s decorated with the MovieService_Cache it will first execute the method in the MovieService_Cache, overruling the original class.

Conclusion

This is just a small example of the decorator pattern. But it can be very powerful. I wouldn’t overuse it; don’t decorate every class you have. You don’t have to change anything in existing classes, which makes it safer if you are working in a big application where a class already works, but needs to change just a little bit.

I would love to see .NET’s own implementation of the decorator so we don’t have to use a third-party package.

--

--

Kenji Elzerman

C# developer | Kens Learning Curve | C# teacher for people diagnosed with ADHD and/or autism | world traveler