Click here to Skip to main content
15,887,746 members
Articles / Programming Languages / C#

Extending Object Behavior with the Decorator Pattern in C#

Rate me:
Please Sign up or sign in to vote.
5.00/5 (6 votes)
5 Apr 2023CPOL4 min read 9.3K   11   1
C# decorator pattern allows adding behavior to objects at runtime. In this tutorial, I show you how to implement the decorator pattern with C# in a minimal API.
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? Well, it's not... Once you get to know it.

Introduction

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.

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.

Explaining the Start Situation

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:

C#
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.

A 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.

C#
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 Injections

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:

C#
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();
    }
}

Adding Some Body to the Methods

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:

C#
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. 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.

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

C#
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();
}

Adding Decoration to the MovieService

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:

PowerShell
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. Add the following line of code under that:

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

That's It, Folks!

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.

History

  • 6th April, 2023: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Kens Learning Curve
Netherlands Netherlands
I am a C# developer for over 20 years. I worked on many different projects, different companies, and different techniques. I was a C# teacher for people diagnosed with ADHD and/or autism. Here I have set up a complete training for them to learn programming with C#, basic cloud actions, and architecture. The goal was to help them to learn developing software with C#. But the mission was to help them find a job suitable to their needs.

Now I am enjoying the freedom of traveling the world. Learning new ways to teach and bring information to people through the internet.

Comments and Discussions

 
BugDelete in cache version Pin
Benny Tordrup6-Apr-23 9:41
Benny Tordrup6-Apr-23 9:41 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.