Miguel Moura
Miguel Moura

Reputation: 39394

How to apply decorators with ASP.NET Core Dependency Injection

On an ASP.NET MVC 5 application I have the following StructureMap configuration:

cfg.For(typeof(IRequestHandler<,>)).DecorateAllWith(typeof(MediatorPipeline<,>));

Does anyone know how to do this configuration with ASP.NET Core IOC?

Upvotes: 31

Views: 29386

Answers (7)

GrahamB
GrahamB

Reputation: 1428

None of these answers appear to fulfil the question - i.e. "how do we annotate a generic with a generic without specifying the type?". As the question is pretty old, this may not have been possible at the time. When you look at scrutor (from 2017) the answer is "you can't due to the underying DI framework" - https://github.com/khellang/Scrutor/issues/39

I'm really confused by this as I've managed to get this working out of the box with the Microsoft DI framework. Can anyone see any issues with this?

Thanks to those who got decorators working in the first place.

public static IServiceCollection AddDecorator(this IServiceCollection services, Type matchInterface, Type decorator, params Assembly[] assemblies)
{
    Constraint.Requires(matchInterface.IsInterface, "Must be an interface to match");
    Constraint.Requires(!decorator.IsInterface, "Must be a concrete type");
    Constraint.Requires(assemblies.Length > 0, "Must provide at least one assembly for scanning for decorators");

    var decoratedType = assemblies.SelectMany(t => t.GetTypes())
      .Distinct()
      .SingleOrDefault(t => t == decorator.GetGenericTypeDefinition());

    if (decoratedType == null)
    {
        throw new InvalidOperationException($"Attempted to decorate services of type {matchInterface.Name} with decorator {decorator.Name} but no such decorator found in any scanned assemblies.");
    }

    foreach (var type in services
      .Where(sd =>
      {
         try
         {
           return sd.ServiceType.GetGenericTypeDefinition() == matchInterface.GetGenericTypeDefinition();
         }
         catch (InvalidOperationException)
         {
           return false;
         }
      }).ToList())
    {
        var decoratedInstanceType = decoratedType.MakeGenericType(type.ServiceType.UnderlyingSystemType.GenericTypeArguments);

        //Create the object factory for our decorator type, specifying that we will supply the interface injection explicitly
        var objectFactory = ActivatorUtilities.CreateFactory(decoratedInstanceType, new[] {type.ServiceType});

        //Replace the existing registration with one that passes an instance of the existing registration to the object factory for the decorator 
        services.Replace(ServiceDescriptor.Describe(
           type.ServiceType,
           s => objectFactory(s, new[] {s.CreateInstance(type)}),
           type.Lifetime));
    }
    return services;
}

Usage:

services
       .AddDecorator(typeof(IAsyncCommandHandler<>), typeof(LoggingCommandDecorator<>), typeof(LoggingCommandDecorator<>).Assembly)
       .AddDecorator(typeof(IAsyncCommandHandler<>), typeof(TracingCommandDecorator<>), typeof(TracingCommandDecorator<>).Assembly)
       .AddDecorator(typeof(IAsyncQueryHandler<,>), typeof(TracingQueryDecorator<,>), typeof(TracingQueryDecorator<,>).Assembly);

Upvotes: 2

Zar Shardan
Zar Shardan

Reputation: 5921

If for whatever reason you can't use Scrutor, this might help:

public static class ServiceCollectionExtensions
{
    public static void AddWithDecorators<TService, TImplementation>(
        this ServiceCollection serviceCollection, IEnumerable<Type> decorators, ServiceLifetime serviceLifetime)
    {
        serviceCollection.Add(new ServiceDescriptor(typeof(TImplementation), typeof(TImplementation), serviceLifetime));
        
        var inner = typeof(TImplementation);
        foreach (var decoratorType in decorators)
        {
            var innerCopy = inner;
            
            var sd = new ServiceDescriptor(decoratorType,
                    sp => ActivatorUtilities.CreateInstance(sp, decoratorType, sp.GetRequiredService(innerCopy)), 
                    serviceLifetime);
            
            serviceCollection.Add(sd);
            inner = decoratorType;
        }

        serviceCollection.Add(new ServiceDescriptor(typeof(TService), sp => sp.GetRequiredService(inner), serviceLifetime));
    }
    
    public static void AddWithDecorator<TService, TImplementation, TDecorator>(this ServiceCollection serviceCollection,
        ServiceLifetime serviceLifetime)
        => AddWithDecorators<TService, TImplementation>(
            serviceCollection, 
            new[] { typeof(TDecorator) },
            serviceLifetime);
    
