Aspect-oriented programming (AOP) decomposes an application’s source code into distinct aspects that isolate the core business logic from cross-cutting concerns. With AOP, cross-cutting concerns like logging and authentication become “aspects” that you program in one place and then apply in all of the places they’re needed. Thus AOP enhances modularity, makes your code more readable and maintainable, and helps you reduce coding errors. You can learn more about AOP from my previous article here.
We’ve examined how we can implement AOP in C# using PostSharp and Autofac. In this article, we’ll look at implementing AOP in C# using the DispatchProxy class. To work with the code examples provided in this article, you should have Visual Studio 2022 Preview installed in your system. If you don’t already have a copy, you can download Visual Studio 2022 Preview here.
Create a console application project in Visual Studio 2022 Preview
First off, let’s create a .NET Core 9 console application project in Visual Studio 2022 Preview. Assuming you have Visual Studio 2022 Preview installed, follow the steps outlined below to create a new .NET Core 9 console application project.
- Launch the Visual Studio IDE.
- Click on “Create new project.”
- In the “Create new project” window, select “Console App (.NET Core)” from the list of templates displayed.
- Click Next.
- In the “Configure your new project” window, specify the name and location for the new project.
- Click Next.
- In the “Additional information” window shown next, choose “.NET 9.0 (Preview)” as the framework version you would like to use.
- Click Create.
We’ll use this .NET 9 console application project in the subsequent sections of this article.
The perils of cross-cutting concerns
Cross-cutting concerns are application concerns that span several modules or layers of an application. Typical examples include logging, authentication, caching, performance monitoring, and transaction management. These concerns add a degree of complexity to the application’s core business logic.
Aspect-oriented programming is a proven approach to addressing these challenges. By modularizing these concerns, AOP decreases code clutter and improves code readability and maintainability, thereby enabling your application’s source code to be flexible.
The DispatchProxy class in C#
The most commonly used technique for implementing aspect-oriented programming is to intercept method calls in AOP, using a dynamic proxy for code interception. The DispatchProxy class provides a way to create proxy objects in C# and intercept method calls. This feature helps implement aspects without polluting or complicating an application’s business logic with cross-cutting concerns.
Essentially, DispatchProxy is an abstract class pertaining to the System.Reflection namespace. It contains the declaration of only two methods, an abstract method named Create and a generic method named Invoke as shown below.
protected abstract object Invoke(MethodInfo targetMethod, object[] args);
public static T Create() where TProxy : DispatchProxy
Using the DispatchProxy class in C#
Consider the following code that contains an interface and a class that implements the interface.
interface IMyInterface
{
void Display(string text);
}
class MyClass : IMyInterface
{
public void Display(string text)
{
Console.WriteLine(text);
}
}
Create another class named MyClassDispatchProxy that extends the DispatchProxy class and implements the IMyInterface interface. We’ll use this class to create proxy object shortly.
class MyClassDispatchProxy : DispatchProxy where T : class, IMyInterface
{
private IMyInterface Target { get; set; }
protected override object Invoke
(MethodInfo targetMethod, object[] args)
{
return targetMethod.Invoke(Target, args);
}
public static T CreateProxy(T target)
{
var proxy = Create>() as MyClassDispatchProxy;
proxy.Target = target;
return proxy as T;
}
}
You can use the following piece of code to invoke the Display method using the proxy instance.
IMyInterface decoratedObject = MyClassDispatchProxy.CreateProxy(new MyClass());
decoratedObject.Display("This is a text message for testing purposes only.");
When you call the CreateProxy method, it calls the Create
Use DispatchProxy to add logging capabilities
Let us now update the MyClassDispatchProxy class to include logging capabilities and change its name to CustomLogger. To do this, create a new C# class named CustomLogger that extends the DispatchProxy class.
public class CustomLogger : DispatchProxy where T : class
{
private readonly ILogger _logger;
private T target;
protected override object Invoke
(MethodInfo targetMethod, object[] args)
{
throw new NotImplementedException();
}
public static T Create(T target)
{
throw new NotImplementedException();
}
}
The CustomLogger class shown in the preceding code uses Serilog to log data in this example. The following code snippet shows how you can update the Invoke method we created in the MyClassDispatchProxy class earlier to incorporate logging capabilities.
protected override object Invoke
(MethodInfo targetMethod, object[] args)
{
if(targetMethod == null)
throw new ArgumentNullException(nameof(targetMethod));
_logger.Information($"Entering method: {targetMethod.Name}...");
var result = targetMethod.Invoke(target, args);
_logger.Information($"Exiting method: {targetMethod.Name}...");
return result;
}
Note how the method calls are logged before and after invocation in the Invoke method. If the targetMethod instance is null, the Invoke method throws an exception. In the Create method, we create a new instance of the CustomLogger
Complete source code for DispatchProxy logging example
The complete source code of the CustomLogger example is given below for your reference.
public class CustomLogger : DispatchProxy where T : class
{
private readonly ILogger _logger;
private T target;
public CustomLogger()
{
_logger = new LoggerConfiguration()
.WriteTo.Console()
.CreateLogger();
}
protected override object Invoke
(MethodInfo targetMethod, object[] args)
{
if(targetMethod == null)
throw new ArgumentNullException(nameof(targetMethod));
_logger.Information($"Entering method: {targetMethod.Name}...");
var result = targetMethod.Invoke(target, args);
_logger.Information($"Exiting method: {targetMethod.Name}...");
return result;
}
public static T Create(T target)
{
var proxy = Create>() as CustomLogger;
proxy.target = target;
return proxy as T;
}
}
Run the application
Finally, create an instance of type IMyInterface that you would want to create a proxy object for and pass it to the Create
var myObject = new MyClass();
var proxy = CustomLogger.Create(myObject);
proxy.Display("Test message");
When you run the above piece of code, you’ll be able to see the text messages displayed before and after the method invocation as shown in Figure 1.
IDG
DispatchProxy proxies interfaces, not classes
The DispatchProxy class in C# enables you to intercept method calls and modify method invocations at runtime by using reflection. It is an excellent choice in applications where interface-based programming is used. Moreover, because the DispatchProxy class is available as part of the .NET Core library, you do not need to use any third-party libraries to implement aspect-oriented programming.
However, remember that DispatchProxy works only with interfaces, i.e., it proxies interfaces, not classes. You should also be aware of the potential performance implications (and be prepared to address any performance challenges) that might creep in because of the dynamic nature of proxy creation when working with DispatchProxy.