How to Dynamically Add Behaviors With Decorator Pattern in C#

Everything you need to know about Decorator Pattern in one place.

How to Dynamically Add Behaviors With Decorator Pattern in C#

What is the Decorator Pattern

The Decorator Pattern is a structural design pattern that allows you to dynamically add behavior to a class without making changes to that class. In other words, it uses composition instead of inheritance to add the functionality of an object.

Decorator-Component.jpg

IComponent

IComponent is an interface. It is an abstraction of what we want to decorate. You can also use an abstract class.

Component

The Component is a class that you want to add behavior. The component needs to implement an interface or extend an abstract class.

Decorator

The Decorator class will implement the same interface or abstract class as the Component class.

The decorator’s constructor always takes an object implementing the same interface as a parameter. Then the object of Component is introduced into the class attribute in Component’s constructor. Like this.

public DecoratorOne(IComponent component)
{
  _component = component;
}

When Decorator implements an interface IComponent, you must implement methods SomeMethod() and AnotherMethod(). Into those methods, you can add new behavior.

public void SomeMethod()
{
  //Decorating before inner component method invocation
  _component.SomeMethod();
  //Decorating after inner compoenent method invocation
}

Multiple Decorators

You are not limited to just one Decorator object per Component class. You can define Decorator which decorates another Decorator which decorates Components and so on. Look at the image below.

Onion-Structure-Decorator.jpg

Both pictured Decorators must implement both methods defined in the IComponent interface.

Block named Code represents a place in the codebase where I am operating with Component.

The code will always invoke methods of the most outer Decorator firstly. To bubbled into a Component class, you must call the same-titled method of an inner Decorator in every Decorator.

Onion-Structure-Decorator_methods.jpg

If you want to use new behaviors of both Decorators at the same time, you must change the initialization of the Component object in the calling code. It can be done in three steps.

  1. Initialization of DecoratorTwo.
  2. Initialization of DecoratorOne and passing into the constructor of DecoratorTwo.
  3. Initialization of Component and passing into the constructor of DecoratorOne.

That’s it! You don’t have to modify any more code. The word dynamic gets its value.

Tutorial

Prerequisites

The tutorial assumes basic knowledge of .NET and Object-Oriented Programming.

Introduction of the Sample project

The sample project is a web application that collects data about the location of the Company's cars. An external API provides data.

Your Role at the Project

Imagine you are working as a support developer, and the Sample project is your responsibility. Suddenly, you have got a phone call from the customer;

Hello, Mr. AwesomeReader, I would like to report that the data location page ends with the timeout error.

Or maybe he is not that polite.

2897.jpg

You are going to ask for advice from colleagues. They advise you to measure the time of how long it application takes to collect data.

LocationService class

LocationService class is taking care of requesting an API and serializing collected data into Data-Transfer objects.

Class implements interface ILocationService. The interface contains a definition of the method GetLocation(). The implementation looks like this.

public class LocationService : ILocationService
{
  private RestClient _apiClient;
  private string _apiKey;

  public LocationService(String apiKey)
  {
    _apiKey = apiKey;
    _apiClient = new 
    RestClient("http://api.companycarlocations.org");
  }

  public List<LocationDto> GetLocations()
  {
    var request = new RestRequest("data/2.5/location");
    request.AddParameter("appid", _apiKey);

    var response = _apiClient.Get(request);

    if (response.IsSuccessful)
    {
      return JsonConvert.DeserializeObject<List<LocationDto>>(response.Content);
    }
    else
    {
      throw new Exception();
    }
  }
}

Maybe you get an idea to add a logging code directly into the method. Let me tell you why it is a bad idea.

First, it will make the class a lot harder to read. More importantly, when you are modifying the existing code, you can quickly introduce an error to the application.

Another reason for not doing this is a violation of the Single Responsibility Principle. The meaning of principle can be expressed in one sentence.

A class should have only one reason to change.

Because you are a great developer and thanks to the great author (me), who tell you why is it a bad idea, you decide to use Decorator Pattern. Making that decision allows you to extend LocationService without modification of class. This reminds me of another SOLID principle and it is an Open-Closed Principle. The main thought of this principle sounds like this.

Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.

The decorator supports the Open-Closed Principle by its nature. What a great pattern.

Logging Decorator

You are going to create a LoggingLocationService which is the Decorator of your Component LocationService.

public class LoggingLocationService : ILocationService
{
  private ILocationService _innerLocationService;
  private ILogger<LoggingLocationService> _logger;

  public LoggingLocationService(ILocationService locationService, ILogger<LoggingLocationService> logger)
  {
  _innerLocationService = locationService;
  _logger = logger;
  }

  public List<LocationDto> GetLocations()
  {
    Stopwatch sw = Stopwatch.StartNew();
    List<LocationDto> locations = innerLocationService.GetLocations();
    sw.Stop();
    long elapsedMillis = sw.ElapsedMilliseconds;
    _logger.LogWarning($"Retrieved location data - Elapsed ms{elapsedMillis}");
    return locations;
  }
}

Nice. I noticed that you are using a built-in Stopwatch class to measure elapsed time in milliseconds. It’s much better than my solution to deducting two variables with different DateTime.Now values.

Now you need to do a little change to the calling code, which you can find in the Index.cshtml.cs class. LocationService initialization looks like this.

public IndexModel(IConfiguration configuration)
{
  string apiKey = configuration.GetValue<String>("AppSettings:CompanyCarLocationsApiKey");
  _locationService = new LocationService(apiKey);
}

