В этом посте я покажу пример того, как можно расширить стандартные возможности IoC-контейнера Unity. Покажу как создается объект в Unity «изнутри». Расскажу про Unity Extensions, Strategies & Policies.
Допустим в нашем приложении есть компонент Persistence, который отвечает за сохранении объектов. Он описывается интерфейсом IPersistence и имеет реализации — FilePersistence, DbPersistence, WsPersistence, InMemoryPersistence.
В классическом варианте мы в начале приложения регистрируем нужную реализацию в Unity и далее, вызывая Resolve для IPersistence, всегда получаем ее.
Но что делать, если необходимая реализация может меняться в процессе работы приложения. Например она задается в конфиг-файле, или при недоступности сети надо автоматически использовать FilePersistence?
В Unity есть возможность регистрировать зависимости по имени. Пример:
Осталось добиться, чтобы при получении реализации без имени, Unity каким-то образом определял какую нам нужно реализацию.
Пусть у нас будет делегат, который мы передадим Unity, определявший нужное имя реализации.
Пример:
Стандартного способа в Unity для этого нет. Но мы решим проблему написанием своего расширения.
Расширение Unity — это класс, унаследованный от UnityContainerExtension. У него есть контекст расширения (ExtensionContext) и виртуальные методы Initialize() и Remove() (соответственно вызываются при инициализации и удалении расширения).
Добавляются расширения через методы контейнера AddNewExtension и AddExtension, удаляются через RemoveAllExtensions.
Чтобы расширение можно было конфигурировать, оно должно реализовывать интерфейс-конфигуратор, унаследованный от IUnityContainerExtensionConfigurator. Конфигурирование происходит через метод контейнера Configure.
Каждый зарегистрированный тип в Unity имеет свой build-ключ (buildKey). Он состоит из зарегистрированного типа и имени, под которым он был зарегистрирован.
Процесс Resolve в Unity реализован при помощи стратегий.
Стратегия — это класс реализующий интерфейс IBuilderStrategy. Он имеет четыре метода: PreBuildUp, PostBuildUp, PreTearDown, PostTearDown.
При вызове Resolve:
В Unity есть 4 предопределенных стратегии, которые вызываются для каждого Resolve:
Мы напишем свою стратеги, которая будет подменять в build-ключе пустое имя на имя нужной реализации.
Т.к. поиск реализации по build-ключу происходит в стратегии BuildKeyMappingStrategy, то мы должны зарегистрировать свою стратегию так, чтобы она выполнилась раньше BuildKeyMappingStrategy. Стратегии сортируются в зависимости от этапа, который был указан при регистрации.
Всего есть 7 этапов — Setup, TypeMapping, Lifetime, PreCreation, Creation, Initialization, PostInitialization. BuildKeyMappingStrategy регистрируется на этап TypeMapping, значит нашу стратегию зарегистрируем на Setup. Регистрация будет происходить в методе Initialize нашего расширения.
Еще одним важным механизмом в Unity являются политики.
Политика — это интерфейс, наследуемый от IBuilderPolicy и класс его реализующий.
В интерфейсе политики можно определять методы для любых действий. Сам IBuilderPolicy пустой.
Стратегия может получить из BuilderContext политику для заданного типа по build-ключу.
Создадим свою политику для получения нового имя по build-ключу.
Используем ее в нашей стратегии.
Добавлять стратегии можно в расширении через context. Политика добавляется для конкретного ключа, или как политика по-умолчанию.
Реализуем политику получения имя через делегат:
Для интерфейса политики IResolveNamePolicy может быть несколько реализация, например через делегат, через интерфейс, через обращение к конфигу.
Добавлять политику для конкретного build-ключа будем при конфигурировании нашего расширения.
Наше расширение готово.
Теперь мы можем делать так:
Можно создать class helper для IUnityContainer чтобы можно было писать «SetNameResolver», как в начале и хотели.
Теперь при вызове Resolve:
Исходный код — тут
Допустим в нашем приложении есть компонент Persistence, который отвечает за сохранении объектов. Он описывается интерфейсом IPersistence и имеет реализации — FilePersistence, DbPersistence, WsPersistence, InMemoryPersistence.
В классическом варианте мы в начале приложения регистрируем нужную реализацию в Unity и далее, вызывая Resolve для IPersistence, всегда получаем ее.
IUnityContainer uc = new UnityContainer();
uc.RegisterType<IPersistence, FilePersistence>();
IPersistence p = uc.Resolve<IPersistence>();
p.Add(obj);
* This source code was highlighted with Source Code Highlighter.
Но что делать, если необходимая реализация может меняться в процессе работы приложения. Например она задается в конфиг-файле, или при недоступности сети надо автоматически использовать FilePersistence?
В Unity есть возможность регистрировать зависимости по имени. Пример:
uc.RegisterType<IPersistence, InMemoryPersistence>("none");
uc.RegisterType<IPersistence, FilePersistence>("file");
uc.RegisterType<IPersistence, DbPersistence>("db");
uc.RegisterType<IPersistence, WsPersistence>("ws");
IPersistence p = uc.Resolve<IPersistence>("file"); // Получили file реализацию.
IPersistence p = uc.Resolve<IPersistence>("db"); // Получили dbреализацию.
* This source code was highlighted with Source Code Highlighter.
Осталось добиться, чтобы при получении реализации без имени, Unity каким-то образом определял какую нам нужно реализацию.
Пусть у нас будет делегат, который мы передадим Unity, определявший нужное имя реализации.
Пример:
uc.SetNameResolver<IPersistence>(GetPersistenceImplementationName);
IPersistence p = uc.Resolve<IPersistence>(); // Тут мы получим ту реализацию, имя который вернул GetPersistenceImplementationName
* This source code was highlighted with Source Code Highlighter.
Стандартного способа в Unity для этого нет. Но мы решим проблему написанием своего расширения.
Unity Extensions
Расширение Unity — это класс, унаследованный от UnityContainerExtension. У него есть контекст расширения (ExtensionContext) и виртуальные методы Initialize() и Remove() (соответственно вызываются при инициализации и удалении расширения).
Добавляются расширения через методы контейнера AddNewExtension и AddExtension, удаляются через RemoveAllExtensions.
public class NameResolverExtension : UnityContainerExtension
{
protected override void Initialize()
{
}
protected override void Remove()
{
}
public NameResolverExtension()
: base()
{
}
}
uc.AddNewExtension<NameResolverExtension>();
* This source code was highlighted with Source Code Highlighter.
Чтобы расширение можно было конфигурировать, оно должно реализовывать интерфейс-конфигуратор, унаследованный от IUnityContainerExtensionConfigurator. Конфигурирование происходит через метод контейнера Configure.
// Наш делегат для получения имени
public delegate string NameResolverDelegate(Type typeToBuild);
// Интерфейс-конфигуратор
public interface INameResolverExtensionConfigurator : IUnityContainerExtensionConfigurator
{
INameResolverExtensionConfigurator RegisterDelegatedName<TTypeToBuild>(
NameResolverDelegate resolver);
}
static private string GetPersistenceImplementationName(Type typeToBuild)
{
// На самом деле тут мы должны читать конфиг...
return "db";
}
uc.Configure<INameResolverExtensionConfigurator>()
.RegisterDelegatedName<IPersistence>(GetPersistenceImplementationName);
* This source code was highlighted with Source Code Highlighter.
Strategy
Каждый зарегистрированный тип в Unity имеет свой build-ключ (buildKey). Он состоит из зарегистрированного типа и имени, под которым он был зарегистрирован.
Процесс Resolve в Unity реализован при помощи стратегий.
Стратегия — это класс реализующий интерфейс IBuilderStrategy. Он имеет четыре метода: PreBuildUp, PostBuildUp, PreTearDown, PostTearDown.
При вызове Resolve:
- Создается список зарегистрированных стратегий;
- Формируется build-ключ искомого типа и контекст построения (BuilderContext);
- Контекст последовательно обрабатывается стратегиями до тех пор, пока одна из них не установит флаг BuildComplete в true.
В Unity есть 4 предопределенных стратегии, которые вызываются для каждого Resolve:
- BuildKeyMappingStrategy. Заменяет build-ключ в контексте с искомого типа на ключ реализации. По сути весь resolving тут и происходит;
- LifetimeStrategy. Проверяет наличие реализации в Lifetime-менеджере;
- ArrayResolutionStrategy. Разрешение зависимостей-массивов;
- BuildPlanStrategy. Создании экземпляра реализации (если он еще не создан) и автоматическое разрешение его зависимостей.
Мы напишем свою стратеги, которая будет подменять в build-ключе пустое имя на имя нужной реализации.
internal class ResolveNameBuilderStrategy : BuilderStrategy
{
private NamedTypeBuildKey ReplaceBuildKeyName(IBuilderContext context, NamedTypeBuildKey buildKey)
{
}
public override void PreBuildUp(IBuilderContext context)
{
if (context.BuildKey is NamedTypeBuildKey)
context.BuildKey = ReplaceBuildKeyName(context, (NamedTypeBuildKey)(context.BuildKey));
}
public ResolveNameBuilderStrategy()
: base()
{
}
}
* This source code was highlighted with Source Code Highlighter.
Т.к. поиск реализации по build-ключу происходит в стратегии BuildKeyMappingStrategy, то мы должны зарегистрировать свою стратегию так, чтобы она выполнилась раньше BuildKeyMappingStrategy. Стратегии сортируются в зависимости от этапа, который был указан при регистрации.
Всего есть 7 этапов — Setup, TypeMapping, Lifetime, PreCreation, Creation, Initialization, PostInitialization. BuildKeyMappingStrategy регистрируется на этап TypeMapping, значит нашу стратегию зарегистрируем на Setup. Регистрация будет происходить в методе Initialize нашего расширения.
public class NameResolverExtension : UnityContainerExtension, INameResolverExtensionConfigurator
{
protected override void Initialize()
{
Context.Strategies.AddNew<ResolveNameBuilderStrategy>(UnityBuildStage.Setup);
}
}
* This source code was highlighted with Source Code Highlighter.
Policies
Еще одним важным механизмом в Unity являются политики.
Политика — это интерфейс, наследуемый от IBuilderPolicy и класс его реализующий.
В интерфейсе политики можно определять методы для любых действий. Сам IBuilderPolicy пустой.
Стратегия может получить из BuilderContext политику для заданного типа по build-ключу.
Создадим свою политику для получения нового имя по build-ключу.
internal interface IResolveNamePolicy : IBuilderPolicy
{
string ResolveName(NamedTypeBuildKey buildKey);
}
* This source code was highlighted with Source Code Highlighter.
Используем ее в нашей стратегии.
internal class ResolveNameBuilderStrategy : BuilderStrategy
{
private NamedTypeBuildKey ReplaceBuildKeyName(
IBuilderContext context, NamedTypeBuildKey buildKey)
{
IResolveNamePolicy policy = context.Policies.Get<IResolveNamePolicy>(buildKey);
if (policy != null)
return new NamedTypeBuildKey(buildKey.Type, policy.ResolveName(buildKey));
return buildKey;
}
}
* This source code was highlighted with Source Code Highlighter.
Добавлять стратегии можно в расширении через context. Политика добавляется для конкретного ключа, или как политика по-умолчанию.
Реализуем политику получения имя через делегат:
internal class ResolveNamePolicyDelegated : IResolveNamePolicy
{
protected readonly NameResolverDelegate Resolver;
public ResolveNamePolicyDelegated(NameResolverDelegate resolver)
: base()
{
Resolver = resolver;
}
public string ResolveName(NamedTypeBuildKey buildKey)
{
return Resolver(buildKey.Type);
}
}
* This source code was highlighted with Source Code Highlighter.
Для интерфейса политики IResolveNamePolicy может быть несколько реализация, например через делегат, через интерфейс, через обращение к конфигу.
Добавлять политику для конкретного build-ключа будем при конфигурировании нашего расширения.
public class NameResolverExtension : UnityContainerExtension, INameResolverExtensionConfigurator
{
public INameResolverExtensionConfigurator RegisterDelegatedName<TTypeToBuild>(NameResolverDelegate resolver)
{
Context.Policies.Set<IResolveNamePolicy>(
new ResolveNamePolicyDelegated(resolver),
NamedTypeBuildKey.Make<TTypeToBuild>());
return this;
}
}
* This source code was highlighted with Source Code Highlighter.
Результат
Наше расширение готово.
Теперь мы можем делать так:
IUnityContainer uc = new UnityContainer();
uc.RegisterType<IPersistence, InMemoryPersistence>("none");
uc.RegisterType<IPersistence, FilePersistence>("file");
uc.RegisterType<IPersistence, DbPersistence>("db");
uc.RegisterType<IPersistence, WsPersistence>("ws");
uc.AddNewExtension<NameResolverExtension>();
uc.Configure<INameResolverExtensionConfigurator>().RegisterDelegatedName<IPersistence>(GetPersistenceImplementationName);
IPersistence p = uc.Resolve<IPersistence>();
p.Add(new Object()); // Будет использоваться та реализация, имя которой вернет GetPersistenceImplementationName
* This source code was highlighted with Source Code Highlighter.
Можно создать class helper для IUnityContainer чтобы можно было писать «SetNameResolver», как в начале и хотели.
Теперь при вызове Resolve:
- Первой запускается наша стратегия;
- Она получает политику для искомого build-ключа;
- Если для этого build-ключа существует политика, то build-ключ заменяется в контексте на ключ с именем из политики;
- Дальше Resolve работает так же как и раньше, но создает объект уже не для безымянного ключа, а для ключа с новым именем.
Исходный код — тут