How to Register a Decorator in Dependency Injection Configuration in .NET?
Simple solutions for registering decorators, proxies, and wrappers in DI.
In this article, we will look at several different ways to register a decorator (proxy, wrapper) in a dependency injection configuration in .NET.
In addition, we will learn the pros and cons of each option.
Sometimes you may want to implement a wrapper on top of a class. This is usually to extend the behavior of the class you are wrapping and to respect the open-closed principle. For example, you have a UsersRepository
class:
public interface IUsersRepository
{
User GetUser(int userId);
}
public class UsersRepository : IUsersRepository
{
public User GetUser(int userId)
{
//Get a user from the database
return new User();
}
}
And this is what the Dependency Injection configuration looks like:
services.AddScoped<IUsersRepository, UsersRepository>();
Later you may decide to extend the UsersRepository
with caching behavior by implementing a CachedUsersRepository
proxy.
Here’s what the implementation of such proxy looks like:
public class CachedUsersRepository : IUsersRepository
{
private readonly IUsersRepository _usersRepository;
public CachedUsersRepository(IUsersRepository usersRepository)
{
_usersRepository = usersRepository;
}
public User GetUser(int userId)
{
//Caching logic here
return _usersRepository.GetUser(userId);
}
}
The proxy must implement the same interface as the subject class. In addition, the proxy redirects method calls to the subject class and implements additional behavior on top of it (in our case, caching).
If we need to start using CachedUsersRepository
instead of the original repository, we obviously need to make some changes to the dependency injection configuration. As you remember, the current configuration does not know anything about the CachedUsersRepository
proxy.
services.AddScoped<IUsersRepository, UsersRepository>();
It seems like it should be a simple change. We can just slightly update the DI configuration in this way:
services.AddScoped<IUsersRepository, CachedUsersRepository>();
And everything should work. However, if we try to run the application, we get an exception saying that some services cannot be constructed.
⚠️ The problem arises because the service provider cannot resolve the circular dependency we have introduced.
So we need to come up with another way to register our proxy. Fortunately, there are at least 3 ways to solve this problem.
Depending on Implementation
For this option, we need to inject the UsersRepository
implementation in the proxy class, not the interface:
public class CachedUsersRepository : IUsersRepository
{
private readonly UsersRepository _usersRepository;
public CachedUsersRepository(UsersRepository usersRepository)
{
_usersRepository = usersRepository;
}
public User GetUser(int userId)
{
//Caching logic here
return _usersRepository.GetUser(userId);
}
}
Next in the DI configuration we need to do two simple things. The first is to register an instance of the UsersRepository
, and the second is to register IUsersRepository
with the CachedUsersRepository
implementation. Here’s what it looks like:
services.AddScoped<UsersRepository>();
services.AddScoped<IUsersRepository, CachedUsersRepository>();
That’s it. Now everything will work as expected.
⚠️ However, there is a drawback to this approach. The proxy depends on UsersRepository
class rather than an IUsersRepository
interface in the constructor. However, in some cases it is necessary for the proxy to depend on the interface only for more flexibility.
The following option will help solve this problem.
Manual Proxy Instantiation
So, we can make our proxy depend on the interface again:
public class CachedUsersRepository : IUsersRepository
{
private readonly IUsersRepository _usersRepository;
public CachedUsersRepository(IUsersRepository usersRepository)
{
_usersRepository = usersRepository;
}
public User GetUser(int userId)
{
//Caching logic here
return _usersRepository.GetUser(userId);
}
}
Then in the DI we to manually construct the CachedUsersRepository
type to avoid issue with circular dependency.
Here is what the full configuration will look like:
services.AddScoped<UsersRepository>();
services.AddScoped<IUsersRepository, CachedUsersRepository>(provider => {
var userRepository = provider.GetRequiredService<UsersRepository>();
return new CachedUsersRepository(userRepository);
});
The service provider will now use the specific UsersRepository
type when creating a CachedUsersRepository
, so the exception will no longer occur.
⚠️ However, this approach has another problem related to readability. This code is a bit cumbersome. It is not very readable, and it will take some time for a new team member to understand what was the intent of the author.
This problem can be solved using the following approach.
Decorate with Scrutor
To implement the latter approach, we need to install the Scrutor
library into our project, which provides useful extension methods for dependency injection. It has a Decorate
extension method that will do the job.
Here’s what the configuration looks like:
services.AddScoped<IUsersRepository, UsersRepository>();
services.Decorate<IUsersRepository, CachedUsersRepository>();
In the first line, we register the IUsersRepository
interface with the UsersRepository
implementation. In the second line, we use the Scrutor Decorate
extension to decorate the CachedUsersRepository
service.
This approach is great for expressing intent so that all readers of the code can quickly understand what the configuration is trying to accomplish.
In this article, we learned 3 ways to decorate a class with a wrapper that is often required in software development.
Thanks for reading. If you liked what you read, check out the story below. Also, please support me on Buy Me a Coffee and follow me on Patreon.