是否可以使用泛型类型的构造函数注入?

Bob*_*orn 3 c# generics dependency-injection autofac

我的事件处理程序当前具有如下类和方法签名:

public class MyEventHandler : IIntegrationEventHandler

public void Handle(IntegrationEventintegrationEvent) // IntegrationEvent is the base class
Run Code Online (Sandbox Code Playgroud)

我想做的是这样的(以便处理程序可以接受具体类型):

public class MyEventHandler : IIntegrationEventHandler<MyEvent>

public void Handle(MyEvent integrationEvent)
Run Code Online (Sandbox Code Playgroud)

在 Startup.cs 中,我这样做了:

services.AddTransient<IIntegrationEventHandler<MyEvent>, MyEventHandler>();
Run Code Online (Sandbox Code Playgroud)

问题是注入这些处理程序的服务不能使用开放泛型,如下所示:

public MyService(IEnumerable<IIntegrationEventHandler<>> eventHandlers)
            : base(eventHandlers)
Run Code Online (Sandbox Code Playgroud)

指定基类也不起作用:

public MyService(IEnumerable<IIntegrationEventHandler<IntegrationEvent>> eventHandlers)
            : base(eventHandlers)
Run Code Online (Sandbox Code Playgroud)

这给了我 0 个处理程序。

当前方法的工作原理是我注入了所有 7 个处理程序。但这意味着处理程序必须在其方法中接受基类,然后进行强制转换。没什么大不了的,但如果我能让处理程序接受它所关心的具体类型,那就太好了。这可能吗?

Zde*_*nek 5

您所请求的内容无法直接完成。C# 集合始终绑定到特定的项类型,如果需要不同的类型,则必须强制转换该类型。和IIntegrationEventHandler<MyEvent>IIntegrationEventHandler<DifferentEvent>不同的类型。

作为替代解决方案,您可以通过中介(调度程序)调度事件,并使用反射和 DI 将它们路由到具体的处理程序类型。处理程序将使用它们声明要处理的特定事件类型向 DI 注册。您还可以使用单个处理程序实现来处理多种类型的事件。调度程序将根据直接从 接收到的事件的运行时类型注入具体事件处理程序的集合IServiceProvider

我还没有编译或测试以下代码,但它应该可以让您有一个大概的了解。

启动.cs

services
    .AddTransient<IIntegrationEventHandler<MyEvent>, MyEventHandler>()
    .AddTransient<IntegrationEventDispatcher>();
Run Code Online (Sandbox Code Playgroud)

IntegrationEventDispatcher.cs

private readonly IServiceProvider _serviceProvider;

public IntegrationEventDispatcher(IServiceProvider serviceProvider)
{
    _serviceProvider = serviceProvider;
}

public void Dispatch(IntegrationEvent @event)
{
    var eventType = @event.GetType();

    // TODO: Possibly cache the below reflected types for improved performance
    var handlerType = typeof(IIntegrationEventHandler<>).MakeGenericType(eventType);
    var handlerCollectionType = typeof(IEnumerable<>).MakeGenericType(handlerType);
    var handlers = (IEnumerable)_serviceProvider.GetService(handlerCollectionType);

    var handleMethod = handlerType.GetMethod("Handle", eventType);

    foreach (var handler in handlers)
    {
        handleMethod.Invoke(handler, new[] { @event });
    }
}
Run Code Online (Sandbox Code Playgroud)

MyService.cs

// ...

private readonly IntegrationEventDispatcher  _eventDispatcher;

public MyService(IntegrationEventDispatcher eventDispatcher)
{
    _eventDispatcher = eventDispatcher;
}

public void DoStuffAndDispatchEvents()
{
    // ...
    _eventDispatcher.Dispatch(new MyEvent());
    _eventDispatcher.Dispatch(new DifferentEvent());
}
Run Code Online (Sandbox Code Playgroud)

编辑:

基于泛型的调度程序实现(要点):

public void Dispatch<TEvent>(TEvent @event) where TEvent : IntegrationEvent 
{
    var handlers = _serviceProvider.GetRequiredService<IEnumerable<IIntegrationEventHandler<TEvent>>>();

    foreach (var handler in handlers)
    {
        handler.Handle(@event);
    }
}
Run Code Online (Sandbox Code Playgroud)

编辑 2:为了支持基本类型的事件处理,我想到了几种方法:

a) 使用上面基于反射的方法。

从性能角度来看,这不会是最好的,但它适用于它将收到的任何类型。

b) 使用类型开关

打开事件类型以Dispatch<T>使用正确的类型进行调用。缺点是您需要列出所有支持的事件类型,并在引入任何新事件类型时更新列表。此外,通过测试发现这可能有些棘手。

