понедельник, 23 июля 2012 г.

UnityContainer. Передача параметра конструктора в метод Resolve()

[UnityContainer. Pass constructor parameters to Resolve() method]

В одном своем проекте использую UnityContainer - расширяемый DI-контейнер (dependency injection container) от Misrosoft. И понадобилось мне передавать в конструкторы создаваемых контейнером сервисов объекты (DataContext), временем жизни которых я управлял сам. То есть мне понадобилось примерно следующее:

// работаем с одним контекстом данных
DataContext dx1 = container.Resolve();
container.Resolve(/*dx1*/); // конструктор сервиса: public Service1(DataContext dx)
container.Resolve(/*dx1*/); // конструктор сервиса: public Service2(DataContext dx)

// работаем с другим контекстом
DataContext dx2 = container.Resolve();
container.Resolve(/*dx2*/);

Оказалось, что для этой довольно типичной задачи UnityContainer не предлагает однозначного решения. Это обстоятельство порождает множество подходов, которые можно найти в интернете. Одним из таких решений является разрешение (resolve) объекта с переопределением его параметра:

DataContext dx1 = container.Resolve();
Service1 srv1 = container.Resolve(new ParameterOverride("dx", dx1));

Но здесь нужно быть крайне осторожным, т.к. при изменении имени параметра, контейнер без какого-либо предупреждения инициализирует сервис им вновь созданным объектом DataContext, а наш объект будет попросту проигнорирован.

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

Другой, приемлемый, на мой взгляд, подход - вынос инициализации сервисов контекстом в отдельный метод:

DataContext dx1 = container.Resolve();
Service1 srv1 = container.Resolve();
Service2 srv2 = container.Resolve();
srv1.Init(dx1);
srv2.Init(dx1);

Я же задействовал потомка контейнера (ChildContainer), что, как мне показалось, является наиболее простым и естественным способом для решения поставленной задачи. Вот что у меня получилось:

DataContext dx1 = container.Resolve();
IUnityContainer childContainer = container.CreateChildContainer();
childContainer.RegisterInstance(dx1, new ExternallyControlledLifetimeManager()); // !!!
Service1 srv1 = childContainer.Resolve();
Service2 srv2 = childContainer.Resolve();
childContainer.Dispose();

Идея проста: В потомке контейнера регистрируем необходимую инстанцию DataContext, создаем с его помощью объект сервиса (Resolve) и в конце заметаем за собой следы (Dispose). Обратите внимание на указание внешнего контроллера времени жизни объекта dx1 (ExternallyControlledLifetimeManager) - тем самым мы исключаем DataContext из утилизации при вызове метода childContainer.Dispose().

Этот код лег в основу метода-расширения, которое я использовал в своем проекте:

public static class UnityExtensions
{
  public static T xResolve(this IUnityContainer container, DataContext dx) {
    using (IUnityContainer childContainer = container.CreateChildContainer())
    {
      childContainer.RegisterInstance(dx, new ExternallyControlledLifetimeManager());
      return childContainer.Resolve();
    }
  }
}

Теперь вызовы выглядят так:

DataContext dx1 = container.Resolve(); // Первая инстанция
Service1 srv1 = container.xResolve(dx1);
Service2 srv2 = container.xResolve(dx1);

DataContext dx3 = container.Resolve(); // Вторая инстанция
Service1 srv3 = container.xResolve(dx3);


PS: К концу написания статьи натолкнулся на еще один способ реализации необходимого функционала - использование переопределения зависимости DependencyOverride:

container.Resolve(new DependencyOverride(dx1));

Здесь объект DataContext встраивается в граф объектов контейнера и используется везде, где есть зависимость этого типа.

Комментариев нет: