Imagine that you are working on an existing application that sends notifications to other programs. The initial version of that application was only sending notifications by email but now you are asked to add some additional features in that library so that it can start sending notifications by SMS, or can send notifications to Facebook, Twitter, etc., or a combination of many other apps.
You don’t want to modify existing code, you don’t want to create a big hierarchy of child and grandchild classes and still, you want to enhance the existing application. This is where the Decorator Pattern will come to rescue you and will allow you to dynamically add or remove functionality to existing classes.
Let’s get started
1. What is a Decorator Pattern?
The decorator pattern in C# (also known as Wrapper) is a structural design pattern and it allows developers to dynamically add new behaviors and features to existing classes without modifying them thus respecting the open-closed principle. This pattern lets you structure your business logic into layers (wrappers) in a way that each layer adds some additional behavior or functionality to an existing object, promoting separation of concern. Furthermore, these layers can be added or removed at runtime and clients can also use the different combinations of decorators to be attached to an existing object.
2. Pros Decorator Pattern
- We can extend an object’s behavior without creating a hierarchy of new child classes.
- We can add or remove features from an object at runtime which gives developer flexibility not available in simple inheritance.
- We can combine several features by wrapping an object into multiple decorators
- We can divide a complex object into several smaller classes with specific behaviors which promotes the Single Responsibility Principle
- It supports the Open-closed principle which states that the classes should be open for extension but closed for modification.
3. Cons Decorator Pattern
- The object instantiation can be complex as we have to create an object by wrapping it in several decorators.
- Sometimes, it’s hard to keep track of the full wrapper stack, and removing a specific wrapper from the stack is not something easy to achieve.
- Decorators can cause issues if the client using them relies heavily on the object concrete type.
Getting Started with Decorator Pattern in ASP.NET Core 5
The decorator pattern can be used to attach cross-cutting concerns such as logging or caching to existing classes without changing their code. Let’s create a new ASP.NET Core 5 REST API to learn how to use the decorator pattern in C# to dynamically add/remove logging and caching features.
First of all, create the following Player
model class in the Models
folder of the project.
Next, create the following PlayerService
and return a fake list of players. Of course, in a live application, this type of service will fetch data from a backend database but I want to keep the example simple as the purpose of this post is to show you the usage of decorator patterns in real-world scenarios.
IPlayerService.cs
PlayerService.cs
Inject the above PlayerService
in an ASP.NET Core API Controller and call the GetPlayersList
method as shown below.
Run the project and make a get request to fetch the list of players. You should see the players list on the page as shown below.
Everything is pretty straightforward so far as we are using standard services and controllers in ASP.NET Core.
4. Implementing a Logging Decorator
The first decorator I want to attach with the above PlayerService
is a logging decorator. This decorator will allow our service to output the log messages at runtime. This can be very useful in a production environment where you want to see how your services are working internally by logging messages to different sources. Let’s create a class PlayerServiceLoggingDecorator
and implement the same IPlayerService
interface on it.
PlayersServiceLoggingDecorator.cs
We are injecting the instances of IPlayerSerice
and ILogger
in the decorator constructor using the dependency injection. The logging decorator is implementing the IPlayersService
interface so it has to define the GetPlayersList
method. Inside the GetPlayersList
method, we are calling the GetPlayersList
method implemented by PlayerService
and once we have the player's list available, we are simply iterating over them to log their Id and Name. There are also few other LogInformation
method calls to log different types of messages. We are also using the Stopwatch
object to log our method execution time.
5. Implementing a Caching Decorator
The second decorator I want to attach is a caching decorator. This decorator will allow our service to cache the player’s list for a certain amount of time so that we don’t need to fetch the data from the backend service or database again. This can be useful in applications where you want to improve your application performance. Let’s create a class PlayersServiceCachingDecorator
and implement the same IPlayerService
interface on it.
PlayersServiceCachingDecorator.cs
This time, we are injecting the instances of IPlayerSerice
and IMemoryCache
in the decorator constructor. Inside the GetPlayersList
method, we are first checking if the player’s list with a matching cache key is available in the memory cache and returning the same list from the cache. If we don’t have the player’s list in the cache, we are calling the GetPlayersList
method of PlayerService
class to get the list and then adding it to the memory cache for one minute.
6. Manually Registering the Decorators with DI Container
We are now ready to register our service and decorators so that they can be injected using the .NET Core dependency injection framework. This is where you will also see how we are wrapping one decorator into another to attach a chain of decorators to an existing service.
Startup.cs
We first registered the PlayerService
using the AddScoped
method. Then we requested the instance of PlayerService
using the GetRequiredService
method to pass it into the constructor of our PlayersServiceCachingDecorator
class. Finally, the instance of caching decorator is passed in the PlayersServiceLoggingDecorator
constructor.
With everything in place, let’s run the application once again and this time check what messages are logged in the output window and how much time our methods took to execute.
You can see all the log messages in the output window as shown in the above screenshot. The first time, our memory cache was empty that’s why the method took 28 milliseconds to execute. Refresh the player’s list page again and this time you will see that method will take less time as compared with the previous request because now the data will be fetched from the memory cache.
7. Registering the Decorators using Scrutor library
We now have a real-world example of using a decorator pattern in an ASP.NET Core application but some of you may not like the way we manually registered our decorators with dependency injection in Startup.cs
file. We are instantiating the decorators ourselves and passing them to other decorators by calling them their constructors. What if the decorator class has many more services injected into the constructor? You don’t want to instantiate a big list of services just to pass in the decorator constructor. We want an easy way to register our decorators and this is where Scrutor library comes to the rescue.
The Scrutor is a small library that includes some extension methods for registering decorators. The simplest and the most common method is Decorate()
which allows us to register decorators just like we register normal classes in .NET. We can install the Scrutor library using the NuGet Package Manager.
With the help of the Scrutor library, the registration of our decorators can be as simple as the following code snippet.
Startup.cs
Now You will run the application it behaves as expected it was working previously.
8. Dynamically Add or Remove Decorators at Runtime
In a real-world application, you may want to add or remove decorators dynamically at runtime based on different use cases such as:
- You may want to add a logging decorator only in the production environment but don’t want to log anything in the development environment.
- You may want to use some configuration settings to dynamically add/remove decorators in any environment.
We can add EnableCaching
and EnableLogging
settings in the appsettings.json
file and cache and logging can be enabled/disabled using these settings.
Here is the code to register decorators based on the above configuration settings.
Startup.cs
Summary
The decorator pattern in Asp.Net Core C# can be used to extend classes or to add cross-cutting concerns without changing their code. In this post, we learned how to use the decorator pattern to add features such as logging and caching in ASP.NET Core APIs. We also learn how to register decorators manually and using the Scrutor library. In the end, we learned how to dynamically enable/disable decorators based on configuration settings. I hope, you will keep the decorator pattern in mind for certain use cases while developing your applications.
You can download source code from my GitHub repository(https://github.com/ankit68543/decorator-pattern). If you like this article! Don’t forget to give me a star. :p
#Happy #Learning