octobre 30, 2021

Aspect oriented programming in .NET

Aujourd’hui nous allons regarder comment implémenter le « pattern » AOP dans une application avec un exemple concret.

Tout d’abord il est important de définir l’AOP. l’AOP est une implémentation permettant de répondre aux « cross cutting concerns » c’est à dire aux préoccupations transversales qui impactent plusieurs fonctionnalités d’une application , comme par exemple l’injection de dépendances, le caching ou les logs.

Dans notre exemple, nous allons prendre l’exemple le plus courant qui est les logs et nous allons pouvoir mettre en place des logs très rapidement grace à l’AOP. En effet, le pattern permet d’éviter la duplication de code et de répondre et des préoccupations métier.

Nous allons commencer par créer une application .NET Web Api. Dans mon exemple, j’ai choisi de partir sur une application .NET 6.0 sans Controllers( Minimal API je reviendrai dessus dans un futur article 😉 ) .

Pour commencer nous allons créer un repositiory en mémoire pour avoir de la matière en termes d’API.

Tout d’abord le modèle dans un dossier Common/Models :

public class Article
    {
        public int Id { get; set; }

        public string? Name { get; set; }

        public string? Author { get; set; }

        public DateTime PublishDate { get; set; }
    }

Créer un dossier Repository et créer l’interface et la classe suivante :

 public interface IArticleRepository
    {
        IEnumerable<Article> GetArticles();

        Article GetArticle(int id);
        Article AddArticle(Article article);
    }
 public class ArticleRepository :IArticleRepository
    {
        private static readonly List<Article> Articles = new List<Article>
        {
            new Article
            {
                Id = 1,
                Author = "Alex CASTRO",
                Name = "AOP using PostSharp",
                PublishDate = new DateTime(2020, 10, 29)
            },
            new Article
            {
                Id = 2,
                Author = "Martin Fowler",
                Name = "Clean Code",
                PublishDate = new DateTime(2020, 1, 13)
            },
            new Article
            {
                Id = 3,
                Author = "Gang of Four",
                Name = "Design patterns",
                PublishDate = new DateTime(2020, 6, 30)
            },
            new Article
            {
                Id = 4,
                Author = "C. Mommer",
                Name = "Docker pour les développeurs .Net",
                PublishDate = new DateTime(2020, 7, 30)
            },
            new Article
            {
                Id = 5,
                Author = "C. Mommer",
                Name = "Blazor :développement front end d'application web dynamiques",
                PublishDate = new DateTime(2020, 8, 13)
            }
        };

        public  IEnumerable<Article> GetArticles()
        {
            return  Articles.OrderByDescending(article => article.PublishDate);
        }

        public Article GetArticle(int id)
        {
            return Articles.Single(article => article.Id == id);
        }

        public Article AddArticle(Article article)
        {
            article.Id = Articles.Max(c => c.Id)+1;
            Articles.ToList().Add(article);
            return article;
        }
}

Pour continuer, nous allons créer une classe de service en rajouter une classe et son interface comment suit :

public interface IArticleService
    {
        IEnumerable<Article> GetArticles();

        Article GetArticle(int id);

        Article AddArticle(Article article);
    }
 public class ArticleService : IArticleService
    {
        private readonly IArticleRepository _repository;

        public ArticleService(IArticleRepository repository)
        {
            _repository = repository;
        }

        public  IEnumerable<Article> GetArticles()
        {
            return  _repository.GetArticles();
        }

        public Article GetArticle(int id)
        {
            if (id <= 0)
            {
                throw new ArgumentException("The Id should be greater than 0", nameof(id));
            }

            return _repository.GetArticle(id);
        }

        public Article AddArticle(Article article)
        {
            if(article.Id <= 0)
            {
                throw new ArgumentException($"invalid id parameter for {typeof(Article)}");
            }
            else if(_repository.GetArticles().Any(c=>c.Id ==article.Id))
            {
                throw new ArgumentException($"already existing article {typeof(Article)}");
            }

            var articleAdded = _repository.AddArticle(article);
            return articleAdded;
        }

    }

Maintenant que tout est prêt, nous allons pouvoir implémenter notre « AOP ».

Pour cela il est possible de le faire de deux façons principalement :

  • utiliser PostSharp
  • utiliser Castle.Core

Nous allons commencer par PostSharp.

Pour cela créons une dossier Aspects dans le dossier Common créé précédemment puis ajoutons notre classe qui nous permettra de logger :

using AOP_Logging_PostSharp_Sample.Common.Extensions;
using Newtonsoft.Json;
using PostSharp.Aspects;
using PostSharp.Serialization;
using System.Diagnostics;
using System.Web;

namespace AOP_Logging_PostSharp_Sample.Common.Aspects
{

    [PSerializable]
    public class Log : OnMethodBoundaryAspect
    {


        /// <summary>
        /// Method executed before the body of methods to which this aspect is applied.
        /// </summary>
        /// <param name="args"></param>
        public override void OnEntry(MethodExecutionArgs args)
        {
            args.MethodExecutionTag = Stopwatch.StartNew();
            var logDescription = $"{args.FullMethodName()} - Starting.";
            if (args.Arguments != null && args.Arguments.Count > 0)
            {
                var parameters = args?.Method?.GetParameters()?.ToDictionary(key => key.Name, value => args.Arguments[value.Position]);

                // Serialize to JSON (Newtonesoft lib)
                logDescription += $" args: {JsonConvert.SerializeObject(parameters)}";
            }
            Serilog.Log.Information(logDescription);
        }

