Как использовать Шаблон Декоратора с Unity без явного указания каждого параметра в InjectionConstructor
Эта полезная статья от Дэвида Гайдна (РЕДАКТИРОВАТЬ: удалена мошенническая ссылка, возможно, это была статья) показывает, как вы можете использовать InjectionConstructor
класс, чтобы помочь вам создать цепочку, используя шаблон декоратора с Unity. Однако, если элементы в вашей цепочке декораторов имеют другие параметры в своем конструкторе, InjectionConstructor
должен явно объявить каждый из них (или Unity будет жаловаться, что не может найти правильный конструктор). Это означает, что вы не можете просто добавить новые параметры конструктора к элементам в цепочке декораторов, не обновив при этом код конфигурации Unity.
Вот пример кода, чтобы объяснить, что я имею в виду. ProductRepository
класс оборачивается первым CachingProductRepository
а затем LoggingProductRepostiory
, И CachingProductRepository, и LoggingProductRepository, в дополнение к принятию IProductRepository в своем конструкторе, также нуждаются в других интерфейсах из контейнера.
public class Product
{
public int Id;
public string Name;
}
public interface IDatabaseConnection { }
public interface ICacheProvider
{
object GetFromCache(string key);
void AddToCache(string key, object value);
}
public interface ILogger
{
void Log(string message, params object[] args);
}
public interface IProductRepository
{
Product GetById(int id);
}
class ProductRepository : IProductRepository
{
public ProductRepository(IDatabaseConnection db)
{
}
public Product GetById(int id)
{
return new Product() { Id = id, Name = "Foo " + id.ToString() };
}
}
class CachingProductRepository : IProductRepository
{
IProductRepository repository;
ICacheProvider cacheProvider;
public CachingProductRepository(IProductRepository repository, ICacheProvider cp)
{
this.repository = repository;
this.cacheProvider = cp;
}
public Product GetById(int id)
{
string key = "Product " + id.ToString();
Product p = (Product)cacheProvider.GetFromCache(key);
if (p == null)
{
p = repository.GetById(id);
cacheProvider.AddToCache(key, p);
}
return p;
}
}
class LoggingProductRepository : IProductRepository
{
private IProductRepository repository;
private ILogger logger;
public LoggingProductRepository(IProductRepository repository, ILogger logger)
{
this.repository = repository;
this.logger = logger;
}
public Product GetById(int id)
{
logger.Log("Requesting product {0}", id);
return repository.GetById(id);
}
}
Вот (проходящий) модульный тест. Посмотрите комментарии для битов излишней конфигурации, которые я хочу устранить:
[Test]
public void ResolveWithDecorators()
{
UnityContainer c = new UnityContainer();
c.RegisterInstance<IDatabaseConnection>(new Mock<IDatabaseConnection>().Object);
c.RegisterInstance<ILogger>(new Mock<ILogger>().Object);
c.RegisterInstance<ICacheProvider>(new Mock<ICacheProvider>().Object);
c.RegisterType<IProductRepository, ProductRepository>("ProductRepository");
// don't want to have to update this line every time the CachingProductRepository constructor gets another parameter
var dependOnProductRepository = new InjectionConstructor(new ResolvedParameter<IProductRepository>("ProductRepository"), new ResolvedParameter<ICacheProvider>());
c.RegisterType<IProductRepository, CachingProductRepository>("CachingProductRepository", dependOnProductRepository);
// don't want to have to update this line every time the LoggingProductRepository constructor changes
var dependOnCachingProductRepository = new InjectionConstructor(new ResolvedParameter<IProductRepository>("CachingProductRepository"), new ResolvedParameter<ILogger>());
c.RegisterType<IProductRepository, LoggingProductRepository>(dependOnCachingProductRepository);
Assert.IsInstanceOf<LoggingProductRepository>(c.Resolve<IProductRepository>());
}
6 ответов
Еще один подход, благодаря предложению @DarkSquirrel42, заключается в использовании InjectionFactory
, Недостатком является то, что код по-прежнему нуждается в обновлении каждый раз, когда в цепочку добавляется новый параметр конструктора. Преимущества кода гораздо проще для понимания, и только одна регистрация в контейнере.
Func<IUnityContainer,object> createChain = container =>
new LoggingProductRepository(
new CachingProductRepository(
container.Resolve<ProductRepository>(),
container.Resolve<ICacheProvider>()),
container.Resolve<ILogger>());
c.RegisterType<IProductRepository>(new InjectionFactory(createChain));
Assert.IsInstanceOf<LoggingProductRepository>(c.Resolve<IProductRepository>());
Смотрите эту статью о реализации расширения контейнера декоратора. Это должно привести вас туда, где вы хотите быть в отношении того, что вам не нужно изменять вашу конфигурацию, если ваши сигнатуры конструктора изменяются.
Другое решение заключается в добавлении параметров типа в вашу кодовую базу, чтобы помочь Unity разрешать ваши декорированные типы. К счастью, Unity прекрасно способна разрешать параметры типа и их зависимости самостоятельно, поэтому нам не нужно заботиться о параметрах конструктора при определении цепочки декораторов.
Регистрация будет выглядеть следующим образом:
unityContainer.RegisterType<IService, Logged<Profiled<Service>>>();
Вот базовый пример реализации. Обратите внимание на шаблонные декораторы Logged<TService>
а также Profiled<TService>
, Посмотрите ниже некоторые недостатки, которые я заметил до сих пор.
public interface IService { void Do(); }
public class Service : IService { public void Do() { } }
public class Logged<TService> : IService where TService : IService
{
private TService decoratee;
private ILogger logger;
public Logged(ILogger logger, TService decoratee) {
this.decoratee = decoratee;
this.logger = logger;
}
public void Do() {
logger.Debug("Do()");
decoratee.Do();
}
}
public class Profiled<TService> : IService where TService : IService
{
private TService decoratee;
private IProfiler profiler;
public Profiled(IProfiler profiler, TService decoratee) {
this.decoratee = decoratee;
this.profiler = profiler;
}
public void Do() {
profiler.Start();
decoratee.Do();
profiler.Stop();
}
}
Недостатки
- Неправильная регистрация, как
uC.RegisterType<IService, Logged<IService>>();
приведет к бесконечной рекурсии, которая переполняет ваше приложение. Это может быть уязвимость в архитектуре плагина. - Это в какой-то степени уродствует вашу кодовую базу. Если вы когда-нибудь откажетесь от Unity и переключитесь на другую инфраструктуру DI, эти параметры шаблона больше никому не будут нужны.
Я выбрал довольно грубый метод расширения для этого, который вел себя как ожидалось, когда я запустил его:
public static class UnityExtensions
{
public static IUnityContainer Decorate<TInterface, TDecorator>(this IUnityContainer container, params InjectionMember[] injectionMembers)
where TDecorator : class, TInterface
{
return Decorate<TInterface, TDecorator>(container, null, injectionMembers);
}
public static IUnityContainer Decorate<TInterface, TDecorator>(this IUnityContainer container, LifetimeManager lifetimeManager, params InjectionMember[] injectionMembers)
where TDecorator : class, TInterface
{
string uniqueId = Guid.NewGuid().ToString();
var existingRegistration = container.Registrations.LastOrDefault(r => r.RegisteredType == typeof(TInterface));
if(existingRegistration == null)
{
throw new ArgumentException("No existing registration found for the type " + typeof(TInterface));
}
var existing = existingRegistration.MappedToType;
//1. Create a wrapper. This is the actual resolution that will be used
if (lifetimeManager != null)
{
container.RegisterType<TInterface, TDecorator>(uniqueId, lifetimeManager, injectionMembers);
}
else
{
container.RegisterType<TInterface, TDecorator>(uniqueId, injectionMembers);
}
//2. Unity comes here to resolve TInterface
container.RegisterType<TInterface, TDecorator>(new InjectionFactory((c, t, sName) =>
{
//3. We get the decorated class instance TBase
var baseObj = container.Resolve(existing);
//4. We reference the wrapper TDecorator injecting TBase as TInterface to prevent stack overflow
return c.Resolve<TDecorator>(uniqueId, new DependencyOverride<TInterface>(baseObj));
}));
return container;
}
}
И в вашей настройке:
container.RegisterType<IProductRepository, ProductRepository>();
// Wrap ProductRepository with CachingProductRepository,
// injecting ProductRepository into CachingProductRepository for
// IProductRepository
container.Decorate<IProductRepository, CachingProductRepository>();
// Wrap CachingProductRepository with LoggingProductRepository,
// injecting CachingProductRepository into LoggingProductRepository for
// IProductRepository
container.Decorate<IProductRepository, LoggingProductRepository>();
Самый краткий ответ, который отлично работает, упоминается в другом посте stackru Марка Симана. Это сжато и не требует от меня использования именованных регистраций или предложения использовать расширения Unity. Рассмотрим интерфейс под названием ILogger с двумя реализациями, а именно Log4NetLogger и реализацией декоратора под названием DecoratorLogger. Вы можете зарегистрировать DecoratorLogger для интерфейса ILogger следующим образом:
container.RegisterType<ILogger, DecoratorLogger>(
new InjectionConstructor(
new ResolvedParameter<Log4NetLogger>()));
Пока я ждал ответов на этот вопрос, я нашел довольно хакерский обходной путь. Я создал метод расширения на IUnityContainer
это позволяет мне зарегистрировать цепочку декораторов, используя отражение для создания параметров InjectionConstructor:
static class DecoratorUnityExtensions
{
public static void RegisterDecoratorChain<T>(this IUnityContainer container, Type[] decoratorChain)
{
Type parent = null;
string parentName = null;
foreach (Type t in decoratorChain)
{
string namedInstance = Guid.NewGuid().ToString();
if (parent == null)
{
// top level, just do an ordinary register type
container.RegisterType(typeof(T), t, namedInstance);
}
else
{
// could be cleverer here. Just take first constructor
var constructor = t.GetConstructors()[0];
var resolvedParameters = new List<ResolvedParameter>();
foreach (var constructorParam in constructor.GetParameters())
{
if (constructorParam.ParameterType == typeof(T))
{
resolvedParameters.Add(new ResolvedParameter<T>(parentName));
}
else
{
resolvedParameters.Add(new ResolvedParameter(constructorParam.ParameterType));
}
}
if (t == decoratorChain.Last())
{
// not a named instance
container.RegisterType(typeof(T), t, new InjectionConstructor(resolvedParameters.ToArray()));
}
else
{
container.RegisterType(typeof(T), t, namedInstance, new InjectionConstructor(resolvedParameters.ToArray()));
}
}
parent = t;
parentName = namedInstance;
}
}
}
Это позволяет мне настроить мой контейнер с гораздо более читаемым синтаксисом:
[Test]
public void ResolveWithDecorators2()
{
UnityContainer c = new UnityContainer();
c.RegisterInstance<IDatabaseConnection>(new Mock<IDatabaseConnection>().Object);
c.RegisterInstance<ILogger>(new Mock<ILogger>().Object);
c.RegisterInstance<ICacheProvider>(new Mock<ICacheProvider>().Object);
c.RegisterDecoratorChain<IProductRepository>(new Type[] { typeof(ProductRepository), typeof(CachingProductRepository), typeof(LoggingProductRepository) });
Assert.IsInstanceOf<LoggingProductRepository>(c.Resolve<IProductRepository>());
}
Мне все еще было бы интересно узнать, есть ли более элегантное решение для этого с Unity
Я знаю, что этот пост немного устарел, но на самом деле нет полностью функциональной реализации декоратора Unity для последних выпусков (есть много критических изменений, см. Вики Unity).
Я взял ответ@garryp (который, на мой взгляд, является здесь единственным правильным ответом) и изменил его в соответствии с последними изменениями API контейнера Unity:
public static IContainerRegistry RegisterDecorator<TInterface, TDecorator>(this IContainerRegistry container, ITypeLifetimeManager lifetimeManager, Type[] additionalInterfaces, params InjectionMember[] injectionMembers)
where TDecorator : class, TInterface
{
var unityContainer = container.GetContainer();
var existingRegistration = unityContainer.Registrations.LastOrDefault(r => r.RegisteredType == typeof(TInterface));
if (existingRegistration == null)
{
throw new ArgumentException("No existing registration found for the type " + typeof(TInterface));
}
var existing = existingRegistration.MappedToType;
var uniqueId = Guid.NewGuid().ToString();
// 1. Create a wrapper. This is the actual resolution that will be used
if (lifetimeManager != null)
{
unityContainer.RegisterType<TDecorator>(uniqueId, lifetimeManager, injectionMembers);
}
else
{
unityContainer.RegisterType<TDecorator>(uniqueId, injectionMembers);
}
unityContainer.RegisterType<TInterface, TDecorator>();
if (additionalInterfaces != null)
{
foreach (var additionalInterface in additionalInterfaces)
{
unityContainer.RegisterType(additionalInterface, typeof(TDecorator));
}
}
unityContainer.RegisterFactory<TDecorator>(DecoratorFactory);
return container;
object DecoratorFactory(IUnityContainer c)
{
// 3. We get the decorated class instance TBase
var baseObj = c.Resolve(existing);
// 4. We reference the wrapper TDecorator injecting TBase as TInterface to prevent stack overflow
return c.Resolve<TDecorator>(uniqueId, new DependencyOverride<TInterface>(baseObj));
}
}
Отличия заключаются в следующем:
IContainerRegistry
тип используется вместоIUnityContainer
- это потому, что я использую оболочки PRISM над контейнером UnityadditionalInterfaces
добавлен необязательный параметр, чтобы иметь возможность регистрировать декораторы, которые также реализуют другие интерфейсы- Логика изменена, чтобы соответствовать текущей реализации Unity API.