Henk Mollema
Henk Mollema

Reputation: 46501

Mediator pattern and contravariance with Simple Injector

This question originates from the fact that I'm trying to create a Simple Injector implementation for MediatR: https://github.com/jbogard/MediatR/pull/14.

I'm having trouble while trying to resolve implementations of a generic handler interface. Consider the following notification handler interface:

public interface INotificationHandler<in TNotification>
    where TNotification : INotification
{
    void Handle(TNotification notification);
}

INotifcation is just an empty marker interface.

I defined the following handlers for the Pinged (which implements INotification) event:

public class PingedHandler : INotificationHandler<Pinged>
{
    public void Handle(Pinged notification) { }
}

public class PingedHandler2 : INotificationHandler<Pinged>
{
    public void Handle(Pinged notification) { }
}

And also a generic handler (notice this should handle every INotification):

public class GenericHandler : INotificationHandler<INotification>
{
    public void Handle(INotification notification) { }
}

With the following registration:

var container = new Container();

container.RegisterManyForOpenGeneric(
    typeof (INotificationHandler<>),
    (service, impls) => container.RegisterAll(service, impls),
    AppDomain.CurrentDomain.GetAssemblies());

Now I expect:

GetAllInstances<INotificationHandler<Pinged>>();

to resolve both PingedHandler and PingedHandler2 which it does. But it doesn't resolve the GenericHandler since it implements INotificationHandler<INotification> and not INotificationHandler<Pinged>. I wonder if there is a way to let Simple Injector search up the whole object graph and resolve anything that is Pinged too.

I've found a blog post from Steven about co-variance and contra-variance, but I'm unable to get it working for my example.

Upvotes: 4

Views: 2181

Answers (2)

Henk Mollema
Henk Mollema

Reputation: 46501

tl;dr: it was a bug/shortcoming in Simple Injector v2.6.0, which is fixed in v2.7.0. The configuration in the question (and shown below) does work now.


To summarize @qujck's answer and @Steven's comment: I was able to get it working by installing Simple Injector v2.7.0-beta2 with the following configuration (actually the same as in the question):

// Simple Injector v3.x
container.RegisterCollection(typeof(INotificationHandler<>),
    AppDomain.CurrentDomain.GetAssemblies());

// Simple Injector v2.x
container.RegisterManyForOpenGeneric(
    typeof(INotificationHandler<>),
    container.RegisterAll,
    AppDomain.CurrentDomain.GetAssemblies());

Now Simple Injector is able to resolve PingedHandler, PingedHandler2 and the GenericHandler when requesting:

container.GetAllInstances<INotificationHandler<Pinged>>();

Upvotes: 2

qujck
qujck

Reputation: 14580

UPDATE

As of Simple Injector release 2.7 this functionality is now standard and you no longer require the workaround shown below.


You are missing a variation of the MultipleDispatchEventHandler described in that article. Taking the basic logic and applying it to your abstractions does give the result you are expecting:

[Fact]
public void RegisterAll_Contravariant_Succeeds()
{
    var container = new Container();

    container.RegisterManyForOpenGeneric(
        typeof(INotificationHandler<>),
        (service, impls) => container.RegisterAll(service, impls),
        AppDomain.CurrentDomain.GetAssemblies());

    var handlersType = typeof(IEnumerable<INotificationHandler<Pinged>>);

    var handlersCollection = (
        from r in container.GetCurrentRegistrations()
        where handlersType.IsAssignableFrom(r.ServiceType)
        select r.GetInstance())
        .Cast<IEnumerable<INotificationHandler<Pinged>>>()
        .ToArray();

    var result = 
        from handlers in handlersCollection
        from handler in handlers
        select handler;

    Assert.Equal(3, result.Count());
}

But it may be easier to make GenericHandler generic:

public class GenericHandler<TNotification> : INotificationHandler<TNotification>
    where TNotification : INotification
{
    public void Handle(TNotification notification) { }
}

Giving a simpler registration

[Fact]
public void RegisterAll_WithOpenAndClosedGenerics_Succeeds()
{
    var container = new Container();

    var types = OpenGenericBatchRegistrationExtensions
        .GetTypesToRegister(
            container,
            typeof(INotificationHandler<>),
            AccessibilityOption.AllTypes,
            AppDomain.CurrentDomain.GetAssemblies())
        .ToList();

    types.Add(typeof(GenericHandler<>));

    container.RegisterAll(typeof(INotificationHandler<>), types);

    var result = container.GetAllInstances<INotificationHandler<Pinged>>().ToList();

    Assert.Equal(3, result.Count());
}

Upvotes: 3

Related Questions