David Haydnからのこの役立つ記事(編集:詐欺リンクが削除された可能性があります この記事 )は、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>());
}
@ DarkSquirrel42からの提案のおかげで、別のアプローチはInjectionFactory
を使用することです。欠点は、新しいコンストラクターパラメーターがチェーン内の何かに追加されるたびにコードを更新する必要があることです。利点は、コードを理解するのがはるかに簡単で、コンテナへの登録が1つだけです。
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>>();
のような誤った登録は、アプリケーションをスタックオーバーフローさせる無限再帰を引き起こします。これは、プラグインアーキテクチャの脆弱性である可能性があります。私はこれのためにかなり大雑把な拡張メソッドをノックアップしました。これは実行したときに期待どおりに動作しました。
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>();
最も簡潔答えうまくいく別の stackoverflow post マークシーマンによって言及されています。 conciseであり、名前付き登録を使用する必要はなく、Unity拡張機能の使用を提案する必要もありません。 Log4NetLoggerとDecoratorLoggerと呼ばれるデコレータ実装の2つの実装を持つILoggerと呼ばれるインターフェイスについて考えてみます。次のように、ILoggerインターフェイスに対してDecoratorLoggerを登録できます。
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でこれにもっとエレガントな解決策があるかどうか知りたいです