La programmation orientée aspect est un concept très attrayant pour simplifier votre base de code, créer du code propre et minimiser les erreurs de copier-coller.
Aujourd'hui, dans la plupart des cas, les aspects sont implémentés au niveau du bytecode, c'est-à-dire après la compilation, certains outils "entrelacent" un octet de code supplémentaire avec le support de la logique requise.
Notre approche (ainsi que l'approche de certains autres outils) consiste à modifier le code source pour implémenter la logique d'aspect. Avec le passage à la technologie Roslyn, il est très facile d'y parvenir, et le résultat donne certains avantages par rapport à la modification du code d'octet lui-même.
Si vous êtes intéressé par les détails, veuillez consulter le chat.
Vous pouvez penser que la programmation orientée aspect ne vous concerne pas et ne vous concerne pas particulièrement, juste un tas de mots incompréhensibles, mais en fait c'est beaucoup plus facile qu'il n'y paraît, il s'agit des problèmes de développement de produits réels et si vous êtes engagé dans le développement industriel, vous pouvez certainement obtenir bénéficier de son utilisation.
Surtout dans les projets de taille moyenne à grande au niveau de l'entreprise, où les exigences de fonctionnalité des produits sont formalisées. Par exemple, il peut y avoir une exigence - lors de la définition de l'indicateur de configuration, consignez tous les paramètres d'entrée pour toutes les méthodes publiques. Ou pour toutes les méthodes de projet d'avoir un système de notification qui enverra un message lorsqu'un certain seuil du temps d'exécution de cette méthode est dépassé.
Comment est-ce possible sans AOP? Soit il est martelé et fait uniquement pour les parties les plus importantes, soit lors de l'écriture de nouvelles méthodes, copier-coller du code similaire à partir de méthodes voisines, avec toutes les méthodes qui l'accompagnent, va.
Lors de l'utilisation d'AOP, un avis est rédigé une fois appliqué au projet et le travail est terminé. Lorsque vous avez besoin de mettre à jour un peu la logique, vous mettrez à nouveau à jour le conseil une fois et il sera appliqué à la prochaine version. Sans AOP, cela représente 100 500 mises à jour dans tout le code du projet.
Le plus est que votre code cesse de ressembler à une personne qui a eu la variole, car il est parsemé de telles fonctionnalités et lors de la lecture du code, il ressemble à un bruit ennuyeux.
Après avoir introduit AOP dans votre projet, vous commencez à implémenter des choses dont vous n'auriez jamais rêvé sans elle, car elles ressemblaient à un avantage relativement petit, à un coût élevé. Avec AOP, tout est exactement le contraire, des coûts relativement faibles et de gros avantages (pour un niveau de coût similaire de vos efforts).
Mon sentiment est que la programmation orientée aspect est nettement moins populaire dans l'écosystème .Net que dans l'écosystème Java. Je pense que la raison principale est le manque d'outils gratuits et open source comparables à la fonctionnalité et à la qualité de Java.
PostSharp offre des fonctionnalités et des commodités similaires, mais peu sont prêts à payer des centaines de dollars pour l'utiliser dans leurs projets, et la version communautaire est très limitée dans ses capacités. Bien sûr, il existe des alternatives, mais malheureusement elles n'ont pas atteint le niveau de PostSharp.
Vous pouvez comparer les capacités des outils (il faut garder à l'esprit que la comparaison a été faite par le propriétaire de PostSharp, mais cela donne une image).
Notre chemin vers la programmation orientée aspect
Nous sommes une petite société de conseil (12 personnes) et le résultat final de notre travail est le code source. Ceux. nous sommes payés pour créer du code source, du code de qualité. Nous ne travaillons que dans un seul secteur et beaucoup de nos projets ont des exigences très similaires et, par conséquent, le code source est également assez similaire entre ces projets.
Et comme nous sommes limités en ressources, l'une des tâches les plus importantes pour nous est la possibilité de réutiliser le code et d'utiliser des outils qui sauvent le développeur des tâches de routine.
Pour y parvenir, l'un des moyens est de faire un usage intensif des capacités de génération automatique de code et de créer plusieurs plugins et analyseurs personnalisés pour Visual Studio spécifiques à nos projets et tâches. Cela a permis d'augmenter considérablement la productivité des programmeurs, tout en maintenant une qualité de code élevée (on pourrait même dire que la qualité est devenue plus élevée).
La prochaine étape logique était l'idée de mettre en œuvre l'utilisation de la programmation orientée aspect. Nous avons essayé plusieurs approches et outils, mais le résultat était loin de nos attentes. Cela a coïncidé avec la sortie de la technologie Roslyn, et à un certain moment, nous avons eu l'idée de combiner les capacités de génération automatique de code et Roslyn.
En quelques semaines à peine, un prototype de l'instrument a été créé et, selon nos sentiments, cette approche semblait plus prometteuse. Après plusieurs itérations d'utilisation et de mise à jour de cet outil, nous pouvons dire que nos attentes ont été satisfaites et même plus que ce à quoi nous nous attendions. Nous avons développé une bibliothèque de modèles utiles et utilisons cette approche dans la plupart de nos projets, et certains de nos clients l'utilisent également et commandent même le développement de modèles pour leurs besoins.
Malheureusement, notre outil est encore loin d'être idéal, je voudrais donc diviser la description en deux parties, la première est de savoir comment je vois l'implémentation de cette fonctionnalité dans un monde idéal et la seconde, comment cela se fait ici.
Avant d'entrer dans les détails, j'aimerais faire une petite explication - tous les exemples de cet article ont été simplifiés à un niveau qui vous permet de montrer l'idée, sans être surchargé de détails non pertinents.
Comment cela se ferait dans un monde parfait
Après plusieurs années d'utilisation de notre outil, j'ai eu une vision de la façon dont j'aimerais que cela fonctionne si nous vivions dans un monde idéal.
Dans ma vision d'un monde idéal, les spécifications du langage permettent l'utilisation de transformations de code source, et il existe un support de compilateur et d'IDE.
L'idée a été inspirée par l'inclusion du modificateur "partiel" dans la spécification du langage C #. Ce concept assez simple (la possibilité de définir une classe, une structure ou une interface dans plusieurs fichiers) a considérablement amélioré et simplifié le support des outils de génération automatique de code source. Ceux. c'est une sorte de division horizontale du code source d'une classe entre plusieurs fichiers. Pour ceux qui ne connaissent pas le langage C #, un petit exemple.
Supposons que nous ayons un formulaire simple décrit dans le fichier Example1.aspx
<%@ Page Language="C#" AutoEventWireup="True" %>
// . . .
<asp:Button id="btnSubmit"
Text="Submit"
OnClick=" btnSubmit_Click"
runat="server"/>
// . . .
Et une logique personnalisée (par exemple, changer la couleur d'un bouton en rouge lorsque vous cliquez dessus) dans le fichier Example1.aspx.cs
public partial class ExamplePage1 : System.Web.UI.Page, IMyInterface
{
protected void btnSubmit_Click(Object sender, EventArgs e)
{
btnSubmit.Color = Color.Red;
}
}
La présence dans la langue des fonctionnalités fournies par "partial" permet à la boîte à outils d'analyser le fichier Example1.aspx et de générer automatiquement le fichier Example1.aspx.designer.cs.
public partial class ExamplePage1 : System.Web.UI.Page
{
protected global::System.Web.UI.WebControls.Button btnSubmit;
}
Ceux. nous avons la possibilité de stocker une partie du code de la classe ExamplePage1 dans un fichier par le programmeur pouvant être mis à jour (Example1.aspx.cs) et la partie dans le fichier Example1.aspx.designer.cs par la boîte à outils générée automatiquement. Pour le compilateur, au final, cela ressemble à une classe générale
public class ExamplePage1 : System.Web.UI.Page, IMyInterface
{
protected global::System.Web.UI.WebControls.Button btnSubmit;
protected void btnSubmit_Click(Object sender, EventArgs e)
{
btnSubmit.Color = Color.Red;
}
}
En utilisant l'exemple avec la définition de l'héritage de l'interface IMyInterface, vous pouvez voir que le résultat final est une combinaison de définitions de classe de différents fichiers.
Si nous manquons de fonctionnalités telles que partial et que le compilateur nécessite de stocker tout le code de classe dans un seul fichier, nous pouvons assumer les inconvénients et les gestes supplémentaires nécessaires pour prendre en charge la génération automatique.
En conséquence, mon idée est d'inclure deux modificateurs supplémentaires dans la spécification du langage, ce qui facilitera l'intégration des aspects dans le code source.
Le premier modificateur est original et nous l'ajoutons à la définition de classe qui devrait pouvoir être transformée.
La seconde est traitée et symbolise qu'il s'agit de la définition de classe finale obtenue par l'outil de transformation source et qui doit être acceptée par le compilateur pour générer le bytecode.
La séquence est quelque chose comme ça
- L'utilisateur travaille avec le code source de la classe qui contient le modificateur d'origine dans le fichier .cs (par exemple Example1.cs)
- Lors de la compilation, le compilateur vérifie l'exactitude du code source, et si la classe a été compilée avec succès, il vérifie la présence de l'original
- Si l'original est présent, le compilateur donne le code source de ce fichier au processus de transformation (qui est une boîte noire pour le compilateur).
- .processed.cs .processed.cs.map ( .cs .processed.cs, IDE)
- .processed.cs ( Example1.processed.cs) .
- ,
a. original processed
b. .cs .processed.cs - , .processed.cs .
Ceux. en ajoutant ces deux modificateurs, nous avons pu organiser le support des outils de transformation de code source au niveau du langage, tout aussi partiel a permis de simplifier le support pour la génération de code source. Ceux. parial est la division horizontale du code, l'original / traité est vertical.
À mon avis, implémenter le support original / traité dans le compilateur est une semaine de travail pour deux stagiaires chez Microsoft (une blague, bien sûr, mais ce n'est pas loin de la vérité). Dans l'ensemble, il n'y a pas de difficultés fondamentales dans cette tâche, du point de vue du compilateur, c'est la manipulation de fichiers et l'invocation de processus.
Dans .NET 5, une nouvelle fonctionnalité a été ajoutée - générateurs de code sourcequi vous permet déjà de générer de nouveaux fichiers de code source lors de la compilation et c'est un mouvement dans la bonne direction. Malheureusement, il vous permet uniquement de générer un nouveau code source, mais pas de modifier l'existant. Nous attendons donc toujours.
Un exemple de processus similaire. L'utilisateur crée le fichier Example2.cs
public original class ExamplePage2 : System.Web.UI.Page, IMyInterface
{
protected global::System.Web.UI.WebControls.Button btnSubmit;
protected void btnSubmit_Click(Object sender, EventArgs e)
{
btnSubmit.Color = Color.Red;
}
}
S'exécute pour la compilation, si tout va bien et que le compilateur voit le modificateur d'origine, il donne le code source au processus de transformation, qui génère le fichier Example2.processed.cs (dans le cas le plus simple, il peut simplement s'agir d'une copie exacte de Example2.cs avec l'original remplacé par traité) ...
Dans notre cas, nous supposerons que le processus de transformation a ajouté un aspect de journalisation et le résultat ressemble à ceci:
public processed class ExamplePage2 : System.Web.UI.Page, IMyInterface
{
protected global::System.Web.UI.WebControls.Button btnSubmit;
protected void btnSubmit_Click(Object sender, EventArgs e)
{
try
{
btnSubmit.Color = Color.Red;
}
catch(Exception ex)
{
ErrorLog(ex);
throw;
}
SuccessLog();
}
private static processed ErrorLog(Exception ex)
{
// some error logic here
}
private static processed SuccessLog([System.Runtime.CompilerServices.CallerMemberName] string memberName = "")
{
// some success logic here
}
}
L'étape suivante consiste à vérifier les signatures. Les signatures _ principales_ sont identiques et satisfont à la condition que les définitions d'origine et traitées doivent être exactement les mêmes.
Dans cet exemple, j'ai spécialement ajouté une autre petite phrase, c'est le modificateur traité pour les méthodes, les propriétés et les champs.
Il marque les méthodes, propriétés et champs comme disponibles uniquement pour les classes avec le modificateur traité et qui sont ignorées lors de la comparaison des signatures. Ceci est fait pour la commodité des développeurs d'aspect et vous permet de déplacer la logique générale dans des méthodes distinctes afin de ne pas créer de redondance de code inutile.
Le compilateur a compilé ce code et si tout va bien, il a pris l'octet de code pour continuer le processus.
Il est clair que dans cet exemple il y a une certaine simplification et en réalité la logique peut être plus compliquée (par exemple, lorsque nous incluons à la fois l'original et le partiel pour une classe), mais ce n'est pas une complexité insurmontable.
Fonctionnalité IDE de base dans un monde parfait
La prise en charge du travail avec le code source des fichiers .processed.cs dans l'EDI réside principalement dans la navigation correcte entre les classes d'origine / traitées et les transitions lors du débogage étape par étape.
La deuxième caractéristique la plus importante de l'EDI (de mon point de vue) est d'aider à lire le code des classes traitées. Une classe Processed peut contenir de nombreux morceaux de code qui ont été ajoutés par plusieurs aspects. La mise en œuvre d'un affichage similaire au concept de couches dans un éditeur graphique nous semble l'option la plus pratique pour atteindre cet objectif. Notre plugin actuel implémente quelque chose de similaire et la réponse de ses utilisateurs est assez positive.
Une autre fonctionnalité qui aiderait à introduire l'AOP dans la vie quotidienne est la fonctionnalité de refactoring. un utilisateur, mettant en évidence une partie du code, pourrait dire "Extraire vers un modèle AOP" et l'EDI a créé les bons fichiers, généré le code initial et après avoir analysé le code du projet, a suggéré des candidats pour l'utilisation d'un modèle d'autres classes.
Eh bien, la cerise sur le gâteau serait la prise en charge de l'écriture de modèles d'aspect, par exemple, en appliquant de manière interactive un aspect à une classe / méthode de votre choix afin que vous puissiez évaluer le résultat final à la volée, sans cycle de compilation explicite de votre part.
Je suis sûr que si les créateurs du réaffûteur reprennent l'entreprise, la magie est garantie.
Écrire un code d'aspect dans un monde parfait
Pour paraphraser TRIZ, l'écriture de code idéale pour l'implémentation d'aspects est l'absence d'écriture de code supplémentaire qui n'existe que pour supporter les processus d'instrumentation.
Dans un monde idéal, nous aimerions écrire du code pour l'aspect lui-même, sans l'effort d'écrire une logique d'aide pour atteindre cet objectif. Et ce code ferait partie intégrante du projet lui-même.
Le deuxième souhait est la possibilité d'avoir un plug & play interactif, c'est-à-dire après avoir écrit un modèle, nous n'aurions pas besoin de prendre des mesures supplémentaires pour qu'il soit utilisé pour la transformation. Il n'était pas nécessaire de recompiler l'outil, de détecter ses erreurs, etc. Et configurez également les options dans les projets pour la post-compilation.
Après avoir créé un modèle et écrit quelques lignes, je verrais immédiatement le résultat et s'il contient des erreurs, leur détection et leur débogage seraient intégrés dans le processus d'application du modèle et ne seraient pas une partie distincte qui nécessite un effort supplémentaire de la part du programmeur.
Eh bien, pour que la syntaxe du modèle soit aussi proche que possible de la syntaxe du langage C #, idéalement un add-on mineur, plus quelques mots-clés et espaces réservés.
Notre implémentation actuelle
Malheureusement, nous ne vivons pas dans un monde idéal, nous devons donc réinventer les vélos et les conduire.
Injection de code, compilation et débogage
Notre modèle actuel consiste à créer deux copies du projet. L'un est l'original avec lequel le programmeur travaille, le second est celui transformé, qui est utilisé pour la compilation et l'exécution.
Le scénario est quelque chose comme ça
- , , ..
- , , , .
- , , , WPF , ..
Pour le débogage, la deuxième copie de l'EDI est lancée, une copie du projet formée par pays est ouverte et elle fonctionne avec la copie à laquelle la transformation a été appliquée.
Le processus nécessite une certaine discipline, mais de temps en temps c'est devenu une habitude, et dans certains cas cette approche présente certains avantages (par exemple, une build peut être lancée et déployée sur un serveur distant, au lieu de travailler avec une machine locale). De plus, l'aide du plugin de VisualStudio simplifie le processus.
IDE
Nous utilisons un plugin adapté à nos tâches et processus spécifiques et la prise en charge de la mise en œuvre du code source ne représente qu'une petite partie de ses capacités.
Par exemple, la fonctionnalité d'affichage des couches, dans un style similaire à un éditeur graphique, permet, par exemple, de masquer / afficher les couches de commentaires, par étendue (par exemple, pour que seules les méthodes publiques soient visibles), les régions. Le code intégré est entouré de commentaires d'un format spécial, et ils peuvent également être masqués en tant que calque séparé.
Une autre possibilité est d'afficher une différence entre le fichier d'origine et le fichier transformé. puisque l'EDI connaît l'emplacement relatif de la copie du fichier dans le projet, il peut afficher les différences entre les fichiers d'origine et ceux générés par le pays.
En outre, le plugin avertit lorsque vous essayez d'apporter des modifications à la copie générée par le pays (afin de ne pas les perdre lors de la re-transformation ultérieure)
Configuration
Une tâche distincte consiste à définir les règles de transformation, c'est-à-dire à quelles classes et méthodes nous appliquerons la transformation.
Nous utilisons plusieurs niveaux.
Le premier niveau est le fichier de configuration de niveau supérieur. Nous pouvons définir des règles en fonction du chemin sur le système de fichiers, des modèles dans le nom des fichiers, des classes ou des méthodes, des portées des classes, des méthodes ou des propriétés.
Le deuxième niveau est une indication de l'application des règles de transformation au niveau des attributs de classes, méthodes ou champs.
Le troisième au niveau du bloc de code et le quatrième est une indication explicite pour inclure les résultats de la transformation du modèle à un endroit précis du code source.
Modèles
Historiquement, à des fins de génération automatique, nous utilisons des modèles au format T4, il était donc assez logique d'utiliser la même approche que les modèles de transformation. Les modèles T4 incluent la possibilité d'exécuter du code C # arbitraire, ont une surcharge minimale et une bonne expressivité.
Pour ceux qui n'ont jamais travaillé avec T4, l'analogue le plus simple serait de présenter le format ASPX, qui au lieu de HTML génère du code source en C # et est exécuté non pas sur IIS, mais comme un utilitaire séparé avec sortie du résultat sur la console (ou dans un fichier).
Exemples de
Pour comprendre comment cela fonctionne dans la réalité, le plus simple est de démontrer le code avant et après la transformation et le code source des modèles qui est utilisé lors de la transformation. Je vais vous montrer les options les plus simples, mais le potentiel n'est limité que par votre imagination.
Exemple de code source avant la transformation
// ##aspect=AutoComment
using AOP.Common;
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace Aspectimum.Demo.Lib
{
[AopTemplate("ClassLevelTemplateForMethods", NameFilter = "First")]
[AopTemplate("StaticAnalyzer", Action = AopTemplateAction.Classes)]
[AopTemplate("DependencyInjection", AdvicePriority = 500, Action = AopTemplateAction.PostProcessingClasses)]
[AopTemplate("ResourceReplacer", AdvicePriority = 1000, ExtraTag = "ResourceFile=Demo.resx,ResourceClass=Demo", Action = AopTemplateAction.PostProcessingClasses)]
public class ConsoleDemo
{
public virtual Person FirstDemo(string firstName, string lastName, int age)
{
Console.Out.WriteLine("FirstDemo: 1");
// ##aspect="FirstDemoComment" extra data here
return new Person()
{
FirstName = firstName,
LastName = lastName,
Age = age,
};
}
private static IConfigurationRoot _configuration = inject;
private IDataService _service { get; } = inject;
private Person _somePerson = inject;
[AopTemplate("LogExceptionMethod")]
[AopTemplate("StopWatchMethod")]
[AopTemplate("MethodFinallyDemo", AdvicePriority = 100)]
public Customer[] SecondDemo(Person[] people)
{
IEnumerable<Customer> Customers;
Console.Out.WriteLine("SecondDemo: 1");
Console.Out.WriteLine(i18("SecondDemo: i18"));
int configDelayMS = inject;
string configServerName = inject;
using (new AopTemplate("SecondDemoUsing", extraTag: "test extra"))
{
Customers = people.Select(s => new Customer()
{
FirstName = s.FirstName,
LastName = s.LastName,
Age = s.Age,
Id = s.Id
});
_service.Init(Customers);
foreach (var customer in Customers)
{
Console.Out.WriteLine(i18($"First Name {customer.FirstName} Last Name {customer.LastName}"));
Console.Out.WriteLine("SecondDemo: 2 " + i18("First Name ") + customer.FirstName + i18(" Last Name ") + customer.LastName);
}
}
Console.Out.WriteLine(i18(String.Format("SecondDemo: {0}", "3")));
Console.Out.WriteLine($"Server {configServerName} default delay {configDelayMS}");
Console.Out.WriteLine($"Customer for ID=5 is {_service.GetCustomerName(5)}");
return Customers.ToArray();
}
protected static string i18(string s) => s;
protected static dynamic inject;
[AopTemplate("NotifyPropertyChangedClass", Action = AopTemplateAction.Classes)]
[AopTemplate("NotifyPropertyChanged", Action = AopTemplateAction.Properties)]
public class Person
{
[AopTemplate("CacheProperty", extraTag: "{ \"CacheKey\": \"name_of_cache_key\", \"ExpiresInMinutes\": 10 }")]
public string FullName
{
get
{
// ##aspect="FullNameComment" extra data here
return $"{FirstName} {LastName}";
}
}
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
}
[AopTemplate("NotifyPropertyChanged", Action = AopTemplateAction.Properties)]
public class Customer : Person
{
public double CreditScore { get; set; }
}
public interface IDataService
{
void Init(IEnumerable<Customer> customers);
string GetCustomerName(int customerId);
}
public class DataService: IDataService
{
private IEnumerable<Customer> _customers;
public void Init(IEnumerable<Customer> customers)
{
_customers = customers;
}
public string GetCustomerName(int customerId)
{
return _customers.FirstOrDefault(w => w.Id == customerId)?.FullName;
}
}
public class MockDataService : IDataService
{
private IEnumerable<Customer> _customers;
public void Init(IEnumerable<Customer> customers)
{
if(customers == null)
throw (new Exception("IDataService.Init(customers == null)"));
}
public string GetCustomerName(int customerId)
{
if (customerId < 0)
throw (new Exception("IDataService.GetCustomerName: customerId cannot be negative"));
if (customerId == 0)
throw (new Exception("IDataService.GetCustomerName: customerId cannot be zero"));
return $"FirstName{customerId} LastName{customerId}";
}
}
}
}
Version complète du code source après transformation
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated from a template.
//
// Manual changes to this file may cause unexpected behavior in your application.
// Manual changes to this file will be overwritten if the code is regenerated.
//
// Generated base on file: ConsoleDemo.cs
// ##sha256: ekmmxFSeH5ev8Epvl7QvDL+D77DHwq1gHDnCxzeBWcw
// Created By: JohnSmith
// Created Machine: 127.0.0.1
// Created At: 2020-09-19T23:18:07.2061273-04:00
//
// </auto-generated>
//------------------------------------------------------------------------------
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
namespace Aspectimum.Demo.Lib
{
public class ConsoleDemo
{
public virtual Person FirstDemo(string firstName, string lastName, int age)
{
Console.Out.WriteLine("FirstDemo: 1");
// FirstDemoComment replacement extra data here
return new Person()
{FirstName = firstName, LastName = lastName, Age = age, };
}
private static IConfigurationRoot _configuration = new ConfigurationBuilder()
.SetBasePath(System.IO.Path.Combine(AppContext.BaseDirectory))
.AddJsonFile("appsettings.json", optional: true)
.Build();
private IDataService _service { get; } = new DataService();
#error Cannot find injection rule for Person _somePerson
private Person _somePerson = inject;
public Customer[] SecondDemo(Person[] people)
{
try
{
#error variable "Customers" doesn't match code standard rules
IEnumerable<Customer> Customers;
Console.Out.WriteLine("SecondDemo: 1");
#error Cannot find resource for a string "SecondDemo: i18", please add it to resources
Console.Out.WriteLine(i18("SecondDemo: i18"));
int configDelayMS = Int32.Parse(_configuration["delay_ms"]);
string configServerName = _configuration["server_name"];
{
// second demo test extra
{
Customers = people.Select(s => new Customer()
{FirstName = s.FirstName, LastName = s.LastName, Age = s.Age, Id = s.Id});
_service.Init(Customers);
foreach (var customer in Customers)
{
Console.Out.WriteLine(String.Format(Demo.First_Last_Names_Formatted, customer.FirstName, customer.LastName));
Console.Out.WriteLine("SecondDemo: 2 " + (Demo.First_Name + " ") + customer.FirstName + (" " + Demo.Last_Name + " ") + customer.LastName);
}
}
}
#error Argument for i18 method must be either string literal or interpolated string, but instead got Microsoft.CodeAnalysis.CSharp.Syntax.InvocationExpressionSyntax
#warning Please replace String.Format with string interpolation format.
Console.Out.WriteLine(i18(String.Format("SecondDemo: {0}", "3")));
Console.Out.WriteLine($"Server {configServerName} default delay {configDelayMS}");
Console.Out.WriteLine($"Customer for ID=5 is {_service.GetCustomerName(5)}");
return Customers.ToArray();
}
catch (Exception logExpn)
{
Console.Error.WriteLine($"Exception in SecondDemo\r\n{logExpn.Message}\r\n{logExpn.StackTrace}");
throw;
}
}
protected static string i18(string s) => s;
protected static dynamic inject;
public class Person : System.ComponentModel.INotifyPropertyChanged
{
public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName));
}
public string FullName
{
get
{
System.Runtime.Caching.ObjectCache cache = System.Runtime.Caching.MemoryCache.Default;
string cachedData = cache["name_of_cache_key"] as string;
if (cachedData == null)
{
cachedData = GetPropertyData();
if (cachedData != null)
{
cache.Set("name_of_cache_key", cachedData, System.DateTimeOffset.Now.AddMinutes(10));
}
}
return cachedData;
string GetPropertyData()
{
// FullNameComment FullName
return $"{FirstName} {LastName}";
}
}
}
private int _id;
public int Id
{
get
{
return _id;
}
set
{
if (_id != value)
{
_id = value;
NotifyPropertyChanged();
}
}
}
private string _firstName;
public string FirstName
{
get
{
return _firstName;
}
set
{
if (_firstName != value)
{
_firstName = value;
NotifyPropertyChanged();
}
}
}
private string _lastName;
public string LastName
{
get
{
return _lastName;
}
set
{
if (_lastName != value)
{
_lastName = value;
NotifyPropertyChanged();
}
}
}
private int _age;
public int Age
{
get
{
return _age;
}
set
{
if (_age != value)
{
_age = value;
NotifyPropertyChanged();
}
}
}
}
public class Customer : Person
{
private double _creditScore;
public double CreditScore
{
get
{
return _creditScore;
}
set
{
if (_creditScore != value)
{
_creditScore = value;
NotifyPropertyChanged();
}
}
}
}
public interface IDataService
{
void Init(IEnumerable<Customer> customers);
string GetCustomerName(int customerId);
}
public class DataService : IDataService
{
private IEnumerable<Customer> _customers;
public void Init(IEnumerable<Customer> customers)
{
_customers = customers;
}
public string GetCustomerName(int customerId)
{
return _customers.FirstOrDefault(w => w.Id == customerId)?.FullName;
}
}
public class MockDataService : IDataService
{
private IEnumerable<Customer> _customers;
public void Init(IEnumerable<Customer> customers)
{
if (customers == null)
throw (new Exception("IDataService.Init(customers == null)"));
}
public string GetCustomerName(int customerId)
{
if (customerId < 0)
throw (new Exception("IDataService.GetCustomerName: customerId cannot be negative"));
if (customerId == 0)
throw (new Exception("IDataService.GetCustomerName: customerId cannot be zero"));
return $"FirstName{customerId} LastName{customerId}";
}
}
}
}
// ##template=AutoComment sha256=Qz6vshTZl2/u+NgtcV4u5W5RZMb9JPkJ2Zj0yvQBH9w
// ##template=AopCsharp.ttinclude sha256=2QR7LE4yvfWYNl+JVKQzvEBwcWvReeupVpslWTSWQ0c
// ##template=FirstDemoComment sha256=eIleHCim5r9F/33Mv9B7pcNQ/dlfEhDVXJVhA7+3OgY
// ##template=FullNameComment sha256=2/Ipn8fk2y+o/FVQHAWnrOlhqS5ka204YctZkwl/CUs
// ##template=NotifyPropertyChangedClass sha256=sxRrSjUSrynQSPjo85tmQywQ7K4fXFR7nN2mX87fCnk
// ##template=StaticAnalyzer sha256=zmJsj/FWmjqDDnpZXhoAxQB61nYujd41ILaQ4whcHyY
// ##template=LogExceptionMethod sha256=+zTre3r3LR9dm+bLPEEXg6u2OtjFg+/V6aCnJKijfcg
// ##template=NotifyPropertyChanged sha256=PMgorLSwEChpIPnEWXfEuUzUm4GO/6pMmoJdF7qcgn8
// ##template=CacheProperty sha256=oktDGTfC2hHoqpbKkeNABQaPdq6SrVLRFEQdNMoY4zE
// ##template=DependencyInjection sha256=nPq/ZxVBpgrDzyH+uLtJvD1aKbajKinX/DUBQ4BGG9g
// ##template=ResourceReplacer sha256=ZyUljjKKj0jLlM2nUIr1oJc1L7otYUI8WqWN7um6NxI
Explications et codes modèles
Modèle de commentaire automatique
// ##aspect=AutoComment
Si dans le code source nous rencontrons un commentaire dans un format spécial, alors nous exécutons le modèle spécifié (dans ce cas, il s'agit d'AutoComment) et insérons le résultat de la transformation à la place de ce commentaire. Dans cet exemple, il est logique d'insérer automatiquement une clause de non-responsabilité spéciale qui avertira le programmeur que le code de ce fichier est le résultat d'une transformation et que modifier directement ce fichier n'a aucun sens.
Code de modèle AutoComment.t4
<#@ include file="AopCsharp.ttinclude" #>
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated from a template.
//
// Manual changes to this file may cause unexpected behavior in your application.
// Manual changes to this file will be overwritten if the code is regenerated.
//
// Generated base on file: <#= FileName #>
// ##sha256: <#= FileSha256 #>
// Created By: <#= User #>
// Created Machine: <#= MachineName #>
// Created At: <#= Now #>
//
// </auto-generated>
//------------------------------------------------------------------------------
Les variables FileName, FileSha256, User, MachineName et Now sont exportées vers le modèle à partir du processus de transformation.
Résultat de la transformation
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated from a template.
//
// Manual changes to this file may cause unexpected behavior in your application.
// Manual changes to this file will be overwritten if the code is regenerated.
//
// Generated base on file: ConsoleDemo.cs
// ##sha256: PV3lHNDftTzVYnzNCZbKvtHCbscT0uIcHGRR/NJFx20
// Created By: EuGenie
// Created Machine: 192.168.0.1
// Created At: 2017-12-09T14:49:26.7173975-05:00
//
// </auto-generated>
//------------------------------------------------------------------------------
La transformation suivante est spécifiée comme attribut de la classe
[AopTemplate ("ClassLevelTemplateForMethods", NameFilter = "First")]
Cet attribut signale que le modèle doit être appliqué à toutes les méthodes de classe contenant le mot "First". Le paramètre NameFilter est un modèle d'expression régulière utilisé pour déterminer les méthodes à inclure dans la transformation.
Code de modèle ClassLevelTemplateForMethods.t4
<#@ include file="AopCsharp.ttinclude" #>
// class level template
<#= MethodStart() #><#= MethodBody() #><#= MethodEnd() #>
Il s'agit de l'exemple le plus simple qui ajoute un commentaire
// class level template
avant le code de la méthode
Résultat de la transformation
// class level template
public virtual Person FirstDemo(string firstName, string lastName, int age)
{
Console.Out.WriteLine("FirstDemo: 1");
// ##aspect="FirstDemoComment" extra data here
return new Person()
{
FirstName = firstName,
LastName = lastName,
Age = age,
};
}
Les transformations suivantes sont spécifiées en tant qu'attributs de méthode pour illustrer plusieurs transformations appliquées à la même méthode. Modèle LogExceptionMethod.t4
[AopTemplate("LogExceptionMethod")]
[AopTemplate("StopWatchMethod")]
[AopTemplate("MethodFinallyDemo", AdvicePriority = 100)]
<#@ include file="AopCsharp.ttinclude" #>
<# EnsureUsing("System"); #>
<#= MethodStart() #>
try
{
<#= MethodBody() #>
}
catch(Exception logExpn)
{
Console.Error.WriteLine($"Exception in <#= MethodName #>\r\n{logExpn.Message}\r\n{logExpn.StackTrace}");
throw;
}
<#= MethodEnd() #>
Modèle StopWatchMethod.t4
<#@ include file="AopCsharp.ttinclude" #>
<# EnsureUsing("System.Diagnostics"); #>
<#= MethodStart() #>
var stopwatch = Stopwatch.StartNew();
try
{
<#= MethodBody() #>
}
finally
{
stopwatch.Stop();
Console.Out.WriteLine($"Method <#= MethodName #>: {stopwatch.ElapsedMilliseconds}");
}
<#= MethodEnd() #>
Modèle MethodFinallyDemo.t4
<#@ include file="AopCsharp.ttinclude" #>
<#= MethodStart() #>
try
{
<#= MethodBody() #>
}
finally
{
// whatever logic you need to include for a method
}
<#= MethodEnd() #>
Résultat des transformations
public Customer[] SecondDemo(Person[] people)
{
try
{
var stopwatch = Stopwatch.StartNew();
try
{
try
{
IEnumerable<Customer> customers;
Console.Out.WriteLine("SecondDemo: 1");
{
// second demo test extra
{
customers = people.Select(s => new Customer()
{FirstName = s.FirstName, LastName = s.LastName, Age = s.Age, });
foreach (var customer in customers)
{
Console.Out.WriteLine($"SecondDemo: 2 {customer.FirstName} {customer.LastName}");
}
}
}
Console.Out.WriteLine("SecondDemo: 3");
return customers.ToArray();
}
catch (Exception logExpn)
{
Console.Error.WriteLine($"Exception in SecondDemo\r\n{logExpn.Message}\r\n{logExpn.StackTrace}");
throw;
}
}
finally
{
stopwatch.Stop();
Console.Out.WriteLine($"Method SecondDemo: {stopwatch.ElapsedMilliseconds}");
}
}
finally
{
// whatever logic you need to include for a method
}
}
La transformation suivante est donnée pour un bloc limité à une construction using
using (new AopTemplate("SecondDemoUsing", extraTag: "test extra"))
{
customers = people.Select(s => new Customer()
{
FirstName = s.FirstName,
LastName = s.LastName,
Age = s.Age,
});
foreach (var customer in customers)
{
Console.Out.WriteLine($"SecondDemo: 2 {customer.FirstName} {customer.LastName}");
}
}
Modèle SecondDemoUsing.t4
<#@ include file="AopCsharp.ttinclude" #>
// second demo <#= ExtraTag #>
<#= StatementBody() #>
ExtraTag est une chaîne qui est passée en paramètre. Cela peut être utile pour les génériques qui peuvent avoir un comportement légèrement différent selon les paramètres d'entrée.
Résultat de la transformation
{
// second demo test extra
{
customers = people.Select(s => new Customer()
{FirstName = s.FirstName, LastName = s.LastName, Age = s.Age, });
foreach (var customer in customers)
{
Console.Out.WriteLine($"SecondDemo: 2 {customer.FirstName} {customer.LastName}");
}
}
}
La transformation suivante est spécifiée par les attributs de la classe NotifyPropertyChanged . Il s'agit d'un exemple classique qui, avec l'exemple de journalisation, est donné dans la plupart des exemples de programmation ASP.
[AopTemplate("NotifyPropertyChangedClass", Action = AopTemplaceAction.Classes)]
[AopTemplate("NotifyPropertyChanged", Action = AopTemplaceAction.Properties)]
Modèle NotifyPropertyChangedClass.t4 appliqué au code de classe
.
, AOP .Net
<#@ include file="AopCsharp.ttinclude" #>
<#
// the class already implements INotifyPropertyChanged, nothing to do here
if(ImplementsBaseType(ClassNode, "INotifyPropertyChanged", "System.ComponentModel.INotifyPropertyChanged"))
return null;
var classNode = AddBaseTypes<ClassDeclarationSyntax>(ClassNode, "System.ComponentModel.INotifyPropertyChanged");
#>
<#= ClassStart(classNode) #>
public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName));
}
<#= ClassBody(classNode) #>
<#= ClassEnd(classNode) #>
.
Fogy
using Mono.Cecil;
using Mono.Cecil.Cil;
using Mono.Cecil.Rocks;
public partial class ModuleWeaver
{
public void InjectINotifyPropertyChangedInterface(TypeDefinition targetType)
{
targetType.Interfaces.Add(new InterfaceImplementation(PropChangedInterfaceReference));
WeaveEvent(targetType);
}
void WeaveEvent(TypeDefinition type)
{
var propertyChangedFieldDef = new FieldDefinition("PropertyChanged", FieldAttributes.Private | FieldAttributes.NotSerialized, PropChangedHandlerReference);
type.Fields.Add(propertyChangedFieldDef);
var propertyChangedField = propertyChangedFieldDef.GetGeneric();
var eventDefinition = new EventDefinition("PropertyChanged", EventAttributes.None, PropChangedHandlerReference)
{
AddMethod = CreateEventMethod("add_PropertyChanged", DelegateCombineMethodRef, propertyChangedField),
RemoveMethod = CreateEventMethod("remove_PropertyChanged", DelegateRemoveMethodRef, propertyChangedField)
};
type.Methods.Add(eventDefinition.AddMethod);
type.Methods.Add(eventDefinition.RemoveMethod);
type.Events.Add(eventDefinition);
}
MethodDefinition CreateEventMethod(string methodName, MethodReference delegateMethodReference, FieldReference propertyChangedField)
{
const MethodAttributes Attributes = MethodAttributes.Public |
MethodAttributes.HideBySig |
MethodAttributes.Final |
MethodAttributes.SpecialName |
MethodAttributes.NewSlot |
MethodAttributes.Virtual;
var method = new MethodDefinition(methodName, Attributes, TypeSystem.VoidReference);
method.Parameters.Add(new ParameterDefinition("value", ParameterAttributes.None, PropChangedHandlerReference));
var handlerVariable0 = new VariableDefinition(PropChangedHandlerReference);
method.Body.Variables.Add(handlerVariable0);
var handlerVariable1 = new VariableDefinition(PropChangedHandlerReference);
method.Body.Variables.Add(handlerVariable1);
var handlerVariable2 = new VariableDefinition(PropChangedHandlerReference);
method.Body.Variables.Add(handlerVariable2);
var loopBegin = Instruction.Create(OpCodes.Ldloc, handlerVariable0);
method.Body.Instructions.Append(
Instruction.Create(OpCodes.Ldarg_0),
Instruction.Create(OpCodes.Ldfld, propertyChangedField),
Instruction.Create(OpCodes.Stloc, handlerVariable0),
loopBegin,
Instruction.Create(OpCodes.Stloc, handlerVariable1),
Instruction.Create(OpCodes.Ldloc, handlerVariable1),
Instruction.Create(OpCodes.Ldarg_1),
Instruction.Create(OpCodes.Call, delegateMethodReference),
Instruction.Create(OpCodes.Castclass, PropChangedHandlerReference),
Instruction.Create(OpCodes.Stloc, handlerVariable2),
Instruction.Create(OpCodes.Ldarg_0),
Instruction.Create(OpCodes.Ldflda, propertyChangedField),
Instruction.Create(OpCodes.Ldloc, handlerVariable2),
Instruction.Create(OpCodes.Ldloc, handlerVariable1),
Instruction.Create(OpCodes.Call, InterlockedCompareExchangeForPropChangedHandler),
Instruction.Create(OpCodes.Stloc, handlerVariable0),
Instruction.Create(OpCodes.Ldloc, handlerVariable0),
Instruction.Create(OpCodes.Ldloc, handlerVariable1),
Instruction.Create(OpCodes.Bne_Un_S, loopBegin), // go to begin of loop
Instruction.Create(OpCodes.Ret));
method.Body.InitLocals = true;
method.Body.OptimizeMacros();
return method;
}
}
, AOP .Net
Modèle NotifyPropertyChanged.t4 appliqué aux propriétés de classe
<#@ include file="AopCsharp.ttinclude" #>
<#
if(!(PropertyHasEmptyGetBlock() && PropertyHasEmptySetBlock()))
return null;
string privateUnqiueName = GetUniquePrivatePropertyName(ClassNode, PropertyNode.Identifier.ToString());
#>
private <#= PropertyNode.Type.ToFullString() #> <#= privateUnqiueName #><#= PropertyNode.Initializer != null ? " = " + PropertyNode.Initializer.ToFullString() : "" #>;
<#= PropertyNode.AttributeLists.ToFullString() + PropertyNode.Modifiers.ToFullString() + PropertyNode.Type.ToFullString() + PropertyNode.Identifier.ToFullString() #>
{
get { return <#= privateUnqiueName #>; }
set
{
if(<#= privateUnqiueName #> != value)
{
<#= privateUnqiueName #> = value;
NotifyPropertyChanged();
}
}
}
Code original de la classe et des propriétés
public class Person
{
public int Id { get; set; }
// ...
}
Résultat de la transformation
public class Person : System.ComponentModel.INotifyPropertyChanged
{
public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName));
}
private int _id;
public int Id
{
get
{
return _id;
}
set
{
if (_id != value)
{
_id = value;
NotifyPropertyChanged();
}
}
}
// ...
}
Un exemple de modèle pour la mise en cache des résultats de propriété, il est spécifié par les
[AopTemplate("CacheProperty", extraTag: "{ \"CacheKey\": \"name_of_cache_key\", \"ExpiresInMinutes\": 10 }")]
paramètres du modèle sont spécifiés comme attribut JSON. S'il n'y a pas de paramètres explicites, les paramètres par défaut sont utilisés.
Modèle CacheProperty.t4
<#@ include file="AopCsharp.ttinclude" #>
<#
// The template accepts a configuration value from extraTag in two ways
// 1. as a number of minutes to use for expiration (example: 8)
// 2. as a string in JSON in format { CacheKey: "name_of_cache_key", CacheKeyVariable: "name_of_variable", ExpiresInMinutes: 10, ExpiresVariable: "name_of_variable" }
//
// CacheKey (optional) name of the cache key, the name will be used as a literal string (example: my_key)
// CacheKeyVariable (optional) name of variable that holds the cache key (example: GlobalConsts.MyKeyName)
//
// ExpiresInMinutes (optional) number minutes that the cache value will expires (example: 12)
// ExpiresVariable (optional) name of a variable that the expiration value will be get from (example: AppConfig.EXPIRE_CACHE)
//
// if any of expiration values are not specified, 5 minutes default expiration will be used
if(!PropertyHasAnyGetBlock())
return null;
const int DEFAULT_EXPIRES_IN_MINUTES = 5;
string propertyName = PropertyNode.Identifier.ToFullString().Trim();
string propertyType = PropertyNode.Type.ToFullString().Trim();
string expiresInMinutes = DEFAULT_EXPIRES_IN_MINUTES.ToString();
string cacheKey = "\"" + ClassNode.Identifier.ToFullString() + ":" + propertyName + "\"";
if(!String.IsNullOrEmpty(ExtraTag))
{
if(Int32.TryParse(ExtraTag, out int exp))
{
expiresInMinutes = exp.ToString();
}
else
{
JsonDocument json = ExtraTagAsJson();
if(json != null && json.RootElement.ValueKind == JsonValueKind.Object)
{
if(json.RootElement.TryGetProperty("CacheKey", out JsonElement cacheKeyElement))
{
string s = cacheKeyElement.GetString();
if(!String.IsNullOrEmpty(s))
cacheKey = "\"" + s + "\"";
}
else if(json.RootElement.TryGetProperty("CacheKeyVariable", out JsonElement cacheVariableElement))
{
string s = cacheVariableElement.GetString();
if(!String.IsNullOrEmpty(s))
cacheKey = s;
}
if(json.RootElement.TryGetProperty("ExpiresInMinutes", out JsonElement expiresInMinutesElement))
{
if(expiresInMinutesElement.TryGetInt32(out int v) && v > 0)
expiresInMinutes = "" + v;
}
else if(json.RootElement.TryGetProperty("ExpiresVariable", out JsonElement expiresVariableElement))
{
string s = expiresVariableElement.GetString();
if(!String.IsNullOrEmpty(s))
expiresInMinutes = s;
}
}
}
}
#>
<#= PropertyDefinition() #>
{
get
{
System.Runtime.Caching.ObjectCache cache = System.Runtime.Caching.MemoryCache.Default;
<#= propertyType #> cachedData = cache[<#= cacheKey #>] as <#= propertyType #>;
if(cachedData == null)
{
cachedData = GetPropertyData();
if(cachedData != null)
{
cache.Set(<#= cacheKey #>, cachedData, System.DateTimeOffset.Now.AddMinutes(<#= expiresInMinutes #>));
}
}
return cachedData;
<#= propertyType #> GetPropertyData()
{
<# if(PropertyNode.ExpressionBody != null ) { #>
return (<#= PropertyNode.ExpressionBody.Expression.ToFullString() #>);
<# } else if(PropertyNode?.AccessorList?.Accessors.FirstOrDefault(w => w.ExpressionBody != null && w.Keyword.ToString() == "get") != null) { #>
return (<#= PropertyNode?.AccessorList?.Accessors.FirstOrDefault(w => w.ExpressionBody != null && w.Keyword.ToString() == "get").ExpressionBody.Expression.ToFullString() #>);
<# } else { #>
<#= PropertyGetBlock() #>
<# } #>
}
}
<#
if(PropertyHasAnySetBlock()) { #>
set
{
System.Runtime.Caching.ObjectCache cache = System.Runtime.Caching.MemoryCache.Default;
cache.Remove(<#= cacheKey #>); // invalidate cache for the property
<#= PropertySetBlock() #>
}
<# } #>
}
La source
[AopTemplate("CacheProperty", extraTag: "{ \"CacheKey\": \"name_of_cache_key\", \"ExpiresInMinutes\": 10 }")]
public string FullName
{
get
{
return $"{FirstName} {LastName}";
}
}
Résultat de la transformation pour CacheProperty.t4
public string FullName
{
get
{
System.Runtime.Caching.ObjectCache cache = System.Runtime.Caching.MemoryCache.Default;
string cachedData = cache["name_of_cache_key"] as string;
if (cachedData == null)
{
cachedData = GetPropertyData();
if (cachedData != null)
{
cache.Set("name_of_cache_key", cachedData, System.DateTimeOffset.Now.AddMinutes(10));
}
}
return cachedData;
string GetPropertyData()
{
// FullNameComment FullName
return $"{FirstName} {LastName}";
}
}
}
Le prochain appel au modèle à nouveau à partir du commentaire
// ##aspect="FullNameComment" extra data here
Modèle FullNameComment.t4
<#@ include file="AopCsharp.ttinclude" #>
// FullNameComment <#= PropertyNode.Identifier #>
Très similaire au modèle AutoComment.t4, mais ici nous démontrons l'utilisation de PropertyNode. De plus, les données "extra data here" sont disponibles pour le modèle FullNameComment.t4 via le paramètre ExtraTag (mais dans cet exemple, nous ne les utilisons pas, elles sont donc simplement ignorées)
Résultat de la transformation
// FullNameComment FullName
La transformation suivante dans le fichier est spécifiée par l'attribut
[AopTemplate("NotifyPropertyChanged", Action = AopTemplaceAction.Properties)]
AND est identique à celle de la classe Person. Le code source du modèle NotifyPropertyChanged.t4 a déjà été inclus ci-dessus.
Résultat de la transformation
public class Customer : Person
{
private double _creditScore;
public double CreditScore
{
get
{
return _creditScore;
}
set
{
if (_creditScore != value)
{
_creditScore = value;
NotifyPropertyChanged();
}
}
}
}
Partie finale
Bien que cet article se concentre sur la programmation orientée aspect, la technique de transformation du code source est universelle et, en principe, peut être utilisée pour des tâches qui ne sont pas liées à AOP.
Par exemple, il peut être utilisé pour l'injection de dépendances, c'est-à-dire nous modifions le code de création de ressource en fonction des paramètres de construction.
Modèle DependencyInjection.t4
<#@ include file="AopCsharp.ttinclude" #>
<#
var syntaxNode = FieldsInjection(SyntaxNode);
syntaxNode = VariablesInjection(syntaxNode);
syntaxNode = PropertiesInjection(syntaxNode);
if(syntaxNode == SyntaxNode)
return null;
#>
<#= syntaxNode.ToFullString() #>
<#+
private SyntaxNode VariablesInjection(SyntaxNode syntaxNode)
{
return RewriteNodes<LocalDeclarationStatementSyntax >(syntaxNode, OnLocalVariablesInjection);
SyntaxNode OnLocalVariablesInjection(LocalDeclarationStatementSyntax node)
{
var errorMsgs = new System.Text.StringBuilder();
SyntaxNode syntaxNode = RewriteNodes<VariableDeclaratorSyntax>(node, (n) => OnVariableDeclaratorVisit(n, node.Declaration.Type, errorMsgs));
if(errorMsgs.Length > 0)
return AddErrorMessageTrivia(syntaxNode, errorMsgs.ToString());
return syntaxNode;
}
}
private SyntaxNode PropertiesInjection(SyntaxNode syntaxNode)
{
return RewriteNodes<PropertyDeclarationSyntax>(syntaxNode, OnPropertyInjection);
SyntaxNode OnPropertyInjection(PropertyDeclarationSyntax node)
{
if(node.Initializer?.Value?.ToString() != "inject")
return node;
var errorMsgs = new System.Text.StringBuilder();
SyntaxNode syntaxNode = DoInjection(node, node.Identifier.ToString().Trim(), node.Initializer.Value, node.Type, errorMsgs);
if(errorMsgs.Length > 0)
return AddErrorMessageTrivia(syntaxNode, errorMsgs.ToString());
return syntaxNode;
}
}
private SyntaxNode FieldsInjection(SyntaxNode syntaxNode)
{
return RewriteNodes<BaseFieldDeclarationSyntax>(syntaxNode, OnFieldsInjection);
SyntaxNode OnFieldsInjection(BaseFieldDeclarationSyntax node)
{
var errorMsgs = new System.Text.StringBuilder();
SyntaxNode syntaxNode = RewriteNodes<VariableDeclaratorSyntax>(node, (n) => OnVariableDeclaratorVisit(n, node.Declaration.Type, errorMsgs));
if(errorMsgs.Length > 0)
return AddErrorMessageTrivia(syntaxNode, errorMsgs.ToString());
return syntaxNode;
}
}
private SyntaxNode OnVariableDeclaratorVisit(VariableDeclaratorSyntax node, TypeSyntax typeSyntax, System.Text.StringBuilder errorMsgs)
{
if(node.Initializer?.Value?.ToString() != "inject")
return node;
return DoInjection(node, node.Identifier.ToString().Trim(), node.Initializer.Value, typeSyntax, errorMsgs);
}
private SyntaxNode DoInjection(SyntaxNode node, string varName, ExpressionSyntax initializerNode, TypeSyntax typeSyntax, System.Text.StringBuilder errorMsgs)
{
string varType = typeSyntax.ToString().Trim();
Log($"{varName} {varType} {initializerNode.ToString()}");
if(varName.StartsWith("config"))
{
string configName = Regex.Replace(Regex.Replace(varName, "^config", ""), "([a-z])([A-Z])", (m) => m.Groups[1].Value + "_" + m.Groups[2].Value).ToLower();
ExpressionSyntax configNode = CreateElementAccess("_configuration", CreateStringLiteral(configName));
if(varType == "int")
{
configNode = CreateMemberAccessInvocation("Int32", "Parse", configNode);
}
return node.ReplaceNode(initializerNode, configNode);
}
switch(varType)
{
case "Microsoft.Extensions.Configuration.IConfigurationRoot":
case "IConfigurationRoot":
EnsureUsing("Microsoft.Extensions.Configuration");
ExpressionSyntax pathCombineArg = CreateMemberAccessInvocation("System.IO.Path", "Combine", CreateMemberAccess("AppContext", "BaseDirectory"));
ExpressionSyntax builderNode = CreateNewType("ConfigurationBuilder").WithTrailingTrivia(SyntaxFactory.EndOfLine("\r\n"));
builderNode = CreateMemberAccessInvocation(builderNode, "SetBasePath", pathCombineArg).WithTrailingTrivia(SyntaxFactory.EndOfLine("\r\n"));
ExpressionSyntax addJsonFileArg = CreateMemberAccessInvocation("System.IO.Path", "Combine", CreateMemberAccess("AppContext", "BaseDirectory"));
builderNode = CreateMemberAccessInvocationNamedArgs(builderNode, "AddJsonFile",
(null, CreateStringLiteral("appsettings.json")),
("optional", SyntaxFactory.LiteralExpression(SyntaxKind.TrueLiteralExpression))).WithTrailingTrivia(SyntaxFactory.EndOfLine("\r\n"));
if(GetGlobalSetting("env")?.ToLower() == "test")
{
builderNode = CreateMemberAccessInvocationNamedArgs(builderNode, "AddJsonFile",
(null, CreateStringLiteral("appsettings.test.json")),
("optional", SyntaxFactory.LiteralExpression(SyntaxKind.FalseLiteralExpression)));
}
builderNode = CreateMemberAccessInvocation(builderNode, "Build");
return node.ReplaceNode(initializerNode, builderNode);
case "IDataService":
{
string className = (GetGlobalSetting("env")?.ToLower() == "test" ? "MockDataService" : "DataService");
return node.ReplaceNode(initializerNode, CreateNewType(className));
}
}
errorMsgs.AppendLine($"Cannot find injection rule for {varType} {varName}");
return node;
}
#>
Dans le code source (ici, la fonction de variable dynamique est utilisée, ce qui permet de les affecter à n'importe quel type), c.-à-d. pour l'expressivité, nous avons en quelque sorte trouvé un nouveau mot-clé.
private static IConfigurationRoot _configuration = inject;
private IDataService _service { get; } = inject;
// ...
public Customer[] SecondDemo(Person[] people)
{
int configDelayMS = inject; // we are going to inject dependency to local variables
string configServerName = inject;
}
// ...
protected static dynamic inject;
Lors de la transformation, la comparaison GetGlobalSetting ("env") == "test" est utilisée, et en fonction de cette condition, un nouveau DataService () ou un nouveau MockDataService () sera injecté.
Résultat de la transformation
private static IConfigurationRoot _configuration = new ConfigurationBuilder()
.SetBasePath(System.IO.Path.Combine(AppContext.BaseDirectory))
.AddJsonFile("appsettings.json", optional: true)
.Build();
private IDataService _service { get; } = new DataService();
// ...
public Customer[] SecondDemo(Person[] people)
{
int configDelayMS = Int32.Parse(_configuration["delay_ms"]);
string configServerName = _configuration["server_name"];
}
// ...
Ou vous pouvez utiliser cet outil comme une analyse statique «pauvre» (mais il est beaucoup, beaucoup plus correct d'implémenter des analyseurs utilisant la fonctionnalité native de Roslyn), nous analysons le code de nos règles et l'insérons dans le code source.
#error our error message here
Cela entraînera une erreur de compilation.
#warning our warning message here
Ce qui servira d'avertissement dans l'EDI ou lors de la compilation.
Modèle StaticAnalyzer.t4
<#@ include file="AopCsharp.ttinclude" #>
<#
var syntaxNode = AnalyzeLocalVariables(SyntaxNode);
syntaxNode = AnalyzeStringFormat(syntaxNode);
if(syntaxNode == SyntaxNode)
return null;
#>
<#= syntaxNode.ToFullString() #>
<#+
private SyntaxNode AnalyzeLocalVariables(SyntaxNode syntaxNode)
{
return RewriteNodes<LocalDeclarationStatementSyntax>(syntaxNode, OnAnalyzeLocalVariablesNodeVisit);
SyntaxNode OnAnalyzeLocalVariablesNodeVisit(LocalDeclarationStatementSyntax node)
{
var errorMsgs = new System.Text.StringBuilder();
string d = "";
foreach(VariableDeclaratorSyntax variableNode in node.DescendantNodes().OfType<VariableDeclaratorSyntax>().Where(w => Regex.IsMatch(w.Identifier.ToString(), "^[A-Z]")))
{
LogDebug($"variable: {variableNode.Identifier.ToString()}");
errorMsgs.Append(d + $"variable \"{variableNode.Identifier.ToString()}\" doesn't match code standard rules");
d = ", ";
}
if(errorMsgs.Length > 0)
return AddErrorMessageTrivia(node, errorMsgs.ToString());
return node;
}
}
private SyntaxNode AnalyzeStringFormat(SyntaxNode syntaxNode)
{
return RewriteLeafStatementNodes(syntaxNode, OnAnalyzeStringFormat);
SyntaxNode OnAnalyzeStringFormat(StatementSyntax node)
{
bool hasStringFormat = false;
foreach(MemberAccessExpressionSyntax memberAccessNode in node.DescendantNodes().OfType<MemberAccessExpressionSyntax>())
{
if(memberAccessNode.Name.ToString().Trim() != "Format")
continue;
string expr = memberAccessNode.Expression.ToString().Trim().ToLower();
if(expr != "string" && expr != "system.string")
continue;
hasStringFormat = true;
break;
}
if(hasStringFormat)
return AddWarningMessageTrivia(node, "Please replace String.Format with string interpolation format.");
return node;
}
}
#>
Résultat de la transformation
#error variable "Customers" doesn't match code standard rules
IEnumerable<Customer> Customers;
// ...
#warning Please replace String.Format with string interpolation format.
Console.Out.WriteLine(i18(String.Format("SecondDemo: {0}", "3")));
Ou comme outil automatique de localisation d'une application, c'est-à-dire trouvez toutes les chaînes dans les classes et remplacez-les par l'utilisation des ressources appropriées.
Modèle ResourceReplacer.t4
<#@ include file="AopCsharp.ttinclude" #>
<#
Dictionary<string, string> options = ExtraTagAsDictionary();
_resources = LoadResources(options["ResourceFile"]);
_resourceClass = options["ResourceClass"];
var syntaxNode = RewriteLeafStatementNodes(SyntaxNode, OnStatementNodeVisit);
#>
<#= syntaxNode.ToFullString() #>
<#+
private SyntaxNode OnStatementNodeVisit(StatementSyntax node)
{
if(!node.DescendantNodes().OfType<InvocationExpressionSyntax>().Any(w => (w.Expression is IdentifierNameSyntax) && ((IdentifierNameSyntax)w.Expression).Identifier.ToString() == "i18" ))
return node;
var errorMsgs = new System.Text.StringBuilder();
SyntaxNode syntaxNode = RewriteNodes<InvocationExpressionSyntax>(node, (n) => OnInvocationExpressionVisit(n, errorMsgs));
if(errorMsgs.Length > 0)
return AddErrorMessageTrivia(syntaxNode, errorMsgs.ToString());
return syntaxNode;
}
private SyntaxNode OnInvocationExpressionVisit(InvocationExpressionSyntax node, System.Text.StringBuilder errorMsgs)
{
if(!(node.Expression is IdentifierNameSyntax && ((IdentifierNameSyntax)node.Expression).Identifier.ToString() == "i18" ))
return node;
ArgumentSyntax arg = node.ArgumentList.Arguments.Single(); // We know that i18 method accepts only one argument. Keep in mind that it is just a demo and in real life you could be more inventive
var expr = arg.Expression;
if(!(expr is LiteralExpressionSyntax || expr is InterpolatedStringExpressionSyntax))
{
errorMsgs.AppendLine($"Argument for i18 method must be either string literal or interpolated string, but instead got {arg.Expression.GetType().ToString()}");
return node;
}
string s = expr.ToString();
if(s.StartsWith("$"))
{
(string format, List<ExpressionSyntax> expressions) = ConvertInterpolatedStringToFormat((InterpolatedStringExpressionSyntax)expr);
ExpressionSyntax stringNode = ReplaceStringWithResource("\"" + format + "\"", errorMsgs);
if(stringNode != null)
{
var memberAccess = CreateMemberAccess("String", "Format");
var arguments = new List<ArgumentSyntax>();
arguments.Add(SyntaxFactory.Argument(stringNode));
expressions.ForEach(item => arguments.Add(SyntaxFactory.Argument(item)));
var argumentList = SyntaxFactory.SeparatedList(arguments);
return SyntaxFactory.InvocationExpression(memberAccess, SyntaxFactory.ArgumentList(argumentList));
}
}
else
{
SyntaxNode stringNode = ReplaceStringWithResource(s, errorMsgs);
if(stringNode != null)
return stringNode;
}
return node;
}
private ExpressionSyntax ReplaceStringWithResource(string s, System.Text.StringBuilder errorMsgs)
{
Match m = System.Text.RegularExpressions.Regex.Match(s, "^\"(\\s*)(.*?)(\\s*)\"$");
if(!m.Success)
{
errorMsgs.AppendLine($"String doesn't match search criteria");
return null;
}
if(!_resources.TryGetValue(m.Groups[2].Value, out string resourceName))
{
errorMsgs.AppendLine($"Cannot find resource for a string {s}, please add it to resources");
return null;
}
string csharpName = Regex.Replace(resourceName, "[^A-Za-z0-9]", "_");
ExpressionSyntax stringNode = CreateMemberAccess(_resourceClass, csharpName);
if(!String.IsNullOrEmpty(m.Groups[1].Value) || !String.IsNullOrEmpty(m.Groups[3].Value))
{
if(!String.IsNullOrEmpty(m.Groups[1].Value))
{
stringNode = SyntaxFactory.BinaryExpression(SyntaxKind.AddExpression,
CreateStringLiteral(m.Groups[1].Value),
stringNode);
}
if(!String.IsNullOrEmpty(m.Groups[3].Value))
{
stringNode = SyntaxFactory.BinaryExpression(SyntaxKind.AddExpression,
stringNode,
CreateStringLiteral(m.Groups[3].Value));
}
stringNode = SyntaxFactory.ParenthesizedExpression(stringNode);
}
return stringNode;
}
private string _resourceClass;
private Dictionary<string,string> _resources;
#>
La source
Console.Out.WriteLine(i18("SecondDemo: i18"));
// ...
Console.Out.WriteLine(i18($"First Name {customer.FirstName} Last Name {customer.LastName}"));
Console.Out.WriteLine("SecondDemo: 2 " + i18("First Name ") + customer.FirstName + i18(" Last Name ") + customer.LastName);
// ...
Console.Out.WriteLine(i18(String.Format("SecondDemo: {0}", "3")));
// ...
protected static string i18(string s) => s;
Dans le fichier de ressources Demo.resx, par exemple, nous avons créé les lignes suivantes
<data name="First Last Names Formatted" xml:space="preserve">
<value>First Name {0} Last Name {1}</value>
</data>
<data name="First Name" xml:space="preserve">
<value>First Name</value>
</data>
<data name="Last Name" xml:space="preserve">
<value>Last Name</value>
</data>
et le code généré automatiquement du fichier Demo.Designer.cs
public class Demo
{
// ...
public static string First_Last_Names_Formatted
{
get
{
return ResourceManager.GetString("First Last Names Formatted", resourceCulture);
}
}
public static string First_Name
{
get
{
return ResourceManager.GetString("First Name", resourceCulture);
}
}
public static string Last_Name
{
get
{
return ResourceManager.GetString("Last Name", resourceCulture);
}
}
}
Résultat de la transformation (notez que la chaîne interpolée a été remplacée par String.Format et que la ressource "Prénom {0} Nom {1}" a été utilisée). Pour les lignes qui n'existent pas dans le fichier de ressources ou qui ne correspondent pas à notre format, un message d'erreur est ajouté
//#error Cannot find resource for a string "SecondDemo: i18", please add it to resources
Console.Out.WriteLine(i18("SecondDemo: i18"));
// ...
Console.Out.WriteLine(String.Format(Demo.First_Last_Names_Formatted, customer.FirstName, customer.LastName));
Console.Out.WriteLine("SecondDemo: 2 " + (Demo.First_Name + " ") + customer.FirstName + (" " + Demo.Last_Name + " ") + customer.LastName);
// ...
//#error Argument for i18 method must be either string literal or interpolated string, but instead got Microsoft.CodeAnalysis.CSharp.Syntax.InvocationExpressionSyntax
Console.Out.WriteLine(i18(String.Format("SecondDemo: {0}", "3")));
De plus, l'outil de transformation vous permet de travailler non seulement avec des fichiers C #, mais également avec n'importe quel type de fichier (bien sûr, avec certaines restrictions). Si vous avez un analyseur qui peut créer un AST pour votre langage, vous pouvez remplacer Roslyn par cet analyseur, modifier l'implémentation du gestionnaire de code et cela fonctionnera. Malheureusement, il existe un nombre très limité de bibliothèques avec des fonctionnalités proches de Roslyn et leur utilisation nécessite beaucoup plus d'efforts. En plus de C #, nous utilisons des transformations pour les projets JavaScript et TypeScript, mais certainement pas de manière aussi complète que pour C #.
Encore une fois, je répète que l'exemple de code et les modèles sont donnés à titre d'illustration des possibilités d'une telle approche et, comme on dit, le ciel est la limite.
Merci pour votre temps.
La partie principale de cet article a été écrite il y a quelques années, mais malheureusement, pour certaines raisons, il n'a été possible de le publier que maintenant.
Notre outil original a été développé sur le .Net Framework, mais nous avons commencé à travailler sur une version open source simplifiée sous la licence MIT pour .Net Core. Pour le moment, le résultat est entièrement fonctionnel et prêt à 90%, il y a des améliorations mineures, la coiffure du code, la création de documentation et d'exemples, mais sans tout cela, il sera difficile d'entrer dans le projet, l'idée elle-même sera compromise et DX sera négatif.
La personne qui a travaillé à sa création n'a pas pu la terminer avant de déménager dans une autre entreprise, donc avant d'allouer des ressources pour continuer le travail, nous voulons regarder la réaction de la communauté, car nous comprenons que ce qui convient dans notre cas n'est pas forcément en demande et est tout à fait possible, que ce créneau est occupé par un outil ou une approche alternative du développement.
L'idée même de l'outil est très simple et le développeur a passé un total d'environ un mois sur la mise en œuvre d'une version réalisable, donc je pense qu'un programmeur avec de bonnes qualifications et une bonne expérience avec Roslyn sera en mesure de créer sa propre version spécifique en quelques jours. Pour le moment, la taille du code source du projet n'est que d'environ 150 Ko, exemples et modèles compris.
Je serais heureux de recevoir des critiques constructives (les critiques non constructives ne me dérangeront pas non plus, alors n'hésitez pas).
Merci à Phil Rangin (fillpackart) pour la motivation lors de la rédaction de l'article. Règles de la chaîne "We Are Doomed"!