In my last post, I described how you can use Scrutor to add assembly scanning to the ASP.NET Core built-in DI container, without using a third-party library. Scrutor is not intended to replace fully-featured third-party containers like Autofac, Windsor, or StructureMap/Lamar. Instead, it's designed to add a few additional features to the container that's built-in to every ASP.NET Core application.
In this post, I'll look at the other feature of Scrutor - service decoration. I'll describe how you can use the feature to "decorate" or "wrap" an instance of a service with one that implements the same interface. This can be useful for adding extra functionality to a "base" implementation.
The decorator pattern
The Decorator design pattern is one of the original well-known Gang of Four design patterns. It allows you take an object with existing functionality, and layer extra behaviour on top. For example, in a recent post, Steve Smith discusses a classic example of adding caching functionality to an existing service.
In his example, Steve describes how he has an existing AuthorRepository
class, and he wants to add a caching layer to it. Rather than editing the AuthorRepository
directly, he shows how you can use the decorator / proxy pattern to add additional behaviour to the existing repository.
If you're not familiar with the decorator/proxy pattern, I strongly suggest reading Steve's post first.
In this post, I'm going to use a somewhat stripped-back version of Steve's example. We have an existing AuthorRepository
, which implements IAuthorRepository
. This service loads an Author
from the AppDbContext
(using EF Core for example):
public interface IAuthorRepository
{
Author GetById(Guid id);
}
public class AuthorRepository: IAuthorRepository
{
private readonly AppDbContext _context;
public AuthorRepository(AppDbContext context)
{
_context = context;
}
public Author GetById(Guid id)
{
return _context.Authors.Find(x=> x.Id = id);
}
}
This is our base service that we want to add caching to. Following Steve's example, we create a CachedAuthorRepository
which also implements IAuthorRepository
, but which also takes an IAuthorRepository
as a constructor parameter. To satisfy the interface, the CachedAuthorRepository
either loads the requested Author
object from it's cache, or by calling the underlying IAuthorRepository
.
This is slightly different to Steve's example. In his example, Steve injects the concrete
AuthorRepository
into the caching decorator class. In my example I'm injecting the interfaceIAuthorRepository
instead. I'll explain why this matters in the next section.
public class CachedAuthorRepository : IAuthorRepository
{
private readonly IAuthorRepository _repo;
private readonly ConcurrentDictionary<Guid, Author> _dict;
public CachedAuthorRepository(IAuthorRepository repo)
{
_repo = repo;
_dict = new ConcurrentDictionary<Guid, Author>();
}
public Author GetById(Guid id)
{
return _dict.GetOrAdd(id, i => _repo.GetById(i));
}
}
Note This is a very simplistic version of a caching repository - don't use it for production. Steve's implementation makes use of an
IMemoryCache
instead, and considers cache expiry etc, so definitely have a look at his code if you're actually trying to implement this in your own project.
In your application, you would inject an instance of IAuthorRepository
into your classes. The consuming class itself wouldn't know that it was using a CachedAuthorRepository
rather than an AuthorRepository
, but it wouldn't matter to the consuming class.
That's all the classes we need for now, the question is how do we register them with the DI container?
Manually creating decorators with the ASP.NET Core DI container
As I've discussed previously, the ASP.NET Core DI container is designed to provide only those features required by the core ASP.NET Core framework. Consequently, it does not provide advanced capabilities like assembly scanning, convention-based registration, diagnostics, or interception.
If you need these capabilities, the best approach is generally to use a supported third-party container. However, if for some reason you can't, or don't want to, then you can achieve some of the same things with a bit more work.
For example, imagine you want to decorate the AuthorRepository
from the last section with the CachedAuthorRepository
. That means when an IAuthorRepository
is requested from the service provider, it should return a CachedAuthorRepository
that has been injected with an AuthorRepository
.
In Steve's example, he made sure that the CachedAuthorRepository
required an AuthorRepository
in the constructor, not an IAuthorRepository
. This allowed him to more easily configure the CachedAuthorRepository
as a decorator using the following pattern:
services.AddScoped<AuthorRepository>();
services.AddScoped<IAuthorRepository, CachedAuthorRepository>();
But what if you don't want your decorator to depend on the concrete AuthorRepository
? Maybe you want CachedAuthorRepository
to work with multiple IAuthorRepository
implementations, rather than just the one (current) concrete implementation. One way to achieve this would be to still register the AuthorRepository
directly with the DI container as before, but to manually create a CachedAuthorRepository
when requested. For example:
services.AddScoped<AuthorRepository>();
services.AddScoped<IAuthorRepository>(provider =>
new CachedAuthorRepository(provider.GetRequiredService<AuthorRepository>()));
This construction achieves the requirements - requests for IAuthorRepository
will return a CachedAuthorRepository
, and the CachedAuthorRepository
constructor can use IAuthorRepository
. The downside is that we had to manually create the CachedAuthorRepository
using a factory function. This rarely seems right when you see it as part of DI configuration, and will need to be updated if you change the constructor of CachedAuthorRepository
- something using a DI container is designed to avoid.
There are other ways around this which avoid having to use
new
. I'll describe them later when we look under the covers of Scrutor'sDecorate
command.
In the next section, I'll show how you can use Scrutor to achieve a similar thing, without having to new
-up the CachedAuthorRepository
instance, while still allowing CachedAuthorRepository
to take IAuthorRepository
as a constructor argument.
Using Scrutor to register decorators with the ASP.NET Core DI container
As well as assembly scanning, Scrutor includes a number of extension methods for adding Decorators to your classes. The simplest to use is the Decorate<,>()
extension method. With this method you can register the CachedAuthorRepository
as a decorator for IAuthorRepository
:
services.AddScoped<IAuthorRepository, AuthorRepository>();
services.Decorate<IAuthorRepository, CachedAuthorRepository>();
When you use the Decorate<TService, TDecorator>
method, Scrutor searches for any services which have been registered as the TService
service. In our case, it finds the AuthorRepository
which was registered as an IAuthorRepository
.
Note Order matters with the
Decorate
method. Scrutor will only decorate services which are already registered with the container, so make sure you register your "inner" services first.
Scrutor then replaces the original service registrations with a factory function for the decorator TDecorator
, as you'll see in the next section.
Using Scrutor to register Decorators has a number of advantages compared to the "native" approach shown in the previous section:
- You're not calling the constructor directly. Any extra dependencies added to the constructor won't affect your DI registration code.
- The
AuthorRepository
is registered "normally" as anIAuthorRepository
. This is useful as you don't have to directly control the registration yourself - it could be a different component/library that registers theAuthorRepository
component with the DI container. - The
Decorate<,>
method is explicit in its behaviour. In the "native" approach you have to read all the registration code to figure out what is being achieved.Decorate<,>()
is much clearer.
There are a number of other Decorate
overloads. I won't be focusing on them in this post, but in brief they are:
TryDecorate<TService, TDecorator>()
. Decorate all registered instances of theTService
withTDecorator
. Useful if you don't know whether the service is registered or not.Decorate(Type serviceType, Type decoratorType
). Non-generic version of theDecorate<,>
method. Useful if your service or decorator are open generics likeIRepository<>
.Decorate(Func<TService, IServiceProvider, TService> decorator)
. Provide aFunc<>
to generate the decorator class given the decorated service. Can be useful if the DI container can't easily create an instance of the decorator.
Registering decorator classes in this way is very simple with Scrutor. If that's all you need, you can stop reading now. For those that like to peak behind the curtain, in the next section I describe how Scrutor goes about avoiding the new
construction in its Decorate<,>()
method.
Scrutor: Behind the curtain of the Decorate
method
In this section, I'm going to highlight some of the code behind the Decorate<,>
method of Scrutor, as it's interesting to see how Kristian achieves the same functionality I showed previously with the "native" approach.
The Decorate<,>
method itself is short enough (precondition checks removed - see the original code for details):
public static IServiceCollection Decorate<TService, TDecorator>(this IServiceCollection services)
where TDecorator : TService
{
return services.DecorateDescriptors(typeof(TService), x => x.Decorate(typeof(TDecorator)));
}
This extension method ultimately passes a factory function Func<>
which uses the non-generic Decorate()
to the DecorateDescriptors
method. The related TryDecorateDescriptors
method is shown below:
private static bool TryDecorateDescriptors(this IServiceCollection services, Type serviceType, Func<ServiceDescriptor, ServiceDescriptor> decorator)
{
if (!services.TryGetDescriptors(serviceType, out var descriptors))
{
return false;
}
foreach (var descriptor in descriptors)
{
var index = services.IndexOf(descriptor);
// To avoid reordering descriptors, in case a specific order is expected.
services.Insert(index, decorator(descriptor));
services.Remove(descriptor);
}
return true;
}
This method first fetches the services descriptors of all the registered TService
s.
ServiceDescriptor
is the underlying registration type in the ASP.NET Core DI container. it contains aServiceType
, anImplementationType
(orImplementationFactory
), and aLifetime
. When you register a service with the .NET Core container, you're actually adding an instance of aServiceDescriptor
to a collection.
If the service to be decorated, TService
, hasn't been registered, the method ends. If it has been registered, the TryDecorateDescriptors
method generates a decorated descriptor by calling the decorator
function provided as a parameter, and inserts it into the IServiceCollection
, replacing each original, un-decorated, service registration.
We still haven't quite seen the magic of the non-generic Decorate()
function yet (which was passed as the factory function in the Decorate<,>()
method). This method is shown below, with several intermediate methods inlined. It is implemented as an extension method on a ServiceDescriptor
, and takes a parameter decoratorType
, which is the decorating type (e.g. CachedAuthorRepository
from the earlier example).
private static ServiceDescriptor Decorate(this ServiceDescriptor descriptor, Type decoratorType)
{
var implementationFactory = provider =>
ActivatorUtilities.CreateInstance(provider, decoratorType, provider.GetInstance(descriptor));
return ServiceDescriptor.Describe(
serviceType: descriptor.ServiceType,
implementationFactory: implementationFactory,
lifetime: descriptor.Lifetime);
}
The really interesting part of this method is the use of ActivatorUtilities.CreateInstance()
. Given an IServiceProvider
, this method lets you partially provide the constructor parameters for a Type
, and lets the IServiceProvider
"fill in the blanks".
For example, say you have the following decorator class:
public class Decorator : IRepository
{
public Decorator(IRepository inner, IService somethingElse)
{
}
}
In this example, the Decorator
wraps the IRepository
instance, but has an additional dependency on IService
. Using the ActivatorUtilities.CreateInstance()
method, you can fill the IService
dependency automatically, while providing the explicit instance to use for the IRepository
dependency. For example:
IRepository repository = new Repository();
var decoratedService = ActivatorUtilities.CreateInstance(provider, typeof(Decorator), repository);
Even if you're not using Scrutor, you can make use of ActivatorUtilities.CreateInstance()
in the Microsoft.Extensions.DependencyInjection.Abstractions library to create instances of your classes. You could even use this in place of the "native" approach I showed previously. Though if you're going that far, I'd suggest just using Scrutor instead! 🙂
Summary
In this post I described the Decorator pattern, and showed how you can recreate it "natively" using the ASP.NET Core DI container. I then showed a better approach using the Decorate<,>()
method with Scrutor, and how this works under the hood. I strongly recommend you take a look at Scrutor if you want to augment the built-in DI container in your apps, but don't want to take the step to adding a third-party container.