c# - Personalización de la resolución de componentes de Autofac/Problema con co-/contravarianza genérica
customization resolution (3)
Primero, perdón por el vago título de la pregunta. No podría llegar a una más precisa.
Teniendo en cuenta estos tipos:
{ TCommand : ICommand }
«interface» «interface» /
+-----------+ +----------------------/----+
| ICommand | | ICommandHandler<TCommand> |
+-----------+ +---------------------------+
^ | Handle(command: TCommand) |
| +---------------------------+
| ^
| |
+------------+ +-------------------+
| FooCommand | | FooCommandHandler |
+------------+ +-------------------+
^
|
+-------------------+
| SpecialFooCommand |
+-------------------+
Me gustaría escribir un método de Dispatch
que acepte cualquier comando y lo envíe a un ICommandHandler<>
adecuado. Pensé que usar un contenedor DI (Autofac) podría simplificar enormemente la asignación del tipo de comando a un controlador de comandos:
void Dispatch<TCommand>(TCommand command) where TCommand : ICommand
{
var handler = autofacContainer.Resolve<ICommandHandler<TCommand>>();
handler.Handle(command);
}
Digamos que el contenedor DI conoce todos los tipos mostrados arriba. Ahora estoy llamando:
Dispatch(new SpecialFooCommand(…));
En realidad, esto dará como resultado que Autofac lance una excepción ComponentNotRegisteredException
, ya que no hay ICommandHandler<SpecialFooCommand>
disponible.
Sin embargo, lo ideal sería que el controlador de comandos de coincidencia más cercana SpecialFooCommand
un SpecialFooCommand
, es decir. por un FooCommandHandler
en el ejemplo anterior.
¿Se puede personalizar Autofac para ese fin, tal vez con una fuente de registro personalizada?
PD: Entiendo que podría existir el problema fundamental de que la co- / contravarianza se interponga en el camino (como en el siguiente ejemplo), y que la única solución podría ser una que no use genéricos en absoluto ... pero lo haría desea atenerse a los tipos genéricos, si es posible.
ICommandHandler<FooCommand> fooHandler = new FooCommandHandler(…);
ICommandHandler<ICommand> handler = fooHandler;
// ^
// doesn''t work, types are incompatible
Lo que pides no es posible sin una codificación propia. Básicamente, está preguntando lo siguiente: Si no se encuentra el tipo que intenté resolver, devuelva otro tipo que pueda convertirse, por ejemplo, si intenta resolver IEnumerable
devuelva un tipo que esté registrado para ICollection
. Esto no es compatible. Una solución simple sería la siguiente: Registre FooCommandHandler
como un controlador para ICommandHandler<SpecialFooCommand>
. Para que esto funcione, ICommandHandler debe ser 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);
Por cierto: la forma en que está utilizando el contenedor, aplica el anti-patrón del localizador de servicios . Esto debe evitarse.
Me gusta agregar un enfoque alternativo, que también funciona sin el soporte de variación de C # 4.0.
Puede crear un decorador / envoltorio especial que permita ejecutar un comando como su tipo base:
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);
}
}
Con esto en su lugar, la siguiente línea de código le permitiría manejar SpecialFooCommand
como su tipo base:
builder.Register<FooCommandHandler>()
.As<ICommandHandler<FooCommand>>();
builder.Register<VarianceHandler<SpecialFooCommand, FooCommand>>()
.As<ICommandHandler<SpecialFooCommand>>();
Tenga en cuenta que el uso de dicho VarianceHandler
funciona para la mayoría de los contenedores DI.
No es realmente una respuesta justa, ya que extendí Autofac desde que publicaste la pregunta ... :)
Según la respuesta de Daniel, deberá agregar el modificador in
al parámetro TCommand
de ICommandHandler
:
interface ICommandHandler<in TCommand>
{
void Handle(TCommand command);
}
Autofac 2.5.2 ahora incluye un IRegistrationSource
para habilitar operaciones contravariantes Resolve()
:
using Autofac.Features.Variance;
var builder = new ContainerBuilder();
builder.RegisterSource(new ContravariantRegistrationSource());
Con esta fuente registrada, los servicios representados por una interfaz genérica con un único parámetro de entrada se buscarán teniendo en cuenta las implementaciones de variantes:
builder.RegisterType<FooCommandHandler>()
.As<ICommandHandler<FooCommand>>();
var container = builder.Build();
container.Resolve<ICommandHandler<FooCommand>>();
container.Resolve<ICommandHandler<SpecialFooCommand>>();
Ambas llamadas a Resolve()
recuperarán con éxito el FooCommandHandler
.
Si no puede actualizarse al último paquete de Autofac, obtenga el ContravariantRegistrationSource
de http://code.google.com/p/autofac/source/browse/src/Source/Autofac/Features/Variance/ContravariantRegistrationSource.cs : debería compilar contra cualquier compilación reciente de Autofac.