How to Register a Decorator in Dependency Injection Configuration in .NET?

Simple solutions for registering decorators, proxies, and wrappers in DI.

Sasha Marfut
Level Up Coding

--

Photo by Desola Lanre-Ologun on Unsplash

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.

--

--