Autofac - разрешение параметров времени выполнения без необходимости передавать контейнер

У меня есть более простой класс "ServiceHelper", который принимает два параметра в конструкторе:

public ServiceHelper(ILogger<ServiceHelper> log, string serviceName)

(Универсальная оболочка ILogger для NLog, которую Autofac предоставляет просто отлично, а serviceName - это имя службы Windows, которую я должен предоставить во время выполнения.)

У меня возникают проблемы, когда я не могу понять, как создавать новые экземпляры этого класса во время выполнения, передавая разные имена служб, используя Autofac. Что-то вроде этого не работает, конечно, так как мне нужно указать разные имена служб во время выполнения:

builder.RegisterType<ServiceHelper>().As<IServiceHelper>().WithParameter(new NamedParameter("serviceName", null)).InstancePerDependency();

Из того, что я прочитал, это плохая привычка передавать контейнер и вызывать Resolve вручную правильно (сервисный локатор "анти-шаблон", о котором предупреждает AutoFac), или это так? Если бы я сделал это, то я мог бы сделать

container.Resolve<ServiceHelper>(new NamedParameter("serviceName", "some service name"));

Но даже для того, чтобы зайти так далеко, я не совсем уверен, как заставить Autofac внедрить контейнер в классы, ему просто нужно было бы зарегистрироваться, как именно, как это? И потом, мои классы требуют IContainer в своих конструкторах? (Это в C# Service с использованием инжектора конструктора)

builder.RegisterType<Container>().As<IContainer>().InstancePerDependency();

Я тоже читал о фабриках делегатов, но это, кажется, не избавляет от необходимости передавать контейнер.

На самом деле большинству моих классов, которые используют ServiceHelper, просто нужны 1 или 2 ServiceHelpers для конкретных имен сервисов, так что не то, чтобы я зарабатываю тысячи с неожиданными параметрами serviceName, это просто немного огорчает мою голову.

3 ответа

Решение

Да, пропускать контейнер повсюду - это анти-паттерн.

Вы можете избежать этого, используя такую ​​фабрику:

(примечание: весь код в этом ответе не проверен, я пишу это в текстовом редакторе на машине без Visual Studio)

public interface IServiceHelperFactory
{
    IServiceHelper CreateServiceHelper(string serviceName);
}

public class ServiceHelperFactory : IServiceHelperFactory
{
    private IContainer container;

    public ServiceHelperFactory(IContainer container)
    {
        this.container = container;
    }

    public IServiceHelper CreateServiceHelper(string serviceName)
    {
        return container.Resolve<ServiceHelper>(new NamedParameter("serviceName", serviceName));
    }
}

При запуске вы регистрируете ServiceHelperFactory в автофаке, как и все остальное:

builder.RegisterType<ServiceHelperFactory>().As<IServiceHelperFactory>();

Затем, когда вам нужно ServiceHelper в другом месте вы можете получить фабрику с помощью инжектора конструктора:

public class SomeClass : ISomeClass
{
    private IServiceHelperFactory factory;

    public SomeClass(IServiceHelperFactory factory)
    {
        this.factory = factory;
    }

    public void ThisMethodCreatesTheServiceHelper()
    {
        var helper = this.factory.CreateServiceHelper("some service name");
    }
}

Создавая саму фабрику с помощью инжектора конструктора с помощью Autofac, вы убедитесь, что фабрика знает о контейнере, без необходимости самим передавать контейнер.

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


РЕДАКТИРОВАТЬ:

Хорошо я забыл Как я уже говорил выше, я пишу это на компьютере без Visual Studio, поэтому я не могу протестировать мой пример кода.
Теперь, когда я прочитал ваш комментарий, я помню, что у меня была похожая проблема, когда я использовал Autofac и попытался зарегистрировать сам контейнер.

Моя проблема была в том, что мне нужно было зарегистрировать контейнер в сборщике.
Но чтобы зарегистрировать экземпляр контейнера, мне нужно было вызвать builder.Build()... который создает контейнер, что означает, что я не могу зарегистрировать вещи в компоновщике впоследствии.
Я не помню полученное сообщение об ошибке, но, похоже, у вас сейчас такая же проблема.

Решение, которое я нашел, состояло в том, чтобы создать второго компоновщика, зарегистрировать контейнер там, а затем использовать второй компоновщик для обновления одного-единственного контейнера.

Вот мой рабочий код из одного из моих проектов с открытым исходным кодом:

При запуске я регистрирую контейнер:

var builder = new ContainerBuilder();

// register stuff here

var container = builder.Build();

// register the container
var builder2 = new ContainerBuilder();
builder2.RegisterInstance<IContainer>(container);
builder2.Update(container);

... который затем используется WindowService создать новые окна WPF:

public class WindowService : IWindowService
{
    private readonly IContainer container;

    public WindowService(IContainer container)
    {
        this.container = container;
    }

    public T GetWindow<T>() where T : MetroWindow
    {
        return (T)this.container.Resolve<T>();
    }
}

Я пошел по пути вышеупомянутого подхода, и он отлично работает, однако я счел невозможным модульное тестирование из-за того, что метод "Resolve<>" в IContainer является методом расширения. Это также никогда не казалось "правильным" со всеми разговорами о том, чтобы не передавать свой контейнер.

Я вернулся к чертежной доске и нашел "правильный" способ создания объектов с использованием Autofac Delegate Factories http://docs.autofac.org/en/latest/advanced/delegate-factories.html

Разрешение должно происходить только для корневых композиционных объектов. Вызов решимости - это почти то же самое, что и "создание нового" объекта, что является тестом на запах. Есть моменты, когда разрешение является динамическим и может быть определено только на лету, но большинство зависимостей являются детерминированными и могут быть зарегистрированы заранее. Как это сделать с помощью Autofac - это сложная задача. Наградами ответ @Christian Specht является хорошим ответом, но он предполагает, что все определяется во время выполнения.

Чтобы определить цепочку зависимостей во время разработки, см. Тему SO Регистрация цепочки зависимостей Autofac...

Другие вопросы по тегам