Posts Decorator Pattern in .NET Core 3.1
Post
Cancel

Decorator Pattern in .NET Core 3.1

The decorator pattern is a structural design pattern used for dynamically adding behavior to a class without changing the class. You can use multiple decorators to extend the functionality of a class whereas each decorator focuses on a single-tasking, promoting separations of concerns. Decorator classes allow functionality to be added dynamically without changing the class thus respecting the open-closed principle.

You can see this behavior on the UML diagram where the decorator implements the interface to extend the functionality.

Decorator Pattern UML

Decorator Pattern UML (Source)

When to use the Decorator Pattern

The decorator pattern should be used to extend classes without changing them. Mostly this pattern is used for cross-cutting concerns like logging or caching. Another use case is to modify data that is sent to or from a component.

Decorator Pattern Implementation in ASP .NET Core 3.1

You can find the code of the demo on Github.

I created a DataService class with a GetData method which returns a list of ints. Inside the loop, I added a Thread.Sleep to slow down the data collection a bit to make it more real-world like.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class DataService : IDataService
{
    public List<int> GetData()
    {
        var data = new List<int>();

        for (var i = 0; i < 10; i++)
        {
            data.Add(i);

            Thread.Sleep(350);
        }

        return data;
    }
}  

This method is called in the GetData action and then printed to the website. The first feature I want to add with a decorator is logging. To achieve that, I created the DataServiceLoggingDecorator class and implement the IDataService interface. In the GetData method, I add a stopwatch to measure how long collecting data takes and then log the time it took.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class DataServiceLoggingDecorator : IDataService
{
    private readonly IDataService _dataService;
    private readonly ILogger<DataServiceLoggingDecorator> _logger;

    public DataServiceLoggingDecorator(IDataService dataService, ILogger<DataServiceLoggingDecorator> logger)
    {
        _dataService = dataService;
        _logger = logger;
    }

    public List<int> GetData()
    {
        _logger.LogInformation("Starting to get data");
        var stopwatch = Stopwatch.StartNew();

        var data = _dataService.GetData();

        stopwatch.Stop();
        var elapsedTime = stopwatch.ElapsedMilliseconds;

        _logger.LogInformation($"Finished getting data in {elapsedTime} milliseconds");

        return data;
    }
}  

Additionally, I want to add caching also using a decorator. To do that, I created the DataServiceCachingDecorator class and also implemented the IDataService interface. To cache the data, I use IMemoryCache and check the cache if it contains my data. If not, I load it and then add it to the cache. If the cache already has the data, I simply return it. The cache item is valid for 2 hours.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class DataServiceCachingDecorator : IDataService
{
    private readonly IDataService _dataService;
    private readonly IMemoryCache _memoryCache;

    public DataServiceCachingDecorator(IDataService dataService, IMemoryCache memoryCache)
    {
        _dataService = dataService;
        _memoryCache = memoryCache;
    }

    public List<int> GetData()
    {
        const string cacheKey = "data-key";

        if (_memoryCache.TryGetValue<List<int>>(cacheKey, out var data))
        {
            return data;
        }

        data = _dataService.GetData();
        
        _memoryCache.Set(cacheKey, data, TimeSpan.FromMinutes(120));

        return data;
    }
}  

All that is left to do is to register the service and decorator in the ConfigureServices method of the startup class with the following code:

1
2
3
4
5
6
7
8
9
10
11
12
13
services.AddTransient<IDataService, DataService>();

services.AddScoped(serviceProvider =>  
{  
    var logger = serviceProvider.GetService<ILogger<DataServiceLoggingDecorator>>();  
    var memoryCache = serviceProvider.GetService<IMemoryCache>();
    
    IDataService concreteService = new DataService();  
    IDataService loggingDecorator = new DataServiceLoggingDecorator(concreteService, logger);  
    IDataService cacheingDecorator = new DataServiceCachingDecorator(loggingDecorator, memoryCache);
    
    return cacheingDecorator;  
});  

With everything in place, I can call the GetData method from the service which gets logged and the data placed in the cache. When I call the method again, the data will be loaded from the cache.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class HomeController : Controller
{
    private readonly IDataService _dataService;
    private readonly ILogger<HomeController> _logger;

    public HomeController(ILogger<HomeController> logger, IDataService dataService)
    {
        _logger = logger;
        _dataService = dataService;
    }

    public IActionResult Index()
    {
        return View();
    }

    public IActionResult GetData()
    {
        var data = _dataService.GetData();

        return View(data);
    }
}	  

Start the application and click on Get Data. After a couple of seconds, you will see the data displayed.

Displaying the loaded data

Displaying the loaded data

When you load the site again, the data will be displayed immediately due to the cache.

Conclusion

The decorator pattern can be used to extend classes without changing the code. This helps you to achieve the open-closed principle and separation of concerns. Mostly the decorator pattern is used for cross-cutting concerns like caching and logging.

You can find the code of the demo on Github.

This post is licensed under CC BY 4.0 by the author.