Sometime ago, I wrote this article for the MSDN Magazine, about Aspect Oriented Programming and how it could solve cross-cutting concerns in your application, like:
- Authentication
- Logging
- Data audit
- Data validation
- Data caching
- Performance measuring
The article shows how to use the Decorator pattern and the RealProxy class to create a Dynamic Proxy to solve these issues in a simple manner. If you want to have a full introduction on the subject, I suggest that you take a look at the article. The time has passed, things changed a lot and we are now close to the introduction of .NET 7. The RealProxy class doesn't exist anymore, as it's based in Remoting, which was not ported to .NET Core. Fortunately, we still have the System.Reflection.DispatchProxy class that can solve the problem.
With this class, we can still write proxies that decorate our classes and allow us to implement AOP in our programs. In this article, we will use the DispatchProxy class to create a dynamic proxy that allows us to implement a filter for the methods to be executed and execute other functions before and after the method execution.
In the command prompt, create a new console app with:
dotnet new console -o DynamicProxy
cd DynamicProxy
code .
In Program.cs, we will define a Customer record (put it at the end of the code):
record Customer(string Id, string Name, string Address);
Then, add a new Repository.cs file and add an IRepository
public interface IRepository<T>
{
void Add(T entity);
void Delete(T entity);
IEnumerable<T> GetAll();
}
The next step is to create the generic class Repository
public class Repository<T> : IRepository<T>
{
private readonly List<T> _entities = new List<T>();
public void Add(T entity)
{
_entities.Add(entity);
Console.WriteLine("Adding {0}", entity);
}
public void Delete(T entity)
{
_entities.Remove(entity);
Console.WriteLine("Deleting {0}", entity);
}
public IEnumerable<T> GetAll()
{
Console.WriteLine("Getting entities");
foreach (var entity in _entities)
{
Console.WriteLine($" {entity}");
}
return _entities;
}
}
As you can see, our repository class is a simple class that will store the entities in a list, delete and retrieve them.
With this class created, we can add the code to use it in Program.cs:
Console.WriteLine("***\r\n Begin program\r\n");
var customerRepository = new Repository<Customer>();
var customer = new Customer(1, "John Doe", "1 Main Street");
customerRepository.Add(customer);
customerRepository.GetAll();
customerRepository.Delete(customer);
customerRepository.GetAll();
Console.WriteLine("\r\nEnd program\r\n***");
If you run this program, you will see something like this:
Now, let's say we want to implement logging to this class, and have a log entry for every time it enters a method and another entry when it exits. We could do that manually, but it would be cumbersome to add logging before and after every method.
Using the DispatchProxy class, we can implement a proxy that will add logging to any class that implements the IRepository
using System.Reflection;
class RepositoryLogger<T> : DispatchProxy where T : class
{
T? _decorated;
public T? Create(T decorated)
{
var proxy = Create<T, RepositoryLogger<T>>() as RepositoryLogger<T>;
if (proxy != null)
{
proxy._decorated = decorated;
}
return proxy as T;
}
protected override object? Invoke(MethodInfo? methodInfo, object?[]? args)
{
if (methodInfo == null)
{
return null;
}
Log($"Entering {methodInfo.Name}");
try
{
var result = methodInfo.Invoke(_decorated, args);
Log($"Exiting {methodInfo.Name}");
return result;
}
catch
{
Log($"Error {methodInfo.Name}");
throw;
}
}
private static void Log(string msg)
{
Console.ForegroundColor = msg.StartsWith("Entering") ? ConsoleColor.Blue :
msg.StartsWith("Exiting") ? ConsoleColor.Green : ConsoleColor.Red;
Console.WriteLine(msg);
Console.ResetColor();
}
}
The RepositoryLogger class inherits from DispatchProxy and has a Create method that will create an instance of a class that implements the interface that's decorated. When we call the methods of this class, they are intercepted by the overriden Invoke method and we can add the logging before and after executing the method.
To use this new class, we can use something like:
Console.WriteLine("***\r\n Begin program\r\n");
var customerRepository = new Repository<Customer>();
var customerRepositoryLogger = new RepositoryLogger<IRepository<Customer>>().Create(customerRepository);
if (customerRepositoryLogger == null)
{
return;
}
var customer = new Customer(1, "John Doe", "1 Main Street");
customerRepositoryLogger.Add(customer);
customerRepositoryLogger.GetAll();
customerRepositoryLogger.Delete(customer);
customerRepositoryLogger.GetAll();
Console.WriteLine("\r\nEnd program\r\n***");
Now, running the code, we get:
We have logging entering and exiting the class without having to change it. Remove logging is as simple as changing one line of code.
With this knowledge, we can extend our proxy class to do any action we want. To add actions before, after and on error is just a matter of passing them in the creation of the proxy. We can create a DynamicProxy class with this code:
using System.Reflection;
class DynamicProxy<T> : DispatchProxy where T : class
{
T? _decorated;
private Action<MethodInfo>? _beforeExecute;
private Action<MethodInfo>? _afterExecute;
private Action<MethodInfo>? _onError;
private Predicate<MethodInfo> _shouldExecute;
public T? Create(T decorated, Action<MethodInfo>? beforeExecute,
Action<MethodInfo>? afterExecute, Action<MethodInfo>? onError,
Predicate<MethodInfo>? shouldExecute)
{
var proxy = Create<T, DynamicProxy<T>>() as DynamicProxy<T>;
if (proxy == null)
{
return null;
}
proxy._decorated = decorated;
proxy._beforeExecute = beforeExecute;
proxy._afterExecute = afterExecute;
proxy._onError = onError;
proxy._shouldExecute = shouldExecute ?? (s => true);
return proxy as T;
}
protected override object? Invoke(MethodInfo? methodInfo, object?[]? args)
{
if (methodInfo == null)
{
return null;
}
if (!_shouldExecute(methodInfo))
{
return null;
}
_beforeExecute?.Invoke(methodInfo);
try
{
var result = methodInfo.Invoke(_decorated, args);
_afterExecute?.Invoke(methodInfo);
return result;
}
catch
{
_onError?.Invoke(methodInfo);
throw;
}
}
}
In the Create method, we pass the actions we want to execute before after and on error after each method. We can also pass a predicate to filter the methods we don't want to execute. To use this new class, we can do something like this:
Console.WriteLine("***\r\n Begin program\r\n");
var customerRepository = new Repository<Customer>();
var customerRepositoryLogger = new DynamicProxy<IRepository<Customer>>().Create(customerRepository,
s => Log($"Entering {s.Name}"),
s => Log($"Exiting {s.Name}"),
s => Log($"Error {s.Name}"),
s => s.Name != "GetAll");
if (customerRepositoryLogger == null)
{
return;
}
var customer = new Customer(1, "John Doe", "1 Main Street");
customerRepositoryLogger.Add(customer);
customerRepositoryLogger.GetAll();
customerRepositoryLogger.Delete(customer);
customerRepositoryLogger.GetAll();
Console.WriteLine("\r\nEnd program\r\n***");
static void Log(string msg)
{
Console.ForegroundColor = msg.StartsWith("Entering") ? ConsoleColor.Blue :
msg.StartsWith("Exiting") ? ConsoleColor.Green : ConsoleColor.Red;
Console.WriteLine(msg);
Console.ResetColor();
}
Executing this code will show something like:
Note that, with this code, the method GetAll isn't executed, as it was filtered by the predicate.
As you can see, this is a very powerful class, as it can implement many different aspects for any interface (the DispatchProxy class only works with interfaces). For example, if I want to create my own mocking framework, where I don't execute any method of a class, I can change the code of the Invoke method to
protected override object? Invoke(MethodInfo? methodInfo, object?[]? args)
{
if (methodInfo == null)
{
return null;
}
_beforeExecute?.Invoke(methodInfo);
try
{
object? result = null;
if (_shouldExecute(methodInfo))
{
result = methodInfo.Invoke(_decorated, args);
}
_afterExecute?.Invoke(methodInfo);
return result;
}
catch
{
_onError?.Invoke(methodInfo);
throw;
}
}
And create the proxy with something like this:
var customerRepositoryLogger = new DynamicProxy<IRepository<Customer>>().Create(customerRepository,
s => Log($"Entering {s.Name}"),
s => Log($"Exiting {s.Name}"),
s => Log($"Error {s.Name}"),
s => false);
In this case, the real functions won't be called, just the methods before and after the call:
As you can see, the DispatchProxy class allows the creation of powerful classes that add aspects to your existing classes, without having to change them. With the DynamicProxy class you just have to add the actions to execute and the filter for the functions to be executed.
All the source code for this article is at https://github.com/bsonnino/DynamicProxy