逻辑IntegrationEventDispatcher变成

    public void Dispatch(IntegrationEvent @event)
    {
        switch (@event)
        {
            case MyIntegrationEvent e:
                Dispatch(e); // Calls Dispatch<TEvent> as the overload with a more specific type
                break;

            case OtherIntegrationEvent e:
                Dispatch(e);
                break;

            default:
                throw new NotSupportedException($"Event type {@event.GetType().FullName} not supported.");
        }
    }

    private void Dispatch<TEvent>(TEvent @event) where TEvent : IntegrationEvent
    {
        var handlers = _serviceProvider.GetRequiredService<IEnumerable<IEventHandler<TEvent>>>();

        foreach (var handler in handlers)
        {
            handler.Handle(@event);
        }
    }
Run Code Online (Sandbox Code Playgroud)

此处提供了具有最小实现的要点。

c) 使用访客模式

使基本事件类型接受访问具体事件类型的访问者。这是相当多的代码,并且需要更改基本事件类型。添加新事件类型时,会自动支持它,但如果使用重载而不是通用方法(如在正确的访问者中),则可能需要新的重载。

AIIntegrationEventVisitor应该存在于同一级别上IntegrationEvent- 这可能是一个体系结构问题,但是,由于事件已经被设计为对象,所以我希望具有行为不应该成为问题,尤其是这种抽象。

集成事件.cs

public abstract class IntegrationEvent
{
    public abstract void Accept(IIntegrationEventVisitor visitor);
}
Run Code Online (Sandbox Code Playgroud)

MyIntegrationEvent.cs

public class MyIntegrationEvent : IntegrationEvent
{
    public override void Accept(IIntegrationEventVisitor visitor)
    {
        visitor.Visit(this); // "this" is the concrete type so TEvent is inferred properly
    }
}
Run Code Online (Sandbox Code Playgroud)

IIntegrationEventVisitor.cs

public interface IIntegrationEventVisitor
{
    // Note: This is not a proper visitor, feel free to implement 
    // overloads for individual concrete event types for proper design.
    // Generic method is not very useful for a visitor in general
    // so this is actually an abstraction leak.
    void Visit<TEvent>(TEvent @event) where TEvent : IntegrationEvent;
}
Run Code Online (Sandbox Code Playgroud)

IntegrationEventDispatcher.cs

public class IntegrationEventDispatcher : IIntegrationEventVisitor
{
    // Previously known as Dispatch
    public void Visit<TEvent>(TEvent @event) where TEvent : IntegrationEvent
    {
        var handlers = _serviceProvider.GetRequiredService<IEnumerable<IEventHandler<TEvent>>>();

        foreach (var handler in handlers)
        {
            handler.Handle(@event);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

此处提供了具有最小实现的要点。

d) 退后一步

如果您想分派基本类型,也许注入具体处理程序根本不是您所需要的,可能还有另一种方法。

我们创建一个采用基本类型并仅在相关时处理它的事件处理程序怎么样?但是,我们不要在每个处理程序中重复这些相关性检查,让我们创建一个类,一般为我们执行此操作并委托给适当的处理程序。

我们将使用IIntegrationEventHandler通用处理程序来接受基本类型,使用泛型类型来实现它,检查接受的类型是否相关,然后将其转发到实现IIntegrationEventHandler<TEvent>.

这种方法将允许您按照您最初的要求在任何地方使用构造函数注入,并且通常感觉最接近您的原始方法。缺点是所有处理程序都会被实例化,即使它们没有被使用。这可以通过例如Lazy<T>具体处理程序集合来避免。

请注意,没有通用Dispatch方法或其任何变体,您只需注入一个集合,IIntegrationEventHandler其中每个委托都委托给集合IIntegrationEventHandler<TEvent>(如果接收到的事件类型为 )TEvent

IIntegrationEventHandler.cs

public interface IIntegrationEventHandler
{
    void Handle(IntegrationEvent @event);
}

public interface IIntegrationEventHandler<TEvent> where TEvent : IntegrationEvent
{
    void Handle(TEvent @event);
}
Run Code Online (Sandbox Code Playgroud)

DelegatingIntegrationEventHandler.cs

public class DelegatingIntegrationEventHandler<TEvent> : IIntegrationEventHandler where TEvent : IntegrationEvent
{
    private readonly IEnumerable<IEventHandler<TEvent>> _handlers;

    public DelegatingIntegrationEventHandler(IEnumerable<IEventHandler<TEvent>> handlers)
    {
        _handlers = handlers;
    }

    public void Handle(IntegrationEvent @event)
    {
        // Check if this event should be handled by this type
        if (!(@event is TEvent concreteEvent))
        {
            return;
        }

        // Forward the event to concrete handlers
        foreach (var handler in _handlers)
        {
            handler.Handle(concreteEvent);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

MyIntegrationEventHandler.cs

public class MyIntegrationEventHandler : IIntegrationEventHandler<MyIntegrationEvent>
{
    public void Handle(MyIntegrationEvent @event)
    {
        // ...
    }
}
Run Code Online (Sandbox Code Playgroud)

此处提供了具有最小实现的要点。检查一下实际的设置和使用情况,因为正确设置要稍微复杂一些。