You need to replace the new LocationService(apiKey) with:

new LoggingLocationService(
  new LocationService(apiKey),
    _loggerFactory.CreateLogger<LocationService>())

and that’s it. You are ready to go.

Story continues

Thankfully of implemented logging, you were able to find a cause of long loading time. You did some magic as developers do and the bug no longer exists.

While you were coding, a wild idea came into your mind; “Maybe I can cache the collection of locations into memory and short loading time even more”.

CachingLocationService

After a conversation with teammates, you decide to cache data for one hour. It sounds like a reasonable amount of time. Time for another Decorator.

public class CachingLocationService : ILocationService
{
  private IMemoryCache _cache;
  private ILocationService _innerLocationService;

  public CachingLocationService(
    ILocationService locationService, IMemoryCache cache)
  {
    _innerLocationService = locationService;
    _cache = cache;
  }

  public List<LocationDto> GetLocations()
  {
    string cacheKey = $"Locations::faef89DWWD890";
    if ( _cache.TryGetValue(cacheKey, out List<LocationDto> locations))
    {
      return locations;
    }
    else
    {
      locations = _innerLocationService.GetLocations();

      _cache.Set<List<LocationDto>>
        (cacheKey, locations, TimeSpan.FromMinutes(60));

      return locations;
    }
  }
}

Looking good. I see you are using a built-in MemoryCache class. Also, there is no need to generate a random cache key because you are loading a whole collection, right?

In the first if statement, you simply try to get the value of locations collection by cache key. If the collection is empty, new data will be loaded. Therefore, you set a new record into the cache with the expiration of the hour.

Now I guess you would like to combine both new decorators. Logging Decorator and Caching Decorator together.

_locationService = new CachingLocationService(
  new LoggingLocationService(
    new LocationService(apiKey), logger), memoryCache);

You wrapped the initialization of LoggingLocationService with the initialization of CachingLocationService. It looks a little bit unreadable. Try to use multiple variables.

var withLocationService = new LocationService(apiKey);
var withLoggingLocationService = new LoggingLocationService(withLocationService, logger);
var cachingLocationService = new CachingLocationService(withLoggingLocationService, memoryCache)

Much better. When I see code where you can initialize instances with different behaviors, I am using a Builder Pattern. You can explore it, but not now.

The final class diagram after our changes looks like this.

MultipleDecoratorsDiagram.jpg

Decorator and Dependency Injection

Before any Decorator implementations, registration into the container of LoggingService in the Sample project looked like an example below.

services.AddScoped<ILocationService>(serviceProvider => 
  new LocationService(apiKey));

In this case, we are using a build-in .NET Core IoC container. This registration means that injected ILocationService via constructor will always provide an instance of the LocationService object. But you want an instance of CachingLocationService.

You can take a code of multiple initializations shown at the end of the tutorial and use it in registration like this.

services.AddScoped<ILocationService>(serviceProvider =>
{
  String apiKey = Configuration
    .GetValue<String>("AppSettings:LocationCarsApiKey");var logger = serviceProvider
    .GetService<ILogger<LoggingLocationService>>();var memoryCache = serviceProvider.GetService<IMemoryCache>();ILocationService locationService = new LocationService(apiKey);

  ILocationService withLoggingLocationService = 
    new LoggingLocationService(locationService, logger);

  ILocationService withCachingLocationService = 
    new CachingLocationService(withLoggingLocationService,   
    memoryCache);

  return withCachingLocationService;
});

There is a third option. You can use a great NuGet package named Scrutor. It is an extension of dependency injection in .NET. It adds methods for registration to make registrations more fluently readable. Cool, right?

services.AddScoped<ILocationService>
  (serviceProvider => new LocationService(apiKey));services.Decorate<ILocationService>((inner, provider) =>
  new LoggingLocationService(inner,   
    provider.GetService<ILogger<LoggingLocationService>>()));services.Decorate<ILocationService>((inner, provider) =>
  new CachingLocationService(inner,   
    provider.GetService<IMemoryCache>()));

Decorator in .NET

You can find the Decorator Pattern also in the guts of .NET. A prime example is a Stream class.

At the top of the hierarchy, we have the Stream class. This is an abstract class and requires you to provide implementations for several methods, mainly the Read and Write method. Sounds familiar, right?

Then we have several concrete implementations of the Stream class, including FileStream, MemoryStream, and NetworkStream. Each of these classes would be equivalent to the LocationService class. Finally, we have several classes that act as decorators. These include the BufferedStream, GZipStream, and CryptoStream classes.

How to recognize when to use a Decorator?

Cross-cutting concerns. These are things like logging, performance tracking, caching, and authorization. Whenever you have these cross-cutting concerns come up, you can implement them using a Decorator Pattern.

Another typical situation is the modification of data before being passed into the Component. For example, encrypt and decrypt data or conversion of units.

Build-in shortcut in Visual Studio

There is a built‑in refactoring tool in Visual Studio, either with the keyboard shortcut of Ctrl+R and then Ctrl+I. Visual Studio will create a Decorator class from the selected interface.

Summary

Remember, patterns are just guiding rails, not prescriptive implementation rules. They are not set into stone. Decorator Pattern is dynamic, clean, beloved principles supportive, and you should consider it as a new tool to your toolbox. The sample project can be found at Github.

itixo-logo-blue.png