Разрешить автоматическую фабрику из именованной регистрации для универсального типа

  1. Предположим, простой интерфейс:

    public interface ICommandHandler<T>
    {
        void Handle(T command);
    }
    
  2. Предположим несколько реализаций для разных конкретных T, например:

    public class FooCommandHandler : ICommandHandler<FooCommand> { /*...*/ }
    
  3. Предположим, что универсальная фабричная реализация этого интерфейса:

    public class FactoryCommandHandler<T> : ICommandHandler<T>
    {
        public FactoryCommandHandler(Func<ICommandHandler<T>> factory) { /*...*/ }
        /*...*/
    }
    

Теперь я хочу зарегистрировать обработчик фабрики из 3 как экземпляр, который разрешается при разрешении ICommandHandler<T>.
Моя проблема в том, что я не могу правильно зарегистрировать другие реализации, поэтому фабрика для их можно разрешить.

Вот что я пробовал:

builder.RegisterAssemblyTypes(assembly)
       .Where(type => type.Name.EndsWith("CommandHandler"))
       .Named("concreteCommandHandler", typeof(ICommandHandler<>));


builder.RegisterGeneric(typeof(FactoryCommandHandler<>)
       .WithParameter(
             (p, c) => true,
             (p, c) => c.ResolveNamed("concreteCommandHandler", p.ParameterType))
       .As(typeof(ICommandHandler<>));

Однако это не удается, поскольку Func<ICommandHandler<SomeConcreteCommand>> не зарегистрирован. Autofac, похоже, не может автоматически создать фабрику в этом случае, которую он обычно поддерживает.

Как исправить регистрацию и достичь своей цели?


person Daniel Hilgarth    schedule 12.06.2013    source источник
comment
Какова реализация объекта FooCommand? У него есть родитель? Если это так, вы можете попробовать зарегистрироваться, изменив открытый класс FactoryCommandHandler‹T› : ICommandHandler‹T› где T : [parentclassname]   -  person Azhar Khorasany    schedule 12.06.2013
comment
@AzharKhorasany: у него нет базового класса. И я не вижу, как ограничение общего типа, которое вы предлагаете, что-то меняет.   -  person Daniel Hilgarth    schedule 12.06.2013
comment
Применение ограничения сообщает компилятору, что объекты T могут быть только конкретного типа, и он должен иметь возможность регистрироваться, где без ограничения компилятор не знает, что такое T.   -  person Azhar Khorasany    schedule 12.06.2013
comment
@AzharKhorasany: я знаю, что делает ограничение типа. Мой вопрос не имеет ничего общего с компилятором. Речь идет об API Autofac.   -  person Daniel Hilgarth    schedule 12.06.2013


Ответы (3)


К сожалению, вы не можете использовать RegisterAssemblyTypes в этом случае — он не полностью обрабатывает общие регистрации так, как вам нужно (вам необходимо явно зарегистрировать каждый конкретный обработчик команд для его реализованного интерфейса, включая универсальный тип этого интерфейса).

Вместо этого вы можете использовать следующее в качестве регистрации:

assembly.GetTypes()
    .Where(type => type.Name.EndsWith("CommandHandler"))
    .ToList()
    .ForEach(t => builder.RegisterType(t)
        .Named("concreteCommandHandler", typeof (ICommandHandler<>)
            .MakeGenericType(t.GetInterfaces()[0].GenericTypeArguments[0])
    ));

builder.RegisterGeneric(typeof(FactoryCommandHandler<>)
   .WithParameter(
         (p, c) => true,
         (p, c) => c.ResolveNamed("concreteCommandHandler", p.ParameterType))
   .As(typeof(ICommandHandler<>));

Это успешно позволит вам сделать что-то подобное, возвращая универсальную фабрику с именованной командой в качестве параметра конструктора:

container.Resolve<ICommandHandler<FooCommand>>().Handle(new FooCommand());
person Matt Davies    schedule 12.06.2013
comment
Большое спасибо. Я только что узнал, что проблема в RegisterAssemblyTypes, после написания моего собственного IRegistrationSource... Я еще не понял, как это сделать, большое спасибо за это! - person Daniel Hilgarth; 12.06.2013
comment
Проверено и принято. Спасибо еще раз! Я немного улучшил код, который вы описали, см. мой ответ. - person Daniel Hilgarth; 12.06.2013

Извините, что подключаю здесь Simple Injector, но я не мог не заметить, что вы боретесь с чем-то, что это детская игра в Simple Injector. В Simple Injector вы можете делать то, что хотите, в двух строках кода:

// using SimpleInjector;
// using SimpleInjector.Extensions;

var container = new Container();

container.RegisterManyForOpenGeneric(
    typeof(ICommandHandler<>), 
    assembly);

container.RegisterSingleDecorator(
    typeof(ICommandHandler<>), 
    typeof(FactoryCommandHandler<>));

Эти две простые строки обеспечивают следующее:

  • В предоставленной сборке выполняется поиск конкретных реализаций ICommandHandler<T>.
  • Если конкретная реализация определяет интерфейс ICommandHandler<T> несколько раз, он регистрируется для каждой закрытой универсальной версии этого интерфейса.
  • FactoryCommandHandler<T> зарегистрирован для использования в ICommandHandler<T> реализациях. Для каждой закрытой универсальной версии ICommandHandler<T> возвращается один экземпляр этого универсального FactoryCommandHandler<T>.
  • В этот FactoryCommandHandler<T> вводится Func<ICommandHandler<T>>, что позволяет создать декоративный элемент (завернутый экземпляр). Это эффективно задерживает создание этого экземпляра.
  • Введенная фабрика сохранит образ жизни украшенного.

FactoryCommandHandler<T> зависит только от Func<T>, который является синглтоном. Таким образом, FactoryCommandHandler<T> может быть зарегистрирован как синглтон (что происходит при регистрации выше). Если это зависит от зависимостей других образов жизни, возможно, лучше зарегистрировать его как переходный.

person Steven    schedule 12.06.2013
comment
Спасибо за ваш комментарий. Autofac также поддерживает фабрики на основе автоматических делегатов без необходимости их явной регистрации. Однако метод RegisterSingleDecorator действительно хорош, потому что тот факт, что он отсутствует, является источником этого запутанного решения в Autofac. Когда я несколько лет назад решил использовать Autofac, мне показалось, что у него самый чистый и простой синтаксис регистрации. Но это действительно выглядит красиво. Думаю, я повнимательнее рассмотрю Simple Injector. - person Daniel Hilgarth; 13.06.2013
comment
Я согласен с тем, что у Autofac действительно хороший и удобный API. Однако Simple Injector разработан специально для типов архитектур, с которыми вы имеете дело. Вот почему регистрация в этом случае так проста. Для других сценариев, проектов или архитектур Autofac может быть лучшим выбором. - person Steven; 13.06.2013

Я использовал код в ответе Мэтта Дэвиса и немного улучшил его:

  • Теперь он правильно обрабатывает обработчики команд, которые реализуют другие интерфейсы.
  • Теперь он правильно обрабатывает обработчики команд, реализующие ICommandHandler<T> несколько раз.
  • Я улучшил свою первоначальную версию, зафиксировав первый параметр на WithParameter. Таким образом, теперь он поддерживает несколько параметров конструктора для FactoryCommandHandler<T>.

Результат выглядит следующим образом:

public static class AutofacExtensions
{
    public static void RegisterGenericTypesWithFactoryDecorator(
        this ContainerBuilder builder, 
        IEnumerable<Type> relevantTypes, 
        Type factoryDecorator,
        Type implementedInterfaceGenericTypeDefinition)
    {
        var serviceName = implementedInterfaceGenericTypeDefinition.ToString();

        foreach (var implementationType in relevantTypes)
        {
            var implementedInterfaces =
                implementationType.GetGenericInterfaces(
                    implementedInterfaceGenericTypeDefinition);
            foreach (var implementedInterface in implementedInterfaces)
                builder.RegisterType(implementationType)
                       .Named(serviceName, implementedInterface);
        }

        builder.RegisterGeneric(factoryDecorator)
               .WithParameter(
                   (p, c) => IsSpecificFactoryParameter(p, implementedInterfaceGenericTypeDefinition), 
                   (p, c) => c.ResolveNamed(serviceName, p.ParameterType))
               .As(implementedInterfaceGenericTypeDefinition)
               .SingleInstance();
    }

    private static bool IsSpecificFactoryParameter(ParameterInfo p,
                                                   Type expectedFactoryResult)
    {
        var parameterType = p.ParameterType;
        if (!parameterType.IsGenericType ||
            parameterType.GetGenericTypeDefinition() != typeof(Func<>))
            return false;

        var actualFactoryResult = p.ParameterType.GetGenericArguments()
                                                 .First();
        if (actualFactoryResult == expectedFactoryResult)
            return true;
        if (expectedFactoryResult.IsGenericTypeDefinition && 
            actualFactoryResult.IsGenericType)
            return expectedFactoryResult == 
                   actualFactoryResult.GetGenericTypeDefinition();
        return false;
    }
}

public static class TypeExtensions
{
    public static IEnumerable<Type> GetGenericInterfaces(
        this Type type, Type openGenericInterface)
    {
        return
            type.GetInterfaces()
                .Where(x => x.IsGenericType &&
                            x.GetGenericTypeDefinition() == openGenericInterface);
    }
}

Использование:

var relevantTypes = assembly.GetTypes();
builder.RegisterGenericTypesWithFactoryDecorator(
    relevantTypes.Where(type => type.Name.EndsWith("CommandHandler")), 
    typeof(FactoryCommandHandlerDecorator<>), 
    typeof(ICommandHandler<>));
person Daniel Hilgarth    schedule 12.06.2013