Настройка разрешения компонента Autofac/проблема с общей ко-контравариантностью
Во-первых, извините за неопределенное название вопроса. Я не мог придумать более точный.
Учитывая эти типы:
{ TCommand : ICommand }
«interface» «interface» /
+-----------+ +----------------------/----+
| ICommand | | ICommandHandler<TCommand> |
+-----------+ +---------------------------+
^ | Handle(command: TCommand) |
| +---------------------------+
| ^
| |
+------------+ +-------------------+
| FooCommand | | FooCommandHandler |
+------------+ +-------------------+
^
|
+-------------------+
| SpecialFooCommand |
+-------------------+
Я хотел бы написать метод Dispatch
, который принимает любую команду и отправляет ее соответствующему ICommandHandler<>
. Я думал, что использование контейнера DI (Autofac) может значительно упростить сопоставление от типа команды к обработчику команд:
void Dispatch<TCommand>(TCommand command) where TCommand : ICommand
{
var handler = autofacContainer.Resolve<ICommandHandler<TCommand>>();
handler.Handle(command);
}
Скажем, контейнер DI знает обо всех типах, показанных выше. Теперь я звоню:
Dispatch(new SpecialFooCommand(…));
В действительности это приведет к тому, что Autofac выбрасывает ComponentNotRegisteredException
, поскольку нет ICommandHandler<SpecialFooCommand>
.
В идеале, однако, мне бы хотелось, чтобы SpecialFooCommand
обрабатывался доступным обработчиком ближайшего соответствия, т.е. на FooCommandHandler
в приведенном выше примере.
Можно ли настроить Autofac с этой целью, возможно, с помощью настраиваемого источника регистрации?
PS: Я понимаю, что может возникнуть фундаментальная проблема совпадения/контравариантности (как в следующем примере), и единственным решением может быть то, что не использовать общие файлы вообще... но я хотел бы придерживаться общих типов, если это возможно.
ICommandHandler<FooCommand> fooHandler = new FooCommandHandler(…);
ICommandHandler<ICommand> handler = fooHandler;
// ^
// doesn't work, types are incompatible
Ответы
Ответ 1
Не совсем честный ответ, поскольку я расширил Autofac, так как вы разместили вопрос...:)
В соответствии с ответом Дэниела вам нужно добавить модификатор in
к параметру TCommand
ICommandHandler
:
interface ICommandHandler<in TCommand>
{
void Handle(TCommand command);
}
Autofac 2.5.2 теперь включает IRegistrationSource
для разрешения контравариантных операций Resolve()
:
using Autofac.Features.Variance;
var builder = new ContainerBuilder();
builder.RegisterSource(new ContravariantRegistrationSource());
При регистрации этого источника службы, представленные общим интерфейсом с одним параметром in
, будут рассмотрены с учетом вариантов реализации:
builder.RegisterType<FooCommandHandler>()
.As<ICommandHandler<FooCommand>>();
var container = builder.Build();
container.Resolve<ICommandHandler<FooCommand>>();
container.Resolve<ICommandHandler<SpecialFooCommand>>();
Оба вызова Resolve()
будут успешно извлекать FooCommandHandler
.
Если вы не можете перейти на последний пакет Autofac, возьмите ContravariantRegistrationSource
из http://code.google.com/p/autofac/source/browse/src/Source/Autofac/Features/Variance/ContravariantRegistrationSource.cs - он должен скомпилироваться против любых последних Autofac build.
Ответ 2
То, что вы просите, невозможно без собственного кодирования.
В основном, вы спрашиваете следующее: Если тип, который я пытался разрешить, не найден, верните другой тип, который можно преобразовать в него, например. если вы попытаетесь разрешить IEnumerable
вернуть тип, зарегистрированный для ICollection
.
Это не поддерживается.
Одним из простых решений было бы следующее:
Зарегистрируйте FooCommandHandler
как обработчик для ICommandHandler<SpecialFooCommand>
. Чтобы это сработало, ICommandHandler должен быть контравариантным:
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: Как вы используете контейнер, вы применяете анти-шаблон шаблона сервиса. Этого следует избегать.
Ответ 3
Мне нравится добавлять альтернативный подход, который также работает без поддержки дисперсии С# 4.0.
Вы можете создать специальный декоратор/обертку, который позволяет выполнять команду в качестве базового типа:
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);
}
}
При этом следующая строка кода позволит вам обрабатывать SpecialFooCommand
в качестве базового типа:
builder.Register<FooCommandHandler>()
.As<ICommandHandler<FooCommand>>();
builder.Register<VarianceHandler<SpecialFooCommand, FooCommand>>()
.As<ICommandHandler<SpecialFooCommand>>();
Обратите внимание, что использование такого VarianceHandler
работает для большинства контейнеров DI.