Customizing Autofac's component resolution / Issue with generic co-/contravariance
Asked Answered
C

3

19

First, sorry for the vague question title. I couldn't come up with a more precise one.

Given these types:

                                                     { TCommand : ICommand }
       «interface»                   «interface»    /
      +-----------+         +----------------------/----+
      | ICommand  |         | ICommandHandler<TCommand> |
      +-----------+         +---------------------------+
            ^               | Handle(command: TCommand) |
            |               +---------------------------+
            |                              ^
            |                              |
      +------------+            +-------------------+
      | FooCommand |            | FooCommandHandler |
      +------------+            +-------------------+
            ^
            |
   +-------------------+
   | SpecialFooCommand |
   +-------------------+

I would like to write a method Dispatch that accepts any command and sends it to an appropriate ICommandHandler<>. I thought that using a DI container (Autofac) might greatly simplify the mapping from a command's type to a command handler:

void Dispatch<TCommand>(TCommand command) where TCommand : ICommand
{
    var handler = autofacContainer.Resolve<ICommandHandler<TCommand>>();
    handler.Handle(command);
}

Let's say the DI container knows about all the types shown above. Now I'm calling:

Dispatch(new SpecialFooCommand(…));

In reality, this will result in Autofac throwing a ComponentNotRegisteredException, since there is no ICommandHandler<SpecialFooCommand> available.

Ideally however, I would still want a SpecialFooCommand to be handled by the closest-matching command handler available, ie. by a FooCommandHandler in the above example.

Can Autofac be customized towards that end, perhaps with a custom registration source?


P.S.: I understand that there might be the fundamental problem of co-/contravariance getting in the way (as in the following example), and that the only solution might be one that doesn't use generics at all... but I would want to stick to generic types, if possible.

ICommandHandler<FooCommand> fooHandler = new FooCommandHandler(…);
ICommandHandler<ICommand> handler = fooHandler;
//                                ^
//              doesn't work, types are incompatible
Cecilycecity answered 10/8, 2011 at 11:43 Comment(5)
+1 for the way you represent your question.Sellma
Daniel's suggestions below are a good approach, in any case the in parameter modifier is necessary. It is possible to support contravariant Resolve() using an IRegistrationSource as you have guessed. I've put them together in the past but don't have the code with me, if I can hack one up I'll post it for you.Charlie
I like to question the usefulness of this in your particular situation. When you create a special command sub type, how can the handler for the base type ever handle that command correctly, since it won’t see the extra properties that the sub command adds? Wouldn’t the cases were it would execute correctly be exceptional? In that corner case it would be easy to define a SpecialFooCommandHandler that wraps on an ICommandHandler<FooCommand> does nothing more than calling this.wrapped.Handle(command) in its Handle method.Piled
@Piled more commonly this approach is used to handle events rather than commands. In that context, calling say IHandler<IAuditableEvent> in addition to IHandler<AddressChangedEvent> can be useful (where AddressChangedEvent implements the interface.)Charlie
@Nicholas: I agree. For events this might indeed be useful.Piled
C
17

Not really a fair answer, as I've extended Autofac since you posted the question... :)

As per Daniel's answer, you'll need to add the in modifier to the TCommand parameter of ICommandHandler:

interface ICommandHandler<in TCommand>
{
    void Handle(TCommand command);
}

Autofac 2.5.2 now includes an IRegistrationSource to enable contravariant Resolve() operations:

using Autofac.Features.Variance;

var builder = new ContainerBuilder();
builder.RegisterSource(new ContravariantRegistrationSource());

With this source registered, services represented by a generic interface with a single in parameter will be looked up taking variant implementations into account:

builder.RegisterType<FooCommandHandler>()
   .As<ICommandHandler<FooCommand>>();

var container = builder.Build();
container.Resolve<ICommandHandler<FooCommand>>();
container.Resolve<ICommandHandler<SpecialFooCommand>>();

Both calls to Resolve() will successfully retrieve the FooCommandHandler.

If you can't upgrade to the latest Autofac package, grab the ContravariantRegistrationSource from http://code.google.com/p/autofac/source/browse/src/Source/Autofac/Features/Variance/ContravariantRegistrationSource.cs - it should compile against any recent Autofac build.

Charlie answered 12/8, 2011 at 10:16 Comment(2)
+1. Thanks once again for the great work on Autofac. I'll update to the new version. I'll also check out the implementation of ContravariantRegistrationSource as soon as I get around to it!Cecilycecity
Wow, I hadn't seen the contravariant features. That's amazing! Definitely going to be using these.Aviator
S
5

What you are asking is not possible without own coding. Basically, you are asking the following: If the type I tried to resolve isn't found, return another type that can be converted to it, e.g. if you try to resolve IEnumerable return a type that is registered for ICollection. This is not supported. One simple solution would be the following: Register FooCommandHandler as a handler for ICommandHandler<SpecialFooCommand>. For this to work, ICommandHandler needs to be contravariant:

interface ICommand { }

class FooCommand : ICommand { }

class SpecialFooCommand : FooCommand { }

interface ICommandHandler<in T> where T : ICommand
{
    void Handle(T command);
}

class FooCommandHandler : ICommandHandler<FooCommand>
{
    public void Handle(FooCommand command)
    {
        // ...
    }
}

var builder = new ContainerBuilder();
builder.RegisterType<FooCommandHandler>()
       .As<ICommandHandler<SpecialFooCommand>>()
       .As<ICommandHandler<FooCommand>>();
var container = builder.Build();
var fooCommand = new FooCommand();
var specialCommand = new SpecialFooCommand();
container.Resolve<ICommandHandler<FooCommand>>().Handle(fooCommand);
container.Resolve<ICommandHandler<FooCommand>>().Handle(specialCommand);
container.Resolve<ICommandHandler<SpecialFooCommand>>().Handle(specialCommand);

BTW: The way you are using the container, you apply the Service locator anti-pattern. This should be avoided.

Sellma answered 10/8, 2011 at 13:7 Comment(2)
Nice answer. FWIW, I haven't found any way to avoid Service Locator when dispatching to generic handlers like this. I'd love to hear about any alternatives you know of. Cheers!Charlie
@Daniel, thanks for your answer. This would actually work well in my case, and it's so simple that I completely overlooked it. +1 The only thing this requires is knowledge of the command handler class.Cecilycecity
P
3

I like to add an alternative approach, which also works without C# 4.0 variance support.

You can create a special decorator / wrapper that allows executing a command as its base type:

public class VarianceHandler<TSubCommand, TBaseCommand> 
    : ICommandHandler<TSubCommand>
    where TSubCommand : TBaseCommand
{
    private readonly ICommandHandler<TBaseCommand> handler;

    public VarianceHandler(ICommandHandler<TBaseCommand> handler)
    {
        this.handler = handler;
    }

    public void Handle(TSubCommand command)
    {
        this.handler.Handle(command);
    }
}

With this in place, the following line of code would allow you to handle SpecialFooCommand as its base type:

builder.Register<FooCommandHandler>()
    .As<ICommandHandler<FooCommand>>();

builder.Register<VarianceHandler<SpecialFooCommand, FooCommand>>()
    .As<ICommandHandler<SpecialFooCommand>>();

Note that the use of such VarianceHandler works for most DI containers.

Piled answered 14/8, 2011 at 12:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.