Decorators
October 28, 2024 ยท View on GitHub
- Decorators
- Overview
- General use-case
- Decorator of service registered with serviceKey
- Nested Decorators
- Open-generic decorators
- Decorator of generic T
- Decorator Reuse
- Decorator of Wrapped Service
- Decorator of Wrapper
- Decorator as Initializer
- Decorator as Interceptor with Castle DynamicProxy
- Doing the interesting things with decorators
Overview
Decorator in DryIoc generally represents a Decorator Pattern. But in conjunction with other DryIoc features, especially with a FactoryMethod, the concept may be extended further to cover more scenarios.
DryIoc Decorators support:
- General decorating of service with adding functionality around decorated service calls.
- Applying decorator based on condition.
- Nesting decorators and specifying custom nesting order.
- Open-generic decorators with support of: generic variance, constraints, generic factory methods in generic classes.
- Decorator may have its own Reuse different from decorated service. There is an additional option for decorators to
useDecorateeReuse. - Decorator may decorate service wrapped in
Func,Lazyand the rest of Wrappers. Nesting is supported as well. - Decorator registered with a FactoryMethod may be used to add functionality around decorated service creation, aka Initializer.
- Combining decorator reuse and factory method registration, decorator may provide additional action on service dispose, aka Disposer.
- With Factory Method and decorator condition, it is possible to register decorator of generic
Tservice type. This opens possibility for more generic use-cases, e.g. EventAggregator with attributed subscribe. - Combining Decorator with library like Castle.DynamicProxy enables Interception and AOP support.
General use-case
We start by defining the IHandler which we will decorate adding the logging capabilities:
usings ...
namespace DryIoc.Docs;
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using DryIoc;
using DryIoc.FastExpressionCompiler.LightExpression; // light alternative to the System.Linq.Expressions
// ReSharper disable UnusedParameter.Local
public interface IHandler
{
bool IsHandled { get; }
void Handle();
}
public class FooHandler : IHandler
{
public bool IsHandled { get; private set; }
public void Handle() => IsHandled = true;
}
public interface ILogger
{
void Log(string line);
}
class InMemoryLogger : ILogger
{
public readonly List<string> Lines = new List<string>();
public void Log(string line) => Lines.Add(line);
}
public class LoggingHandlerDecorator : IHandler
{
private readonly IHandler _handler;
public readonly ILogger Logger;
public LoggingHandlerDecorator(IHandler handler, ILogger logger)
{
_handler = handler;
Logger = logger;
}
public bool IsHandled => _handler.IsHandled;
public void Handle()
{
Logger.Log("About to handle");
_handler.Handle();
Logger.Log("Successfully handled");
}
}
public class Decorator_with_logger
{
[Test]
public void Example()
{
var container = new Container();
container.Register<IHandler, FooHandler>();
container.Register<ILogger, InMemoryLogger>();
// decorator is the normal registration with `Decorator` setup
container.Register<IHandler, LoggingHandlerDecorator>(setup: Setup.Decorator);
// resolved handler will be a decorator instance
var handler = container.Resolve<IHandler>();
Assert.IsInstanceOf<LoggingHandlerDecorator>(handler);
handler.Handle();
CollectionAssert.AreEqual(
new[] { "About to handle", "Successfully handled" },
((handler as LoggingHandlerDecorator)?.Logger as InMemoryLogger)?.Lines);
}
}
Decorator of service registered with serviceKey
In most cases you only need to add setup: Setup.Decorator parameter.
The rest of registration options are available for decorators as for the normal services.
Except for the serviceKey, which is not supported. The serviceKey is not available because
really you don't want to register decorator with the serviceKey, instead you want decorator
to decorate service registered with specific serviceKey.
To apply decorator for service registered with serviceKey you need to specify a setup condition or
use a Decorator.Of method.
public class Decorator_of_service_with_serviceKey
{
[Test]
public void Example_with_condition()
{
var container = new Container();
container.Register<ILogger, InMemoryLogger>();
container.Register<IHandler, FooHandler>(serviceKey: "foo");
container.Register<IHandler, LoggingHandlerDecorator>(
setup: Setup.DecoratorWith(condition: request => "foo".Equals(request.ServiceKey)));
var foo = container.Resolve<IHandler>("foo");
Assert.IsInstanceOf<LoggingHandlerDecorator>(foo);
}
[Test]
public void Example_with_DecoratorOf()
{
var container = new Container();
container.Register<ILogger, InMemoryLogger>();
container.Register<IHandler, FooHandler>(serviceKey: "foo");
container.Register<IHandler, LoggingHandlerDecorator>(
setup: Setup.DecoratorOf(decorateeServiceKey: "foo")); // a condition in disguise
var foo = container.Resolve<IHandler>("foo");
Assert.IsInstanceOf<LoggingHandlerDecorator>(foo);
}
}
Using Setup.DecoratorOf
DecoratorOf is just a DecoratorWith(condition: ...) wrapped in a
more simple API to specify a decorated service key or/and type.
public class Decorator_of_service_with_key_and_type
{
class BooHandler : IHandler
{
public bool IsHandled { get; private set; }
public void Handle() => IsHandled = true;
}
[Test]
public void Example_with_DecoratorOf_type_and_key()
{
var container = new Container();
container.Register<ILogger, InMemoryLogger>();
container.Register<IHandler, FooHandler>(serviceKey: "foo");
container.Register<IHandler, BooHandler>(serviceKey: "boo");
// Decorating `Boo` with key "boo"
container.Register<IHandler, LoggingHandlerDecorator>(
setup: Setup.DecoratorOf<BooHandler>(decorateeServiceKey: "boo"));
var boo = container.Resolve<IHandler>("boo");
Assert.IsInstanceOf<LoggingHandlerDecorator>(boo);
}
[Test]
public void Example_with_DecoratorOf_type()
{
var container = new Container();
container.Register<ILogger, InMemoryLogger>();
container.Register<IHandler, FooHandler>();
container.Register<IHandler, BooHandler>(serviceKey: "boo");
// Decorating `Boo`
container.Register<IHandler, LoggingHandlerDecorator>(
setup: Setup.DecoratorOf<BooHandler>());
var boo = container.Resolve<IHandler>("boo");
Assert.IsInstanceOf<LoggingHandlerDecorator>(boo);
}
}
Nested Decorators
DryIoc supports decorator nesting. The first registered decorator will wrap the actual service, and the subsequent decorators will wrap the already decorated objects.
class S { }
class D1 : S { public D1(S s) { } }
class D2 : S { public D2(S s) { } }
public class Nested_decorators
{
[Test]
public void Example()
{
var container = new Container();
container.Register<S>();
container.Register<S, D1>(setup: Setup.Decorator);
container.Register<S, D2>(setup: Setup.Decorator);
var s = container.Resolve<S>();
// s is created as `new D2(new D1(new S()))`
Assert.IsInstanceOf<D2>(s);
// ACTUALLY, you even can see how service is created yourself
var expr = container.Resolve<LambdaExpression>(typeof(S));
StringAssert.Contains("new D2(new D1(new S()))", expr.ToString());
}
}
Decorators Order
The order of decorator nesting may be explicitly specified with order setup option:
public class Nested_decorators_order
{
[Test]
public void Example()
{
var container = new Container();
container.Register<S>();
container.Register<S, D1>(setup: Setup.DecoratorWith(order: 2));
container.Register<S, D2>(setup: Setup.DecoratorWith(order: 1));
var s = container.Resolve<S>();
Assert.IsInstanceOf<D1>(s);
var expr = container.Resolve<LambdaExpression>(typeof(S));
StringAssert.Contains("new D1(new D2(new S()))", expr.ToString());
}
}
The decorators without defined order have an implicit order value of 0. Decorators with identical order are ordered by registration.
You can specify -1, -2, etc. order to insert decorator closer to decorated service.
Note: The order does not prefer a specific decorator type, e.g. concrete, open-generic, or decorator of any generic T type. All decorators are ordered based on the same rules.
Open-generic decorators
Decorators may be open-generic and registered to wrap open-generic services.
// constructors are skipped for brevity, they just accept A parameter
class S<T> { }
class D1<T> : S<T> { }
class D2<T> : S<T> { }
class DStr : S<string> { }
public class Open_generic_decorators
{
[Test]
public void Example()
{
var container = new Container();
container.Register(typeof(S<>));
container.Register(typeof(S<>), typeof(D1<>), setup: Setup.Decorator);
container.Register(typeof(S<>), typeof(D2<>), setup: Setup.Decorator);
// decorator specific to the `S<string>` type
container.Register<S<string>, DStr>(setup: Setup.Decorator);
var intS = container.Resolve<S<int>>();
Assert.IsInstanceOf<D2<int>>(intS); // won't use DStr decorator
var strS = container.Resolve<S<string>>();
Assert.IsInstanceOf<DStr>(strS); // will use DStr decorator
}
}
Decorator of generic T
Decorator may be registered using a FactoryMethod. This brings an interesting question: what if the factory method is open-generic and returns a generic type T:
public interface IStartable
{
void Start();
}
public static class DecoratorFactory
{
public static T Decorate<T>(T service) where T : IStartable
{
service.Start();
return service;
}
}
Now we will use method Decorate to register decorator applied to any T service.
For this we need to register a decorator of type object, cause T does not make sense outside of its defined method.
public class X : IStartable
{
public bool IsStarted { get; private set; }
public void Start() => IsStarted = true;
}
public class Decorator_of_generic_T_type
{
[Test]
public void Example()
{
var container = new Container();
container.Register<X>();
// register with a service type of `object`
container.Register<object>(
made: Made.Of(req => typeof(DecoratorFactory)
.SingleMethod(nameof(DecoratorFactory.Decorate))
.MakeGenericMethod(req.ServiceType)),
setup: Setup.Decorator);
var x = container.Resolve<X>();
Assert.IsTrue(x.IsStarted);
}
}
The "problem" of object decorators that they will be applied to all services,
affecting the resolution performance.
To make decorator more targeted, we can provide the setup condition:
public class Decorator_of_generic_T_type_with_condition
{
[Test]
public void Example()
{
var container = new Container();
container.Register<X>();
var decorateMethod = typeof(DecoratorFactory)
.SingleMethod(nameof(DecoratorFactory.Decorate));
container.Register<object>(
made: Made.Of(r => decorateMethod.MakeGenericMethod(r.ServiceType)),
setup: Setup.DecoratorWith(r => r.ImplementationType.IsAssignableTo<IStartable>()));
// or we may simplify the condition via `DecoratorOf` method:
container.Register<object>(
made: Made.Of(r => decorateMethod.MakeGenericMethod(r.ServiceType)),
setup: Setup.DecoratorOf<IStartable>());
var x = container.Resolve<X>();
Assert.IsTrue(x.IsStarted);
}
}
Decorator Reuse
Decorator may have its own reuse the same way as a normal service. This way you may add a context-based reuse to already registered service just by applying the decorator.
Note: If no reuse specified for decorator, it means the decorator is of default container reuse container.Rules.DefaultReuse
(which is Reuse.Transient by default).
public class Decorator_reuse
{
[Test]
public void Example()
{
var container = new Container();
container.Register<S>();
// singleton decorator effectively makes the decorated service a singleton too.
container.Register<S, D1>(Reuse.Singleton, setup: Setup.Decorator);
var s1 = container.Resolve<S>();
var s2 = container.Resolve<S>();
Assert.AreSame(s1, s2);
}
}
UseDecorateeReuse
This setup option allows decorator to match the decorated service reuse whatever it might be. It is a good "default" option when you don't want to adjust decorated service reuse, but just want it to follow along.
public class Decoratee_reuse
{
[Test]
public void Example()
{
var container = new Container();
container.Register<S>(Reuse.Singleton);
container.Register<S, D1>(setup: Setup.DecoratorWith(useDecorateeReuse: true));
var s1 = container.Resolve<S>();
var s2 = container.Resolve<S>();
Assert.AreSame(s1, s2); // because of decoratee service reuse.
}
}
Decorator of Wrapped Service
Decorator may be applied to the wrapped service in order to provide laziness, create decorated service on demand, proxy-ing, etc.
class ACall
{
public virtual void Call() { }
}
class LazyCallDecorator : ACall
{
public Lazy<ACall> A;
public LazyCallDecorator(Lazy<ACall> a) { A = a; }
// Creates an object only when calling it.
public override void Call() => A.Value.Call();
}
public class Decorator_of_wrapped_service
{
[Test]
public void Example()
{
var container = new Container();
container.Register<ACall>();
container.Register<ACall, LazyCallDecorator>(setup: Setup.Decorator);
var a = container.Resolve<ACall>(); // A is not created.
a.Call(); // A is created and called.
}
}
Decorators of wrappers may be nested the same way as decorators of normal services:
class DLazy : S
{
public Lazy<S> S { get; }
public DLazy(Lazy<S> s) { S = s; }
}
class DFunc : S
{
public Func<S> S { get; }
public DFunc(Func<S> s) { S = s; }
}
public class Nesting_decorators_of_wrapped_service
{
[Test]
public void Example()
{
var container = new Container();
container.Register<S>();
container.Register<S, DLazy>(setup: Setup.Decorator);
container.Register<S, DFunc>(setup: Setup.Decorator);
var s = container.Resolve<S>(); // `s` is wrapped in Func with DFunc decorator
s = ((DFunc)s).S(); // then its wrapped in Lazy with DLazy decorator
s = ((DLazy)s).S.Value; // an actual decorated `S` object
Assert.IsInstanceOf<S>(s);
}
}
Decorator of Wrapper
DryIoc supports decorating of wrappers directly to adjust corresponding wrapper behavior, to add new functionality, to apply filtering, etc.
Consider the decorating of collection wrapper. Let`s say we want to change the default collection behavior and exclude keyed services from the result:
public interface I { }
public class A : I { }
public class B : I { }
public class C : I { }
public class Collection_wrapper_of_non_keyed_and_keyed_services
{
[Test]
public void Example()
{
var container = new Container();
container.Register<I, A>();
container.Register<I, B>();
container.Register<I, C>(serviceKey: "test");
// by default collection will contain instances of all registered types
var iis = container.Resolve<IEnumerable<I>>();
CollectionAssert.AreEqual(new[] { typeof(A), typeof(B), typeof(C) },
iis.Select(i => i.GetType()));
}
}
To exclude keyed service C we may define the decorator for IEnumerable<I>,
which accepts all instances and filters out the keyed things:
public class Decorator_of_wrapper
{
public static IEnumerable<I> GetNoneKeyed(IEnumerable<KeyValuePair<object, I>> all) =>
all.Where(kv => kv.Key is DefaultKey).Select(kv => kv.Value);
[Test]
public void Example()
{
var container = new Container();
container.Register<I, A>();
container.Register<I, B>();
container.Register<I, C>(serviceKey: "test");
// a collection decorator to filter out keyed services, a `C` with `"test"` key
container.Register<IEnumerable<I>>(
Made.Of(() => GetNoneKeyed(Arg.Of<IEnumerable<KeyValuePair<object, I>>>())),
setup: Setup.Decorator);
var iis = container.Resolve<IEnumerable<I>>();
CollectionAssert.AreEqual(new[] { typeof(A), typeof(B) },
iis.Select(i => i.GetType()));
}
}
Note: By decorating the IEnumerable<T> we are automatically decorating the array of T[] as well.
Decorator as Initializer
When registering decorator with FactoryMethod, it is possible to decorate an initialization pipeline of a service.
It means that decorator factory method accepts injected decorated service, invokes some initialization logic on the decorated instance, and returns this (or another) instance.
public class Decorator_as_initializer
{
public class Foo
{
public string Message { get; set; }
}
public static Foo DecorateFooWithGreet(Foo foo)
{
foo.Message = "Hello, " + (foo.Message ?? "");
return foo;
}
[Test]
public void Example()
{
var container = new Container();
container.Register<Foo>();
container.Register<Foo>(
Made.Of(() => DecorateFooWithGreet(Arg.Of<Foo>())),
setup: Setup.Decorator);
var foo = container.Resolve<Foo>();
StringAssert.Contains("Hello", foo.Message);
}
}
Here DecorateFooWithGreet is a static method just for the demonstration.
It also may be a non-static and include additional dependencies to be injected by Container,
check the FactoryMethod for more details.
DryIoc has a dedicated RegisterInitializer method,
which is a decorator in disguise.
Moreover, to complement the RegisterInitializer there is also a RegisterDisposer.
Decorator as Interceptor with Castle DynamicProxy
Doing interesting things with decorators
Reusing a scoped service from the parent scope
Originated from the issue #333.
Let's imagine that we have a scoped service and number of the nested scopes, and we want the already created service from the parent scope instead of creating the new one in the child scope:
public class Reusing_a_scoped_service_from_the_parent_scope
{
[Test]
public void Example()
{
var c = new Container();
// Generic wrapper to extract the FactoryID, we need it to lookup the scope for the created service
c.Register(typeof(FactoryInfo<>),
new ExpressionFactory(
req =>
{
var wrapperType = req.ServiceType;
var serviceType = wrapperType.GetGenericArguments()[0];
var factory = req.Container.ResolveFactory(req.Push(serviceType));
return Expression.New(wrapperType.SingleConstructor(), Expression.Constant(factory.FactoryID));
},
setup: Setup.Wrapper));
// Generic decorator applied to all Scoped services (see condition)
c.Register<object>(
made: Made.Of(req => GetType().SingleMethod(nameof(GetFromParentOrCurrent)).MakeGenericMethod(req.ServiceType)),
setup: Setup.DecoratorWith(condition: req => req.Reuse == Reuse.Scoped));
// Application code
c.Register<A>(Reuse.Scoped);
var s = c.OpenScope();
var a = s.Resolve<A>();
var ss = s.OpenScope();
var aa = ss.Resolve<A>();
Assert.AreSame(aa, a);
// health check that the other services are not affected
c.Register<B>(Reuse.Singleton);
Assert.IsNotNull(c.Resolve<B>());
}
// Application services
class A { }
class B { }
public class FactoryInfo<T>
{
public int Id;
public FactoryInfo(int id) => Id = id;
}
public static T GetFromParentOrCurrent<T>(Func<IResolverContext, T> fd, FactoryInfo<T> fi, IResolverContext r)
{
var id = fi.Id;
for (var s = r.CurrentScope; s != null; s = s.Parent)
if (s.TryGet(out var res, id))
return (T)res;
return fd(r);
}
}
Using the Decorator directly for the complex initialization
When you have a complex initialization scenario at your hands where RegisterInitializer behavior does not fit,
you may remember that RegisterInitializer is just a Decorator in disguise. So we may use it directly.
Especially, because the features of DryIoc are greatly composable - you can decorate wrapped services, inject the additional services (either into the decorator constructor or into the method or into both!), etc., etc.
Let's imagine that we need a transient DbContext which should be consumed lazily,
but requires a one-time initialization logic (say a database migration) based on the config provided.
Btw, this is the real case from the Gitter discussion with the user.
public class Using_the_Decorator_directly_for_the_complex_initialization
{
[Test]
public void Example()
{
var c = new Container();
var config = new Config();
c.RegisterInstance(config);
c.Register<DbContext>();
c.Register<InitDbContext>(Reuse.Singleton, serviceKey: "initializer"); // serviceKey is optional, here it basically hides the service unless you know its key
c.Register<DbContext>(setup: Setup.Decorator,
made: Made.Of(_ => ServiceInfo.Of<InitDbContext>(serviceKey: "initializer"),
f => f.Init(Arg.Of<DbContext>())));
var ctx0 = c.Resolve<Lazy<DbContext>>();
var ctx1 = c.Resolve<Lazy<DbContext>>();
var initializer = c.Resolve<InitDbContext>("initializer");
Assert.IsFalse(initializer.Initialized);
Assert.IsTrue(ctx0.Value.Migrated);
Assert.IsTrue(initializer.Initialized);
Assert.AreNotSame(ctx0.Value, ctx1.Value);
}
// DI infrastructure, but it does not need any knowledge of DryIoc - it may perfectly work and be tested without DryIoc.
class InitDbContext
{
Config _config;
public bool Initialized;
public InitDbContext(Config config) { _config = config; }
public DbContext Init(DbContext ctx)
{
if (!Initialized)
{
if (_config.ShouldMigrateDatabase())
ctx.MigrateDatabase();
Initialized = true;
}
return ctx;
}
}
// Application services
class DbContext
{
public bool Migrated;
public void MigrateDatabase() { Migrated = true; }
}
class Config
{
public bool ShouldMigrateDatabase() => true;
}
}