I implemented AOP in .NET Core 2. I’m starting to love the idea of transferring my programming skills in .NET to all platforms.
Here’s how I was able to apply on Mac the same ole’ techniques I’m used to on MS Windows.
An Opportunity
I recently switched jobs and started working at Nearsoft. It's a great company to work for. This switch allowed me to start looking at the latest things in the .NET world.
I'm a fan of C# and all the features that the language has gained over the years. However, I haven't been working on the latest versions of it, but I've been reading a lot about all the new stuff and doing exercise projects along with it.
At Nearsoft, I started to look at .NET Core 2 and how to implement the techniques I used previously. What stood out for me was Aspect Oriented Programming (AOP).
Aspect Oriented Programming
AOP is a programming paradigm that aims at increasing modularity by allowing the separation of side-effects from changes in the code itself. This is something that your app needs to do in a lot of different places, such as logging, caching, etc.
These behaviors that are not central to the business logic can be added without cluttering your code.
You'll be using Autofac and DynamicProxy from the Castle project for the examples, below.
You need the .NET Core framework on our system and optionally Visual Studio (VS). You can install them using homebrew and cask.
In fact, you should be using them both to install software on you mac. They’re great!
macbook:aop cblanco$ brew cask install dotnet-sdk
macbook:aop cblanco$ brew cask install visual-studio
New Project
Now with .NET in your machine, let's create a new console project. Open up a terminal window and run the following command,
macbook:dotnet cblanco$ dotnet new console -n aop
I'll be using Visual Studio for Mac for this post. So, let's open the solution in VS for Mac.
Cd
into the project's folder, run our solution, and "Hello World!" will appear on your console,
macbook:aop cblanco$ dotnet run
Hello World!
Using Autofac and DynamicProxy
So far so good. Now let's implement AOP using Autofac and DynamicProxy.
Let's add the nuget package Autofac.Extras.DynamicProxy to your solution. This package also adds the packages Autofac and Castle.Core as dependencies,
Autofac.Extras.DynamicProxy enables interceptions of method calls on Autofac components. This definition matches pretty much the goal of AOP. Good enough for me. Some common use-cases are logging, transaction handling, and caching.
There are four steps to implementing interception using DynamicProxy,
- Create Interceptors.
- Register Interceptors with Autofac.
- Enable Interception on Types.
- Associate Interceptors with Types to be Intercepted.
You’ll be implementing two interceptos. One for logging and another one for caching. You’ll then combine them so you can get a better picture of how the whole thing works.
Implementing a Logger
The first thing to do is implement the Castle.DynamicProxy.IInterceptor interface to create a new interceptor. This will log which method is being executed and which parameter values it is being fed. It will tell you how long it took to execute too. I think this is good enough for our example,
public class Logger: IInterceptor
{
TextWriter writer;
public Logger(TextWriter writer)
{
if(writer == null){
throw new ArgumentNullException(nameof(writer));
}
this.writer = writer
}
public void Intercept(IInvocation invocation)
{
var name = $"{invocation.Method.DeclaringType}.{invocation.Method.Name}";
var args = string.Join(", ", invocation.Arguments.Select(a => (a ?? "").ToString()));
writer.WriteLine($"Calling: {name}");
writer.WriteLine($"Args: {args}");
var watch = System.Diagnostics.Stopwatch.StartNew();
invocation.Proceed(); //Intercepted method is executed here.
watch.Stop();
var executionTime = watch.ElapsedMilliseconds;
writer.WriteLine($"Done: result was {invocation.ReturnValue}");
writer.WriteLine($"Execution Time: {executionTime} ms.");
writer.WriteLine();
}
}
Autofac
Then, you need to register our interceptor with Autofac. This enables you to then associate it to types. To set your Logger to log to Console.Out, pass Console.Out as a parameter to its constructor.
We can do this in our composition root like this,
var b = new ContainerBuilder();
b.Register(i=> new Logger(Console.Out));
var container = b.Build();
Enabling Interceptors
The third step is to enable interceptors on types which is done by calling the EnableInterfaceInterceptors method when registering your own types. Modify your composition root to register a type and enable interceptions on it,
var b = new ContainerBuilder();
b.Register(i=> new Logger(Console.Out));
b.RegisterType()
.As()
.EnableInterfaceInterceptors();
var container = b.Build();
The Calculator and ICalculator interface that you'll be using as the intercepted type contain a very simple implementation of an addition method that works very nicely for this example.
They look like this,
public interface ICalculator {
int add(int a, int b);
}
public class Calculator : ICalculator
{
public int add(int a, int b)
{
return a + b;
}
}
Last but Not Least
Finally, you have to associate interceptors with our types. You can do this while registering the type, by calling the InterceptedBy method, as follows,
b.RegisterType()
.As()
.EnableInterfaceInterceptors()
.InterceptedBy(typeof(Logger));
Once you have set this up, the methods in the Calculator type will be intercepted by the Logger interceptor which will then log accordingly,
macbook:aop cblanco$ dotnet run
Calling: aop.Domain.ICalculator.add
Args: 5, 8
Done: result was 13
Execution Time: 0 ms.
macbook:aop cblanco$
As you'll see, they execute quite fast.
Gotchas
I want to mention that you can use attributes to associate interceptors with types as well. Also, there are some gotchas to doing this. For example, you are not able to use it with private classes. There is more on this on Autofac's documentation.
But, Wait, There Is More!
So far so good. You created an interceptor that adds logging to your application and wraps your business logic without intermingling code. That's not all, though.
This technique for AOP allows you to layer interceptors to achieve more functionality.
You'll implement a very simple memory cache for our next sample. It won't be a production ready cache, but it will give you the idea of how to combine interceptors and how to start to write a memory cache.
Implementing a Memory Cache
Building on what you've done, you can now implement the IInterceptor interface to create a MemoryCaching interceptor. It will need memory to hold the return value of the intercepted methods so that it can give the return value from storage the next time the same method is executed instead of actually executing the method a second time.
It also has to take into account the value of the arguments fed to the intercepted method so it won't return incorrect values for subsequent calls.
The implementation looks like this,
public class MemoryCaching : IInterceptor
{
private Dictionary<string, object> cache = new Dictionary<string, object>();
public void Intercept(IInvocation invocation)
{
var name = $"{invocation.Method.DeclaringType}_{invocation.Method.Name}";
var args = string.Join(", ", invocation.Arguments.Select(a => (a ?? "").ToString()));
var cacheKey = $"{name}|{args}";
cache.TryGetValue(cacheKey, out object returnValue);
if (returnValue == null)
{
invocation.Proceed();
returnValue = invocation.ReturnValue;
cache.Add(cacheKey, returnValue);
}
else
{
invocation.ReturnValue = returnValue;
}
}
}
This implementation is lacking a lot still. Things like serialization of not primitive arguments and creating a hash of the string as the cache key. That is left for you to implement. You can use Json.NET and xxHash for that.
Layering Interceptors
Register both interceptors in your composition root. Combine them to intercept the Calculator type. For example,
var b = new ContainerBuilder();
b.Register(i => new Logger(Console.Out));
b.Register(i => new MemoryCaching());
b.RegisterType()
.As()
.EnableInterfaceInterceptors()
.InterceptedBy(typeof(Logger))
.InterceptedBy(typeof(MemoryCaching));
var container = b.Build();
Now, execute the add method in your Calculator type a few times and check the results.
var calc = container.Resolve();
calc.add(5, 8);
calc.add(5, 8);
calc.add(6, 8);
calc.add(6, 8);
calc.add(5, 8);
We can see in the output that the first time the method is executed it takes some time. Subsequent calls with the same values, however, are instant because they are retrieved from the cache.
Note that I stopped the executing thread for 1000ms to make the execution time more obvious.
macbook:aop cblanco$ dotnet run
Calling: aop.Domain.ICalculator.add
Args: 5, 8
Done: result was 13
Execution Time: 1006 ms.
Calling: aop.Domain.ICalculator.add
Args: 5, 8
Done: result was 13
Execution Time: 0 ms.
Calling: aop.Domain.ICalculator.add
Args: 6, 8
Done: result was 14
Execution Time: 1004 ms.
Calling: aop.Domain.ICalculator.add
Args: 6, 8
Done: result was 14
Execution Time: 0 ms.
Calling: aop.Domain.ICalculator.add
Args: 5, 8
Done: result was 13
Execution Time: 0 ms.
macbook:aop cblanco$
You have now combined two interceptors to add functionality to our app without modifying the code inside the Calculator type.
Your application can be extended without its code being modified directly. The new functionality is added to the existing code without touching it. That makes our work easier.
Finally…
This exercise gave me the chance to work with the latest version of .NET and C#. I learned that programming techniques that I'm used to can also be used in this new version.
I wrote this post on a Mac using Visual Studio and .NET. I like that now Microsoft is porting its technologies to other platforms.
I can now work with the language I like and not be tied to just one platform. Visual Studio for MS Windows is still the best IDE!