如何在ASP.NET Core依赖注入中应用装饰器

27

在一个ASP.NET MVC 5应用程序中,我有以下StructureMap配置:

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

有人知道如何在ASP.NET Core IOC中进行此配置吗?


只需使用Scrutor。请查看我的下面的答案。 - VivekDev
7个回答

17

使用Scrutor。只需安装nuget包,然后执行以下操作。

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

顺序很重要。在上面的例子中, GreeterLogger 装饰了 Greeter。而 GreeterExceptionHandler 又装饰了 GreeterLogger。

如果需要更多信息,请查看 这个这个

当然,您也可以使用流行的 Autofac 框架。

如果想了解如何配置 Autofac,请查看 Ardalis Clean Arch 模板


16

开箱即用的IoC容器没有支持装饰器模式或自动发现功能,据我所知这是“有意为之”的设计。

其想法是提供一个基本的IoC结构,可直接使用,或可以将其他IoC容器插入以扩展默认功能。

因此,如果您需要任何高级功能(例如支持特定构造函数、自动注册实现接口的所有类型或注入装饰器和拦截器),您必须编写自己的代码或使用提供此功能的IoC容器。


可能在2016年就已经是真的了 - 请看下面我的代码示例,我在2022年(.net6)做到了这一点,尽管它是一个自定义扩展方法,仍然不是“开箱即用”的。 - GrahamB

6
在我的博客文章中,我描述了如何使用相对简单的扩展方法轻松解决这个问题。以下是该文章中的一个示例,展示了装饰器配置可能的样子:
services.AddDecorator<IEmailMessageSender, EmailMessageSenderWithRetryDecorator>(decorateeServices =>
    {
        decorateeServices.AddScoped<IEmailMessageSender, SmtpEmailMessageSender>();
    });

5

这个解决方法并没有将装饰器应用到某个类型的所有实例中,而是使用扩展方法将装饰器逻辑抽象成另一个文件。

定义装饰器结构如下:

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

并像这样调用:

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

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

目前还有Scrutor包正在开发中。


2
Scrutor提到非常有用。那是一个不错的库。令人印象深刻的是,他们成功地使用标准注册描述符类来创建整个机制。 - julealgon
2
请注意,Scrutor 目前不支持在 DI 作用域结束时调用已装饰对象的 Dispose 方法。我已在 GitHub 上提交了一个问题,希望他们最终能够解决:https://github.com/khellang/Scrutor/issues/91 - sich

2

这些回答似乎都没有满足问题的要求,即“如何在不指定类型的情况下对泛型进行注释?”由于该问题比较久远,当时可能还无法实现。当您查看Scrutor(来自2017年)时,答案是“由于基础DI框架,您无法这样做”-https://github.com/khellang/Scrutor/issues/39

我对此感到非常困惑,因为我已经可以在Microsoft DI框架中直接使用它了。有人能看到任何问题吗?

感谢那些首先使装饰器起作用的人们。

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

使用方法:

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


我喜欢你在这里做的东西,而不使用第三方包!虽然这不是一个真正的开箱即用的解决方案,但仍然很好。关于 Constraint.Requires 的快速问题,它是自己开发的功能还是一个包? - Michel
抱歉,是的,我讨厌在 SO 的示例中遇到这种情况。它是一个内部类,接受一个布尔语句并根据需要抛出异常。我猜它是 OOB 的,不需要第三方包,就像你说的那样,但它并没有内置于 Microsoft DI 中。 - GrahamB
@GrahamB - 你是否考虑发布关于那个问题以了解作者的想法? - ajbeaven
@ajbeaven on scrutor? - GrahamB

1

另一个例子之一

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

或者通用
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>()));
}

额外信息 .NET Core DI,传递参数给构造函数的方法


0

如果由于某种原因您无法使用Scrutor,这可能会有所帮助:

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

用法:

var sc = new ServiceCollection();

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

或者

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接