    public static void AddWithDecorators<TService, TImplementation, TDecorator1, TDecorator2>(
        this ServiceCollection serviceCollection, ServiceLifetime serviceLifetime)
        => AddWithDecorators<TService, TImplementation>(
            serviceCollection, 
            new[] { typeof(TDecorator1), typeof(TDecorator2) },
            serviceLifetime);
    
    public static void AddWithDecorators<TService, TImplementation, TDecorator1, TDecorator2, TDecorator3>(
        this ServiceCollection serviceCollection, ServiceLifetime serviceLifetime)
        => AddWithDecorators<TService, TImplementation>(
            serviceCollection, 
            new[] { typeof(TDecorator1), typeof(TDecorator2), typeof(TDecorator3) },
            serviceLifetime);
}

usage:

var sc = new ServiceCollection();

sc.AddWithDecorators<IStore<NamedEntity>, SimpleStore<NamedEntity>, CachedStore<NamedEntity>, WrapperStore<NamedEntity>>(ServiceLifetime.Singleton);

or

sc.AddWithDecorators<IStore<NamedEntity>, SimpleStore<NamedEntity>>(new[] { typeof(CachedStore<NamedEntity>), typeof(WrapperStore<NamedEntity>) },
    ServiceLifetime.Singleton);

Upvotes: 0

VivekDev
VivekDev

Reputation: 25427

Use Scrutor. Just install the nuget package and then do the following.

services.AddSingleton<IGreeter, Greeter>();
services.Decorate<IGreeter, GreeterLogger>();
services.Decorate<IGreeter, GreeterExceptionHandler>();

The order is important. In the above, GreeterLogger decorates Greeter. And GreeterExceptionHandler decorates GreeterLogger.

If you need more info, take a look at this and this.

And of course, you can use the popular Autofac as well.

If you want to know how to configure Autofac, take a look at Ardalis Clean Arch template

Upvotes: 22

xSx
xSx

Reputation: 146

one of another example

services.AddTransient<Greeter>();
services.AddTransient<IGreeter>(g=>
   ActivatorUtilities.CreateInstance<GreeterLogger>(g,g.GetRequiredServices<Greeter>())
);

or generic

private static void AddTransientDecorated<TInterface,TService,TDecorator>(this IServiceCollection services)
{
    services.AddTransient(typeof(TService));
    services.AddTransient(typeof(TInterface), p => ActivatorUtilities.CreateInstance<TDecorator>(p, p.GetRequiredService<TService>()));
}

additional information .NET Core DI, ways of passing parameters to constructor

Upvotes: 1

sich
sich

Reputation: 630

In my blogpost I described how a relatively simple extension method can solve this problem easily. Here is an example from that post which shows how decorator configuration may look like:

services.AddDecorator<IEmailMessageSender, EmailMessageSenderWithRetryDecorator>(decorateeServices =>
    {
        decorateeServices.AddScoped<IEmailMessageSender, SmtpEmailMessageSender>();
    });

Upvotes: 6

Willie
Willie

Reputation: 106

This workaround doesn't apply the decorator to all instances of a type but uses extension methods to abstract the decorator logic into another file.

Defining the decorator structure like:

public static class QueryHandlerRegistration
{
    public static IServiceCollection RegisterQueryHandler<TQueryHandler, TQuery, TResult>(
        this IServiceCollection services) 
        where TQuery : IQuery<TResult>
        where TQueryHandler : class, IQueryHandler<TQuery, TResult>
    {
        services.AddTransient<TQueryHandler>();
        services.AddTransient<IQueryHandler<TQuery, TResult>>(x =>
            new LoggingDecorator<TQuery, TResult>(x.GetService<ILogger<TQuery>>(), x.GetService<TQueryHandler>()));
        return services;
    }
}

And calling it like:

services.AddMvc();
// Add application services.
services.AddTransient<IEmailSender, AuthMessageSender>();
services.AddTransient<ISmsSender, AuthMessageSender>();

services.RegisterQueryHandler<FindThingByIdQueryHandler, FindThingByIdQuery, Thing>();

There's also the Scrutor package being worked on.

Upvotes: 5

Tseng
Tseng

Reputation: 64180

The out of the box IoC container doesn't support decorate pattern or auto discovery, which is "by design" as far as I know.

The idea is to provide a basic IoC structure that works out of the box or where other IoC containers can be plugged in to extend the default functionality.

So if you need any advanced features (support for a specific constructor, auto-registering of all types which implement an interface or inject decorators and interceptors) you have to either write it yourself or use an IoC container which offers this functionality.

Upvotes: 17

Related Questions