27

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?

Steven
  • 166,672
  • 24
  • 332
  • 435
Miguel Moura
  • 36,732
  • 85
  • 259
  • 481

7 Answers7

16

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.

Tseng
  • 61,549
  • 15
  • 193
  • 205
  • True perhaps in 2016 - see my code example below where I do this (in 2022, .net6) although it's a custom extension method and still not "out-of-the-box" – GrahamB Oct 29 '22 at 15:58
16

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

VivekDev
  • 20,868
  • 27
  • 132
  • 202
6

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>();
    });
sich
  • 505
  • 6
  • 10
5

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.

Lukas Körfer
  • 13,515
  • 7
  • 46
  • 62
Willie
  • 106
  • 2
  • 3
  • 2
    The Scrutor mention was very useful. Nice library there. It's impressive that they managed to create the entire mechanism using standard registration for descriptor classes. – julealgon Mar 20 '18 at 13:58
  • 2
    Note that Scrutor doesn't curently support calling Dispose on decorated objects when a DI scope ends. I filed an issue on GitHub for this, hope they will fix it eventually: https://github.com/khellang/Scrutor/issues/91 – sich May 30 '19 at 06:17
1

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

xSx
  • 146
  • 4
1

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);

GrahamB
  • 1,368
  • 15
  • 35
  • I like what you made here without using a third-party package! Although not really a out-of-the-box solution, but still. Quick question about `Constraint.Requires`, is it a self developed functionality or a package? – Michel Dec 15 '22 at 08:04
  • Sorry yes, I hate it when I get that in examples on SO it’s an internal class that’s takes a Boolean statement and throws or not as appropriate. I guess it’s OOB without needing a third party package, as you say, but it’s not built into Microsoft DI – GrahamB Dec 16 '22 at 14:40
  • @GrahamB - would you consider posting on that issue to find out the author's thoughts? – ajbeaven May 10 '23 at 03:58
  • @ajbeaven on scrutor? – GrahamB May 17 '23 at 11:56
0

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);
Zar Shardan
  • 5,675
  • 2
  • 39
  • 37