Completing tasks without writing code is the dream of any developer. In this article, we will learn a pattern that makes things happen without writing a line of code.
The philosophy is AOP (Aspect-Oriented Programming). This technique is widely used in Java and helps to keep high-quality standards with low effort. Today, we will learn how to use it also in .NET Core project with no pain. After a brief theoric explanation, you will find two examples (all the code is in my GitHub profile).
What is AOP
Let’s start from the Wikipedia definition:
Quote:
In computing, aspect-oriented programming (AOP) is a programming paradigm that aims to increase modularity by allowing the separation of cross-cutting concerns. It does so by adding additional behavior to existing code (an advice) without modifying the code itself […] This allows behaviors that are not central to the business logic (such as logging) to be added to a program without cluttering the code, core to the functionality. https://en.wikipedia.org/wiki/Aspect-oriented_programming
The concept is simple and can be summarized with one sentence.
Make things happen without writing code.
This applies to all the code that is needed but does not introduce any business logic.
Some examples of how AOP can change our code below. The first one is about logging.
public void SaveData(InputClass input)
{
Stopwatch timer= new Stopwatch();
timer.Start();
logger.Info("enterd SaveData method");
if(logger.LogLevel==LoggingLeve.Debug)
{
logger.Debug(MyJsonTool.ConvertToJson(input);
}
dataRepositoryInstance.Save(input);
timer.End();
logger.Info($"enterd SaveData method in {timer.ElapsedMilliseconds}ms");
}
What if I tell you that all this code can produce the same output just by writing this?
public virtual void SaveData(InputClass input)
{
dataRepositoryInstance.Save(input);
}
So, all work just adding a virtual keyword to the method, that’s great! We come back to the virtual keyword later to understand how this is related to AOP.
If you are not convinced by the power of AOP, just see how the code for data fetching can be simplified as follows:
[Fetch("SELECT * FROM customers WHERE name=?")]
public List<MyDTO> GetByName(string name)
{
return new List<MyDTO>();
}
I hope you are now quite convinced that AOP can help in many scenarios and can be a powerful ally. Let’s see how it works and how to integrate it into a .NET Core application.
This article contains two examples:
- A simple case using DinamyProxy and Autofact that intercept log
- A really nice deep through on AOP techniques that show how to implement an AOP engine
Example 1: The Automatic Controller Logging
In this sample, we will configure an interceptor to log all incoming requests. This can be extended to all other layers on our application, take it just as a proof of concept.
The Interceptor
The interceptor anatomy is very simple. Log it before and after the method execution. In this sample, I use a GUID to reference event logs together, but many improvements can be made.
public class AutofacModule : Module
{
protected override void Load(ContainerBuilder builder)
{
builder.Register(c => new CallLogger())
.Named<IInterceptor>("log-calls");
builder.Register(c => new ValuesService(c.Resolve<ILogger<ValuesService>>()))
.As<IValuesService>()
.InstancePerLifetimeScope()
.EnableInterfaceInterceptors();
}
}
public class CallLogger : IInterceptor
{
public void Intercept(IInvocation invocation)
{
var executionId = Guid.NewGuid().ToString();
Debug.WriteLine("{0} - Calling method {1} with parameters {2}... ",
executionId,
invocation.Method.Name,
JsonConvert.SerializeObject(invocation.Arguments));
invocation.Proceed();
Debug.WriteLine("{0} - Done: result was {1}.",
executionId,
JsonConvert.SerializeObject( invocation.ReturnValue));
}
}
We can discuss until tomorrow about how is stupid dumping data into logs, or we can improve this system to use a better logging system and a clever way to trace input, timing, and output.
As you see, there is a trace of method execution, with timing. Image this, out of the box, on all controllers of your ASP.NET Web API application, or in each service method on the business logic. Nice? It saves tons of lines of code.
Example 2:The Low Code Query Implementation
This example shows how to add a by-default behavior to methods, just by adding some annotation. This example is implemented from scratch, without using any library, to highlight how it works behind the hood.
The base class to create is a DispatcherProxy
. This class implements a proxy for a generic type that intercepts method calls and returns a custom object. This is what we need to replace an empty method with a working one.
Anyway, to implement a generic engine, we need something more. I created a generic attribute, called AOPAttribute
with a lot of fantasy. Each annotation that inherits this will need to implement the execution. Using this pattern, all the implementation is delegated to the attribute, and our Proxy engine is completely decoupled with the many implementations.
You can check the relevant parts of the code in the snippet below. Using just a few lines of code, we were able to implement a very powerful engine, but this is just an example. You can enjoy imagining how many use cases can be solved for you.
Did I tell you too quickly? Just start step by step.
Step 1: What We Want
First of all, we want to implement a mechanism that allows for implementing methods automatically. In C#, we cannot use DispatcherProxy
on classes but just on interfaces, so we will need to start always from an interface with all method declarations. Anyway, we also want to implement some methods manually, so we also will need a concrete class. Now there is the tricky point. If we let the class inherit from the interface, which is logic, we will be forced to implement all methods because that’s what the compiler requires. The trick I adopted is to simply forget about inheritance. The relationship between class and interface will be defined later, during DI.
Here the snippet of the FruitRepository
. The interface contains methods that will be automatically implemented and the InitDB
that is manually implemented.
public interface IFruitRepository
{
[Query("SELECT * FROM fruits where family={0}")]
List<Fruit> GetFruits(string tree);
[Query("SELECT * FROM fruits where family='Citrus'")]
List<Fruit> GetCitrus(string tree);
public void InitDB();
}
public class FruitRepositoryImpl
{
public void InitDB()
{
using (var db = new FruitDB())
{
db.Database.EnsureCreated();
var count = db.Fruits.Count();
if (count == 0)
{
db.Fruits.Add(new Fruit()
{
Name = "Lemon",
Family = "Citrus"
});
db.SaveChanges();
}
}
}
}
Step 2: The Proxy
What we need now is to create a proxy that will maintain a relationship between the interface and the implemented class, serving methods based on annotations. The code for this is very simple, see the snippet below:
public class ProxyFacotory<T> : DispatchProxy
{
private T _decorated;
protected override object Invoke(MethodInfo targetMethod, object[] args)
{
var annotations = targetMethod.GetCustomAttributes(true);
var aopAttr = annotations.FirstOrDefault
(x => typeof(AOPAttribute).IsAssignableFrom(x.GetType())) as AOPAttribute;
if (aopAttr != null)
{
return aopAttr.Execute(targetMethod, args, annotations);
}
var inherithedMethod=interfaceMethods.FirstOrDefault
(x => x.Name == targetMethod.Name);
var result = inherithedMethod.Invoke(_decorated, args);
return result;
}
public static T Create<T,TProxy>(TProxy instance) where T : class where TProxy:class
{
object proxy = Create<T, ProxyFacotory<TProxy>>();
((ProxyFacotory<TProxy>)proxy).SetParameters(instance);
return (T)proxy;
}
private void SetParameters(T decorated)
{
_decorated = decorated;
}
}
The usage is very simple and uses the regular .NET Core dependency injection.
var instance=ProxyFacotory<IFruitRepository>.Create<IFruitRepository,FruitRepositoryImpl>
(new FruitRepositoryImpl());
var serviceProvider = new ServiceCollection()
.AddSingleton<IFruitRepository>(instance)
.BuildServiceProvider();
Step 3: The Annotation
The base annotation for all is the AOPAnnotation
. This is an abstract
class that contains an Execute
method that replaced the usual method body. Then we have the Query annotation that, in our case, uses the query template passed from the developer to fetch data.
public abstract class AOPAttribute: Attribute
{
public abstract object Execute
(MethodInfo targetMethod, object[] args, object[] annotations);
}
public class QueryAttribute : AOPAttribute
{
public string Template { get; set; }
public QueryAttribute(string template)
{
this.Template = template;
}
public override object Execute
(MethodInfo targetMethod, object[] args, object[] annotations)
{
using (var data = new FruitDB())
{
return data.Fruits
.FromSqlRaw<Fruit>(this.Template,args ).ToList();
}
}
}
Step 4: See It in Action
Putting all together is very simple. Just use the repository as we have been writing it manually.
var fruitRepository = serviceProvider.GetService<IFruitRepository>();
fruitRepository.InitDB();
var fruits=fruitRepository.GetFruits("Citrus");
What to Take Home
AOP is a very interesting pattern because of automating code writing. It is very powerful, but has two weakness:
- Performance: a deep usage of reflection and additional steps in the elaboration, may increase computational times
- Loss of control: more the system does for you, more you don’t know how to fix
Modern tools and frameworks help to reduce code without using it, so it is not always necessary. Anyway, knowing it is very important when you are designing a framework or big infrastructure because it may be the right weapon to win the war. For example, when I designed the architecture of RawCMS, the opensource headless CMS, it was a good allied.
About performance or stability, just remember the Java Spring Framework. It uses it as a base for everything and is, nowadays, one of the best options for enterprise applications.
All the source code is there, on my GitHub profile
References
History
- 12th May, 2020: Initial version
I'm senior developer and architect specialized on portals, intranets, and others business applications. Particularly interested in Agile developing and open source projects, I worked on some of this as project manager and developer.
My programming experience include:
Frameworks \Technlogies: .NET Framework (C# & VB), ASP.NET, Java, php
Client languages:XML, HTML, CSS, JavaScript, angular.js, jQuery
Platforms:Sharepoint,Liferay, Drupal
Databases: MSSQL, ORACLE, MYSQL, Postgres