10

I'm guessing there is no way to do something like the following with Autofac, to ctor inject an enumerable collection of open generic types? The various Handle types have dependencies, otherwise I would just dynamically build those up.

    class EventOne : IEvent {...}
    class EventTwo : IEvent {...}
    class EventThree : IEvent {...}
    interface IHandleEvent<T> where T : IEvent {...}
    class HandleEventOne : IHandleEvent<EventOne> {...}
    class HandleEventTwo : IHandleEvent<EventTwo> {...}
    class HandleEventThree : IHandleEvent<EventThree> {...}

    builder.RegisterAssemblyTypes(myAssembies).AsClosedTypesOf(typeof(IHandleEvent<>));
    builder.RegisterType<AService>().As<IAService>();


    class AService : IAService
    {
      public AService(IEnumerable<IHandleEvent<IEvent>> handles)
      {...}
    }
Steven
  • 166,672
  • 24
  • 332
  • 435
Suedeuno
  • 389
  • 7
  • 21
  • `IEnumerable>` would need to be resolved to exactly 1 concrete type. If you have a type that can dynamically build itself (see Builder pattern) you could define that and register it with AutoFac as the registration for `IEnumerable>`. You could also inject the DI container in that type if you need to retrieve additional info to build it. In short, I think you need a builder or factory pattern here to resolve your `IEnumerable>` instance. – Igor Apr 05 '16 at 20:32
  • 1
    Thanks. I took a look at the delegate factories autofac doc. Sounds like the in this case the container would get injected into the factory to resolve the various types of IHandleEvent<>. – Suedeuno Apr 05 '16 at 21:18
  • Try chaning your `IHandleEvent` to `IHandleEvent`. Autofac has some support for variance and might pick up the registrations automatically when you do that. But I always forget whether Autofac support covariance or contravariance, so you'll have to try it out. – Steven Apr 06 '16 at 09:00
  • 1
    @Steven *Autofac* supports only contravariance – Cyril Durand Apr 06 '16 at 17:15

2 Answers2

11

As explained in the comments, what you want is impossible to achieve in C# and with good reason. If you were able to cast an IHandleEvent<EventOne> to an IHandleEvent<IEvent> it would allow an EventTwo to be passed in as well, which would fail at runtime.

So what you need is an mediator abstraction that allow getting all the compatible event handlers and call them. Such mediator is often called IEventPublisher and might look like this:

public interface IEventPublisher {
    void Publish(IEvent e);
}

You can now create a container specific implementation. For instance, for Autofac this would look as follows:

public class AutofacEventPublisher : IEventPublisher {
    private readonly IComponentContext container;

    public AutofacBusinessRuleValidator(IComponentContext container) {
        this.container = container;
    }

    public void Publish(IEvent e) {
        foreach (dynamic handler in this.GetHandlers(e.GetType())) {
            handler.Handle((dynamic)e);
        }
    }

    private IEnumerable GetHandlers(Type eventType) =>
        (IEnumerable)this.container.Resolve(
            typeof(IEnumerable<>).MakeGenericType(
                typeof(IHandleEvent<>).MakeGenericType(eventType)));
}

Consumers can now depend on this new abstraction:

class AService : IAService
{
    public AService(IEventPublisher publisher) {...}
}
Steven
  • 166,672
  • 24
  • 332
  • 435
  • So only way to do this with autofac is with locator pattern to resolve the types. I may just close the type and use generic methods so that autofac can naturally handle the injection. – Suedeuno Apr 07 '16 at 15:23
  • @Suedeuno: You're incorrect. What I am showing is *not* a *Service Locator*, as long as the `AutofacEventPublisher` is part of your [Composition Root](http://blog.ploeh.dk/2011/07/28/CompositionRoot/), because Service Locator is about [the role, not the mechanics](http://blog.ploeh.dk/2011/08/25/ServiceLocatorrolesvs.mechanics/). But you are right in that having a mediating abstraction is the only way; not only in Autofac, but with any container or even without a container. That's because of the abstractions you use. – Steven Apr 07 '16 at 17:09
  • So it doesn't matter that AutofacEventPublisher resides outside the startup module (mvc ui assembly for instance as noted in the blog post you referenced)? – Suedeuno Apr 07 '16 at 18:43
  • @Suedeuno: You should see the *Composition Root* as a layer. It's a complete layer on top of your (MVC) presentation layer. In case of a MVC project its very common to have both Presentation Layer and Composition Root layer exist in the same assembly. Also read [this](https://stackoverflow.com/a/9505530/264697). – Steven Apr 07 '16 at 20:41
  • 1
    I agree with you, and I've seen the DI delegated to a separate assembly in the case of more than one client. I've seen code with similar semantics where the event publisher exists in the domain layer which doesn't feel right to me even though it is designed similarly to AutofacEventPublisher. – Suedeuno Apr 07 '16 at 21:25
  • But the domain has to call Publish so I'm not sure that this would work otherwise there is circular references. – Suedeuno Apr 07 '16 at 22:14
  • @Suedeuno you should define the interface in a core layer of your application and the implementation in the composition root. – Steven Apr 08 '16 at 05:42
  • Yes, I knew that, I needed to just logoff before I posted that last one because I was clearly done thinking for the day :). Thanks for your help. – Suedeuno Apr 08 '16 at 12:24
