Quantcast
Channel: Andrew Lock | .NET Escapades
Viewing all articles
Browse latest Browse all 743

Adding decorated classes to the ASP.NET Core DI container using Scrutor

$
0
0
Adding decorated classes to the ASP.NET Core DI container using Scrutor

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 interface IAuthorRepository 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's Decorate 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 an IAuthorRepository. This is useful as you don't have to directly control the registration yourself - it could be a different component/library that registers the AuthorRepository 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 the TService with TDecorator. Useful if you don't know whether the service is registered or not.
  • Decorate(Type serviceType, Type decoratorType). Non-generic version of the Decorate<,> method. Useful if your service or decorator are open generics like IRepository<>.
  • Decorate(Func<TService, IServiceProvider, TService> decorator). Provide a Func<> 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 TServices.

ServiceDescriptor is the underlying registration type in the ASP.NET Core DI container. it contains a ServiceType, an ImplementationType (or ImplementationFactory), and a Lifetime. When you register a service with the .NET Core container, you're actually adding an instance of a ServiceDescriptor 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.


Viewing all articles
Browse latest Browse all 743

Trending Articles