For .NET developers it is possible to ‘transfer’ their programming skills to all platforms.
Here’s how to apply on Mac the same techniques used to code Aspect Oriented Programming (AOP) in .NET Core 2 on Microsoft Windows .
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 an 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 the code with Autofac and DynamicProxy.
Guide to .NET in MS Windows
.NET Core framework and Visual Studio (VS) are necessary for this guide. They can be installed using homebrew and cask.
You can use them both to install software on a Mac. Both work great.
macbook:aop cblanco$ brew cask install dotnet-sdk
macbook:aop cblanco$ brew cask install visual-studio
Creating a New Project
Create a new console project. Open up a terminal window and run the following command,
macbook:dotnet cblanco$ dotnet new console -n aop
Visual Studio for Mac will be used in this guide. Open the solution in VS for Mac.
Cd into the project’s folder, run the solution, and “Hello World!” will appear on your console:
macbook:aop cblanco$ dotnet run
Hello World!
Using Autofac and DynamicProxy
Here’s how to implement AOP using Autofac and DynamicProxy.
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. 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.
1. 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 are being fed. It will tell you how long it took to execute too.
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();
}
}
2. Autofac
Then, you need to register the 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.
Do this in the composition root like this:
var b = new ContainerBuilder();
b.Register(i=> new Logger(Console.Out));
var container = b.Build();
3. 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 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;
}
}
4. Associating Interceptors
Finally, you have to associate interceptors with the 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.
You have 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.
In the next sample, a very simple memory cache will be implemented. It won’t be a production-ready cache, but it will teach you 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 can be built upon with things like serialization of not primitive arguments and creating a hash of the string as the cache key. Json.NET and xxHash can be used to accomplish it.
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();
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);
The output will show the first time the method is executed. Iit takes some time. Subsequent calls with the same values, however, are instant because they are retrieved from the cache.
Note that executing thread was stopped 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 the 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 coding easier.
Key Takeaways:
- .NET developers are not tied to a specific Operating System now that Microsoft has ported its technologies to other platforms.
- AOP aims to increase modularity by allowing the separation of side-effects from changes in base code.
- Logging and caching can be added to an app without cluttering the code with Autofac and DynamicProxy.
Conclusion
This guide offers the opportunity to work with the latest version of .NET and C#. Now that Microsoft has ported its technologies to other platforms, .NET can be used without being tied to a specific platform. Keep it mind Visual Studio for MS Windows is still the best IDE!
If you’d like to learn more about our app development services at Encora,
About Encora
At Encora, we create competitive advantages for client partners through accelerated technology innovation. We would be delighted to take you further along your journey.
Why Encora
- We’re global: With 20+ offices and innovation labs in 12 countries, Encora is globally dispersed. Since we operate in different time zones, there is always someone ready to assist you.
- We’re full-service: Technology innovation encompasses a huge array of topics, skills, and strategies. We offer a full-service concept, each component custom tailored as per your needs, to present a complete solution.
- We’re dedicated: Our ever-growing team of over 4,000 skilled and dedicated programmers, developers, engineers and strategic thinkers is the reason Encora has become an award-winning tech company with an enviable reputation.
- We’re experienced: We partner primarily with fast-growing tech companies who are driving innovation and growth within their industries. Our unique delivery model, agile methodology, and consistent unmatched quality have contributed to our steady growth.