If you are new to the decorator pattern, let me start with a short explanation:
Decorator is a structural design pattern that lets you attach new behaviors to objects by placing these objects inside special wrapper objects that contain the behaviors.
A common use case for decorators is to implement Aspect Oriented Programming(AOP) which allows you to implement cross-cutting concerns like logging, caching, …
There are multiple ways to implement this pattern, you can manually implement it, use your DI container, use a source generator to write the boilerplate code or use a dynamic proxy that wraps call to the original class.
It’s this last approach I want to focus on in this blog post. You could use the great Castle.DynamicProxy
library but for simpler use cases, there is a built-in alternative through the System.Reflection.DispatchProxy
class.
Let’s have a look at a small code example on how to use this class.
First we need to create a Decorator class that implements DispatchProxy
:
public class DispatchProxyLoggingDecorator<T> : DispatchProxy | |
where T : class | |
{ | |
public ILogger Logger { get; private set; } | |
public T Target { get; private set; } | |
protected override object Invoke(MethodInfo targetMethod, object[] args) | |
{ | |
try | |
{ | |
// Perform the logging that this proxy is meant to provide | |
Logger.LogInformation("Calling method {TypeName}.{MethodName} with arguments {Arguments}", targetMethod.DeclaringType.Name, targetMethod.Name, args); | |
var result = targetMethod.Invoke(Target, args); | |
// A little more logging | |
Logger.LogInformation("Method {TypeName}.{MethodName} returned {ReturnValue}", targetMethod.DeclaringType.Name, targetMethod.Name, result); | |
return result; | |
} | |
catch (TargetInvocationException exc) | |
{ | |
// If the MethodInvoke.Invoke call fails, log a warning and then rethrow the exception | |
Logger.LogWarning(exc.InnerException, "Method {TypeName}.{MethodName} threw exception: {Exception}", targetMethod.DeclaringType.Name, targetMethod.Name, exc.InnerException); | |
throw exc.InnerException; | |
} | |
} | |
} |
Remark: Be aware that we cannot use constructor injection to inject parameters(like the logger in the example above).
We could now use the DispatchProxy.Create()
method but I typically create a static method that allows me to wrap an existing class instance
public class DispatchProxyLoggingDecorator<T> : DispatchProxy | |
where T : class | |
{ | |
public static T Decorate(T target, ILogger logger) | |
{ | |
// DispatchProxy.Create creates proxy objects | |
var proxy = Create<T, DispatchProxyLoggingDecorator<T>>() | |
as DispatchProxyLoggingDecorator<T>; | |
// If the proxy wraps an underlying object, it must be supplied after creating the proxy. | |
proxy.Target = target; | |
proxy.Logger = logger; | |
return proxy as T; | |
} | |
public ILogger Logger { get; private set; } | |
public T Target { get; private set; } | |
protected override object Invoke(MethodInfo targetMethod, object[] args) | |
{ | |
try | |
{ | |
// Perform the logging that this proxy is meant to provide | |
Logger.LogInformation("Calling method {TypeName}.{MethodName} with arguments {Arguments}", targetMethod.DeclaringType.Name, targetMethod.Name, args); | |
var result = targetMethod.Invoke(Target, args); | |
// A little more logging | |
Logger.LogInformation("Method {TypeName}.{MethodName} returned {ReturnValue}", targetMethod.DeclaringType.Name, targetMethod.Name, result); | |
return result; | |
} | |
catch (TargetInvocationException exc) | |
{ | |
// If the MethodInvoke.Invoke call fails, log a warning and then rethrow the exception | |
Logger.LogWarning(exc.InnerException, "Method {TypeName}.{MethodName} threw exception: {Exception}", targetMethod.DeclaringType.Name, targetMethod.Name, exc.InnerException); | |
throw exc.InnerException; | |
} | |
} | |
} |
Let's create an interface, corresponding class and apply the decorator:
public class Example : IExample | |
{ | |
public string SayHello(string name) | |
{ | |
var message = $"Hello {name}"; | |
Console.WriteLine(message); | |
return message; | |
} | |
} |
public interface IExample | |
{ | |
string SayHello(string name); | |
} |
var loggerFactory= LoggerFactory.Create(builder => builder.AddConsole()); | |
var logger = loggerFactory.CreateLogger("test"); | |
var example = new Example(); | |
var decoratedExample=DispatchProxyLoggingDecorator<IExample>.Decorate(example,logger); | |
decoratedExample.SayHello("Bart"); | |
Console.ReadLine(); |
If we now run this code, the result looks like this:
Sweet!
There are some pros and cons when using this approach:
- Pros
- Works with .NET Standard 1.3+ (so it works with both .NET Framework 4.6+ and .NET Core/.NET 5).
- No 3th party libraries needed.
- Proxies by wrapping the target object, so a proxy can be created around an already existing object.
- Cons
- Proxies interfaces, not classes, so proxied types must implement an interface and access to any members not in the interface (like fields) is complicated.
- Uses reflection, so not usable with AOT.
- No support for async methods/functions.