Get specific interface for a single specified concrete type, with decorators applied with Simple Injector

有些话、适合烂在心里 提交于 2020-05-28 05:46:06

问题


I am using SimpleInjector with MediatR and hooking up my INotifications and INotificationHandlers<INotification> with attributes on my implementation classes - this is so we can map message bus messages (aka notifications) to notification handlers:

  1. Host app receives message from bus
  2. Message is parsed into Notification POCO
  3. Container is used to look up notification handler(s)
  4. Notification is dispatched to each handler

Traditionally one would register all INotificationHandler<> with Container.GetTypesToRegister() and Container.Collection.Register(), and all would be well in that all handlers would be returned by the container for that specific INotification.

In this case, I wish to get a specific concrete instances that implements the specific INotificationHandler<SpecificEvent> to be called. We decide what is called with attributes:

[MessageBusSubscription("v1/alerts/fire"]
class FirstClass : INotificationHandler<Aggregate>
{
    public Task Handle(Aggregate notification, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }
}

[MessageBusSubscription("v1/alerts/fire"]
class SecondClass : INotificationHandler<Aggregate>
{
    public Task Handle(Aggregate notification, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }
}

[MessageBusSubscription("v1/alerts/leak"]
class OtherClass : INotificationHandler<Aggregate>
{
    public Task Handle(Aggregate notification, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }
}

I know I could get and convert to the interface (this cast would be safe as during the registration bootstrap infrastructure would enforce checking that the type implements the interface):

Container Bootstrap()
{
    var container = new Container();

    // we cannot register the below with Container.Collection.Register as we wont be finding the registration
    // by INotificationHandler<INotification> but rather by the specific class (we could auto-wire by reflection)
    container.Register<FirstClass>();
    container.Register<OtherClass>();

    // other bootstrap code, including registration of all other INotificationHandler<> that do not have
    // MessageBusSubscriptionAttribute in the cases where we want to publish to all implementations
    // that are not scoped to a specific topic subscription or message type

    return container;
}

IEnumerable<INotificationHandler<TNotification>>> GetConcreteTypeThatImplementsNotificationHandlers<TNotification>(string topic)
    where TNotification : INotification
{
    // TopicRegistrations consists of the following:
    // { "v1/alerts/fire", [] { typeof(FirstClass), typeof(SecondClass) }
    // { "v1/alerts/leak", [] { typeof(OtherClass) }

    // based on notification type and topic string, get concrete implementation
    var concreteTypes = TopicRegistrations.GetTypeForTopic(topic);

    foreach(var type in concreteTypes)
    {
        var instance = Container.GetInstance(type);
        yield return (INotificationHandler<TNotification>>)instance;
    }
}

I could then call the above as if it were a INotificationHandler for that specific notification type and only for specific concrete implementation(s) based on attribute metadata.

However, how would I ensure that any decorators (like logging or error handlers etc) are still applied to the NotificationHandler fetched when using the above code approach?

Ultimately I am looking for a Container function similar to the below that would return the INotificationHandler for only the provided concrete type, and have decorators (if any) applied:

var notficationHandlerType = typeof(INotificationHandler<>).MakeGenericType(notificationFromBus.GetType());
Container.GetRegistrationOnlyForConcreateType(notficationHandlerType, concreteType);

Anything that could be done to make this cleaner or missing any risks that could blow up or code smells?

Update

Thanks to Steven for his wonderful code, thanks for inspiring us to code better. I have adapted it slightly so that it takes a type, and the specific handlers to register for that type:

public class TestNotification : INotification { }

public class DifferentTestNotification : INotification { }

public class OtherClass : INotificationHandler<TestNotification>, INotificationHandler<DifferentTestNotification>
{
    public Task Handle(TestNotification notification, CancellationToken cancellationToken)
        { throw new NotImplementedException(); }

    public Task Handle(DifferentTestNotification notification, CancellationToken cancellationToken)
    { throw new NotImplementedException(); }
}

/* repeat the exact same for AmazingClass and AllStars giving 3 identical classes for which we can selectively register different handlers later */

And pass a dictionary of the handlers that I wish to register:

registrationTypes = new Dictionary<Type, Type[]>()
{
    { typeof(OtherClass), new [] { typeof(INotificationHandler<>).MakeGenericType(typeof(TestNotification)) } },
    { typeof(AmazingClass), new [] { typeof(INotificationHandler<>).MakeGenericType(typeof(DifferentTestNotification)) } },
    { typeof(AllStars), new [] { typeof(INotificationHandler<>).MakeGenericType(typeof(TestNotification)), typeof(INotificationHandler<>).MakeGenericType(typeof(DifferentTestNotification)) } }
};

Into the below registration class:

public class NotifcationProducerRegistrations
{
    readonly ConcurrentDictionary<Type, Dictionary<Type, InstanceProducer>> handlerProducers
        = new ConcurrentDictionary<Type, Dictionary<Type, InstanceProducer>>();

    public void AddProducer(Type notificationType, Type concreteType, InstanceProducer producer)
    {
        this.handlerProducers.AddOrUpdate(
            notificationType,
            (key) => new Dictionary<Type, InstanceProducer> { { concreteType, producer } },
            (key, dictionary) => { dictionary.Add(concreteType, producer); return dictionary; });
    }

    public IEnumerable<InstanceProducer> GetRegistrations(Type notificationType)
    {
        if(this.handlerProducers.TryGetValue(notificationType, out var dict))
        {
            foreach (var kvp in dict)
                yield return kvp.Value;
        }
    }
}

And register as follows:

public static NotifcationProducerRegistrations GetNotificationHandlerProducersTest(this Container container, Dictionary<Type, Type[]> registrationTypes, Lifestyle lifestyle)
{

    var registrations = new NotifcationProducerRegistrations();

    foreach (var registration in registrationTypes)
    {
        var concreteType = registration.Key;
        var notificationHandlerTypes = registration.Value;
        var interfaceTypes = concreteType.GetClosedTypesOf(typeof(INotificationHandler<>));
        foreach(var filteredInterfaceType in interfaceTypes.Intersect(notificationHandlerTypes))
        {
            registrations.AddProducer(
                            filteredInterfaceType,
                            concreteType,
                            lifestyle.CreateProducer(filteredInterfaceType, concreteType, container));
        }
    }

    return registrations;
}

With reference to my comment, with the above I have a 1:1 relation between producer for a specific type of concrete type, within the array of other concrete types for that notification type.

As mentioned, I am currently seeing an array whereas i think believe (and may be wrong) that i should only need to get one producer, similar to the below map:


回答1:


In your case, prevent adding the INotificationHandler<T> registrations directly to the container using:

container.Register<FirstClass>();

Doing so would register the class by its concrete type, which disallows decorators from being applied. Instead, you would typically use the following registration:

container.Register<INotificationHandler<Aggregate>, FirstClass>();

It allows decorators on INotificationHandler<T> to be applied. This, however, still wouldn't work in your case, because there are multiple implementations of the same abstaction. So instead, you would normally register it as a collection, as you mentioned in your question:

container.Collection.Register(typeof(INotificationHandler<>),
    typeof(FirstClass).Assembly);

But this would work well and allows you to filter a returned collection based on attributes that are placed on the implementation... as long as you don't wrap implementations using decorators, because in that case you would be checking the attributes on the outer-most decorator. This is obviously what you want to achieve.

The solution to your quest is to -not- register those components in Simple Injector's internal resolve-dictionary (i.e. using one of the Register calls), but instead create InstanceProducer instances 'manually' and store them into yourself:

IEnumerable<Type> handlerTypes =
    container.GetTypesToRegister(typeof(INotificationHandler<>),
        typeof(FirstClass).Assembly);

Dictionary<Type, Dictionary<Type, InstanceProducer>> handlerProducers = (
    from handlerType in handlerTypes
    from interfaceType in handlerType.GetClosedTypesOf(typeof(INotificationHandler<>))
    let producer =
        Lifestyle.Transient.CreateProducer(interfaceType, handlerType, container)
    group new { handlerType, producer } by interfaceType into interfaceGroup
    select new
    {
        MessageType = interfaceGroup.GetGenericArguments().Single(),
        Producers = interfaceGroup.ToDictionary(i => i.handlerType, i => i.producer)
    })
    .ToDictionary(i => i.MessageType, i => i.Producers);

The call to GetClosedTypesOf gets all closed versions of INotificationHandler<T> for the given handlerType. A handler can implement multiple interfaces, and GetClosedTypesOf will return all closed versions of INotificationHandler<T> that the handlerTyp implements.

That interfaceType must be used to create the InstanceProducer. This is the equivalent of calling Register(interfaceType, handlerType) as it allows Simple Injector to apply decorators based on the interface type.

Registrations created using Lifestyle.CreateProducer can't be resolved by calling Container.GetInstance, but they are still part of verification process. They are verified and diagnosed, just like any other registration. Instead of calling Container.GetInstance, an InstanceProducer contains its own GetInstance method. For instance:

IEnumerable<INotificationHandler<TNotification>>>
    GetConcreteTypeThatImplementsNotificationHandlers<TNotification>(string topic)
    where TNotification : INotification
{
    var concreteTypes = TopicRegistrations.GetTypeForTopic(topic);

    var notificationHandlerProducers = this.handlerProducers[typeof(TNotification)];

    foreach(var type in concreteTypes)
    {
        var instance = notificationHandlerProducers[type].GetInstance();
        yield return (INotificationHandler<TNotification>>)instance;
    }
}


来源:https://stackoverflow.com/questions/61176338/get-specific-interface-for-a-single-specified-concrete-type-with-decorators-app

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!