Click here to Skip to main content
15,879,095 members
Articles / Programming Languages / C#

How to Improve .NET Applications with AOP

Rate me:
Please Sign up or sign in to vote.
4.93/5 (18 votes)
11 May 2020CPOL6 min read 16.7K   13   4
Learn how to configure dependency injection to make things happen by design
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.

C#
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?

C#
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:

C#
[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:

  1. A simple case using DinamyProxy and Autofact that intercept log
  2. 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.

C#
public class AutofacModule : Module
  {
      protected override void Load(ContainerBuilder builder)
      {
          // Register the interceptor
          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.

Image 1

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.

C#
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"
                  });

                 //... all the fruit of the world here

                  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:

C#
public class ProxyFacotory<T> : DispatchProxy
{
    private T _decorated;

    protected override object Invoke(MethodInfo targetMethod, object[] args)
    {        
        //Find an annotation on the interface
        var annotations = targetMethod.GetCustomAttributes(true);
        var aopAttr = annotations.FirstOrDefault
        (x => typeof(AOPAttribute).IsAssignableFrom(x.GetType())) as AOPAttribute;
        
        //in case the method has an AOP implementation, this is executed
        if (aopAttr != null)
        {
            return aopAttr.Execute(targetMethod, args, annotations);
        }

        //otherwise, the manual implementation on class is triggered
        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.

C#
//the proxy instance return an instance based on the concrete implementation
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.

C#
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(); //Dataset can be 
                                                                //taken by target field
          }
      }
  }

Step 4: See It in Action

Putting all together is very simple. Just use the repository as we have been writing it manually.

C#
var fruitRepository = serviceProvider.GetService<IFruitRepository>();
//This calls the manual method and fill the database
fruitRepository.InitDB();
//This uses a dynamic definition
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:

  1. Performance: a deep usage of reflection and additional steps in the elaboration, may increase computational times
  2. 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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Chief Technology Officer
Italy Italy
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

Comments and Discussions

 
GeneralPartially disagree Pin
Boudino8-Jun-20 23:06
Boudino8-Jun-20 23:06 
SuggestionUse of virtual Pin
Member 975439520-May-20 5:05
professionalMember 975439520-May-20 5:05 
QuestionMagic Strings in Attributes Pin
Jeff Bowman18-May-20 13:11
professionalJeff Bowman18-May-20 13:11 
GeneralMy vote of 5 Pin
Guirec14-May-20 0:07
professionalGuirec14-May-20 0:07 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.