0

You won't be able to cast IHandleEvent<EventThree> to IHandleEvent<IEvent> because IHandleEvent<T> is not a covariant you can add it by adding the out modifier.

public interface IHandleEvent<out TEvent> 
    where TEvent : IEvent
{ }

Unfortunately Autofac doesn't support covariant type but only contravariant type. By the way you can create a custom IRegistrationSource implementation to have the requested behavior. Something like this :

public class CovariantHandleEventRegistrationSource : IRegistrationSource
{
  public bool IsAdapterForIndividualComponents
  {
    get
    {
      return false;
    }
  }

  public IEnumerable<IComponentRegistration> RegistrationsFor(Service service, 
                                Func<Service, IEnumerable<IComponentRegistration>> registrationAccessor)
  {
    IServiceWithType typedService = service as IServiceWithType;
    if (typedService == null)
    {
      yield break;
    }

    if (typedService.ServiceType.IsGenericType && typedService.ServiceType.GetGenericTypeDefinition() == typeof(IHandleEvent<>))
    {
      IEnumerable<IComponentRegistration> eventRegistrations = registrationAccessor(new TypedService(typeof(IEvent)));

      foreach (IComponentRegistration eventRegistration in eventRegistrations)
      {
        Type handleEventType = typeof(IHandleEvent<>).MakeGenericType(eventRegistration.Activator.LimitType);
        IComponentRegistration handleEventRegistration = RegistrationBuilder.ForDelegate((c, p) => c.Resolve(handleEventType, p))
                                          .As(service)
                                          .CreateRegistration();

        yield return handleEventRegistration;
      }
    }
  }
}

With this IRegistrationSource you can have this :

ContainerBuilder builder = new ContainerBuilder();
builder.RegisterType<EventOne>().As<IEvent>();
builder.RegisterType<EventTwo>().As<IEvent>();
builder.RegisterType<EventThree>().As<IEvent>();
builder.RegisterAssemblyTypes(typeof(Program).Assembly)
        .AsClosedTypesOf(typeof(IHandleEvent<>));

builder.RegisterSource(new CovariantHandleEventRegistrationSource());

IContainer container = builder.Build();

var x = container.Resolve<IEnumerable<IHandleEvent<IEvent>>>();
Cyril Durand
  • 15,834
  • 5
  • 54
  • 62
  • 1
    Contravariance is actually what OP needs; not covariance. An `IHandleEvent` will always accept a `T` as input argument (a handler is unlikely to return an event). In other words: `IHandleEvent`. – Steven Apr 06 '16 at 20:41
  • @Steven you're right. Based on the name of the interface, it seems that `IHandleEvent` will have a `Handle(T e)` method, so contravariance is needed. In this case, even without `Autofac` there is no C# way to build a `IHandleEvent`, the `AService` should be generic and have a dependency on `IHandleEvent`. – Cyril Durand Apr 06 '16 at 21:19
  • 1
    Yes, you are right. To be able to cast a collection of `IHandleEvent` to `IHandleEvent` you need covariance (i.e. apply the `out` keyword), but event handlers obviously need to be contravariant (i.e. use the `in` keyword). So what the OP wants is impossible. – Steven Apr 07 '16 at 08:34