        /// <summary>
        /// Method executed after the body of methods to which this aspect is applied, but
        /// only when the method successfully returns (i.e. when no exception flies out the method.).
        /// </summary>
        /// <param name="args"></param>
        public override void OnSuccess(MethodExecutionArgs args)
        {
            Serilog.Log.Information($"{args.FullMethodName()} - Succeeded.");
        }

        /// <summary>
        /// Method executed after the body of methods to which this aspect is applied, even
        /// when the method exists with an exception (this method is invoked from the finally block).
        /// </summary>
        /// <param name="args"></param>
        public override void OnExit(MethodExecutionArgs args)
        {
            var sw = (Stopwatch)args.MethodExecutionTag;
            sw.Stop();
            Serilog.Log.Information($"{args.FullMethodName()} - Elapsed Time : {sw.Elapsed.Milliseconds}  - Exited.");
        }

        /// <summary>
        /// Method executed after the body of methods to which this aspect is applied, in
        /// case that the method resulted with an exception.
        /// </summary>
        /// <param name="args"></param>
        public override void OnException(MethodExecutionArgs args)
        {
            var logDescription = $"{args.FullMethodName()} - Failed.";

            if (args.Exception != null)
            {
                logDescription += $" message: {args.Exception.Message}";
            }

            Serilog.Log.Error(logDescription);
        }
    }


}

Cette classe hérite de OnMethodBoundaryAspect qui lui provient du pakcage Nuget PostSharp.

Une fois que tout cela est fait , nous allons pouvoir « décorer » nos classes afin de log automatiquement à chaque passage dans la classe décorée.

En exemple sur le service :

Pour tester celà j’ai créé des controllers qui apellent mon service comme suit :

Ainsi qu’un logger statique utilisable dans mon « aspect » de log déclaré dans mon program.cs :

Et le résultat ici :

Pour résumer, PostSharp est très efficace et très facile à mettre en place mais souffre d’un souci majeu qui est qu’il n’a pas de mécanisme d’injection de dépendances simple qui permettrait par exemple d’exécuter des actions plus complexes comme des appels à une base de données ou même récupérer simplement récupérer un nom d’utilisateur dans le contexte HTTP. Mais il a un avantage qui est qu’il implémente nativement des évènements comme OnEntry , OnExit qui permettent d’effectuer des actions à certains moment précis de l’éxécution.

Pour parer au souci d’injection de dépendances, nous allons implémenter l’AOP avec Castle.Core.

Créons un dossier Castle dans notre dossier Common et notre classe LoggingInterceptor qui héritera de IInterceptor ( qui héritera de Castle.DynamicProxy)

public class LoggingInterceptor : IInterceptor
    {
        private readonly IHttpContextAccessor _httpContextAccessor;


        public LoggingInterceptor(ILogger<LoggingInterceptor> logger , IHttpContextAccessor httpContextAccessor)
        {
            _httpContextAccessor = httpContextAccessor;
        }

        public void Intercept(IInvocation invocation)
        {
            Serilog.Log.Information($"Calling method {invocation.TargetType}.{invocation.Method.Name}.");
            invocation.Proceed();
        }
    }

Ensuite une autre classe qui va fournir l’injection de dépendances nécessaire pour exécuter notre logger.

 public static class ServicesExtensions
    {
        public static void AddProxiedScoped<TInterface, TImplementation>(this IServiceCollection services)
            where TInterface : class
            where TImplementation : class, TInterface
        {
            services.AddScoped<TImplementation>();
            services.AddScoped(typeof(TInterface), serviceProvider =>
            {
                var proxyGenerator = serviceProvider.GetRequiredService<ProxyGenerator>();
                var actual = serviceProvider.GetRequiredService<TImplementation>();
                var interceptors = serviceProvider.GetServices<IInterceptor>().ToArray();
                return proxyGenerator.CreateInterfaceProxyWithTarget(typeof(TInterface), actual, interceptors);
            });
        }
    }

Cette classe injecte les services et fait que le lien entre notre logger et la classe ou nous souhaitons avoir des logs.

Du coup pour chaque classe ou nous haitons utiliser notre logger Castle, nous aurons du code qui resselmblera à ceci dans notre StartUp (.NET 5.0) ou Program.cs (.NET 6.0) :

Voysons maintenant le résultat :

Pour conclure sur l’utilisation de Castle, c’est aussi très simple à mettre en place il a un avantage fort par rapport à PostSharp qui est l’injection de dépendances.

Nous en avons fini pour l’Aspect Oriented Programming , tout ce qui a été fait ici est extensible dans plein de domaines.

Ici les samples PostSharp :https://samples.postsharp.net/

Et Castle : https://github.com/castleproject/Core/blob/master/docs/dynamicproxy.md

Et pour finir le git de la solution de l’article : https://github.com/AlexCastroAlex/AOP-Logging-Sample

Happy coding !