Programación orientada a aspectos (AOP) a través del código fuente





La programación orientada a aspectos es un concepto muy atractivo para simplificar su base de código, generar código limpio y minimizar los errores de copiar y pegar.



Hoy, en la mayoría de los casos, los aspectos se implementan a nivel de código de bytes, es decir, después de la compilación, alguna herramienta "entrelaza" un código de bytes adicional con el apoyo de la lógica requerida.



Nuestro enfoque (así como el enfoque de algunas otras herramientas) es modificar el código fuente para implementar la lógica de aspecto. Con la transición a la tecnología Roslyn, es muy fácil lograr esto y el resultado brinda ciertas ventajas sobre la modificación del código de bytes en sí.



Si está interesado en los detalles, consulte cat.



Puede pensar que la programación orientada a aspectos no se trata de usted y no le preocupa particularmente, solo un montón de palabras incomprensibles, pero de hecho es mucho más fácil de lo que parece, se trata de los problemas del desarrollo de productos reales y si está involucrado en el desarrollo industrial, definitivamente puede obtener beneficiarse de su uso.



Especialmente en proyectos medianos-grandes de nivel corporativo, donde se formalizan los requisitos para la funcionalidad de los productos. Por ejemplo, puede haber un requisito: al establecer la marca de configuración, registre todos los parámetros de entrada para todos los métodos públicos. O que todos los métodos del proyecto tengan un sistema de notificación que envíe un mensaje cuando se exceda un cierto umbral del tiempo de ejecución de este método.



¿Cómo se hace esto sin AOP? O se martilla y se hace solo para las partes más importantes, o cuando se escriben nuevos métodos, se debe copiar y pegar código similar de los métodos vecinos, con todos los métodos que lo acompañan.



Cuando se utiliza AOP, se escribe un consejo una vez que se aplica al proyecto y se realiza el trabajo. Cuando necesite actualizar un poco la lógica, volverá a actualizar el consejo una vez y se aplicará en la siguiente compilación. Sin AOP, son 100,500 actualizaciones en todo el código del proyecto.



El plus es que tu código deja de parecer una persona que ha tenido viruela, debido a que está plagado de tal funcionalidad y al leer el código parece un ruido molesto.



Después de introducir AOP en su proyecto, comienza a implementar cosas que nunca soñó sin él, porque parecían una ventaja relativamente pequeña, a un costo elevado. Con AOP, todo es exactamente lo contrario, costos relativamente bajos y grandes beneficios (para un nivel similar de costo de sus esfuerzos).



Mi sensación es que la programación orientada a aspectos es significativamente menos popular en el ecosistema .Net en comparación con el ecosistema Java. Creo que la razón principal es la falta de herramientas gratuitas y de código abierto que sean comparables a la funcionalidad y calidad de Java.



PostSharp ofrece una funcionalidad y conveniencia similares, pero no muchos están dispuestos a pagar cientos de dólares para usarlo en sus proyectos, y la versión comunitaria tiene capacidades muy limitadas. Por supuesto que existen alternativas, pero lamentablemente no han alcanzado el nivel de PostSharp.



Puede comparar las capacidades de las herramientas (debe tenerse en cuenta que la comparación fue realizada por el propietario de PostSharp, pero da una idea).



Nuestro camino hacia la programación orientada a aspectos



Somos una pequeña empresa de consultoría (12 personas) y el resultado final de nuestro trabajo es el código fuente. Aquellos. nos pagan por crear código fuente, código de calidad. Solo trabajamos en una industria y muchos de nuestros proyectos tienen requisitos muy similares y, como resultado, el código fuente también es bastante similar entre estos proyectos.



Y dado que tenemos recursos limitados, para nosotros, una de las tareas más importantes es la capacidad de reutilizar el código y utilizar herramientas que salvan al desarrollador de las tareas rutinarias.



Para lograr esto, una de las formas es que hacemos un uso intensivo de las capacidades de generación automática de código y también creamos varios complementos y analizadores personalizados para Visual Studio específicos para nuestros proyectos y tareas. Esto hizo posible aumentar significativamente la productividad de los programadores, manteniendo una alta calidad del código (incluso se podría decir que la calidad ha aumentado).



El siguiente paso lógico fue la idea de implementar el uso de programación orientada a aspectos. Probamos varios enfoques y herramientas, pero el resultado estuvo lejos de nuestras expectativas. Esto coincidió en el tiempo con el lanzamiento de la tecnología Roslyn, y en un momento determinado tuvimos la idea de combinar las capacidades de generación automática de código y Roslyn.



En solo un par de semanas, se creó un prototipo del instrumento y, según nuestros sentimientos, este enfoque parecía más prometedor. Luego de varias iteraciones en la forma de usar y actualizar esta herramienta, podemos decir que nuestras expectativas se cumplieron e incluso más de lo que esperábamos. Hemos desarrollado una biblioteca de plantillas útiles y utilizamos este enfoque en la mayoría de nuestros proyectos, y algunos de nuestros clientes también lo utilizan e incluso solicitan el desarrollo de plantillas para sus necesidades.



Desafortunadamente, nuestra herramienta aún está lejos de ser ideal, por lo que me gustaría dividir la descripción en dos partes, la primera es cómo veo la implementación de esta funcionalidad en un mundo ideal y la segunda es cómo se hace aquí.



Antes de pasar a los detalles, me gustaría hacer una pequeña explicación: todos los ejemplos de este artículo se han simplificado a un nivel que le permite mostrar la idea, sin estar sobrecargado con detalles irrelevantes.



Cómo se haría en un mundo perfecto



Después de varios años de usar nuestra herramienta, tengo una visión de cómo me gustaría que esto funcionara si viviéramos en un mundo ideal.



En mi visión de un mundo ideal, las especificaciones del lenguaje permiten el uso de transformaciones de código fuente, y hay compilador y soporte IDE.



La idea se inspiró en la inclusión del modificador "parcial" en la especificación del lenguaje C #. Este concepto bastante simple (la capacidad de definir una clase, estructura o interfaz en varios archivos) ha mejorado y simplificado drásticamente el soporte de herramientas para la generación automática de código fuente. Aquellos. es una especie de división horizontal del código fuente de una clase entre varios archivos. Para aquellos que no conocen el lenguaje C #, un pequeño ejemplo.



Supongamos que tenemos un formulario simple descrito en el archivo Example1.aspx

<%@ Page Language="C#" AutoEventWireup="True" %>
// . . .
<asp:Button id="btnSubmit"
           Text="Submit"
           OnClick=" btnSubmit_Click" 
           runat="server"/>
// . . .


Y lógica personalizada (por ejemplo, cambiar el color de un botón a rojo cuando se hace clic en él) en el archivo 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 presencia en el idioma de las capacidades proporcionadas por "parcial" permite al kit de herramientas analizar el archivo Example1.aspx y generar automáticamente el archivo Example1.aspx.designer.cs.



public partial class ExamplePage1 : System.Web.UI.Page
{
  protected global::System.Web.UI.WebControls.Button btnSubmit;
}


Aquellos. tenemos la capacidad de almacenar una parte del código para la clase ExamplePage1 en un archivo por el programador actualizable (Example1.aspx.cs) y una parte en el archivo Example1.aspx.designer.cs por el kit de herramientas generado automáticamente. Para el compilador, al final, parece una clase general



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;
  }
}


Usando el ejemplo con la definición de herencia de la interfaz IMyInterface, puede ver que el resultado final es una combinación de definiciones de clases de diferentes archivos.



Si no tenemos una funcionalidad como parcial y el compilador requiere almacenar todo el código de la clase en un solo archivo, entonces podemos asumir los inconvenientes y los gestos adicionales necesarios para soportar la generación automática.



En consecuencia, mi idea es incluir dos modificadores adicionales en la especificación del lenguaje, lo que facilitará la integración de aspectos en el código fuente.



El primer modificador es original y lo agregamos a la definición de clase que debería poder transformarse.



El segundo es procesado y simboliza que esta es la definición de clase final que fue obtenida por la herramienta de transformación de fuente y que debe ser aceptada por el compilador para generar el código de bytes.



La secuencia es algo como esto



  1. El usuario trabaja con el código fuente de la clase que contiene el modificador original en el archivo .cs (por ejemplo, Example1.cs)
  2. Al compilar, el compilador comprueba la exactitud del código fuente y, si la clase se compila correctamente, comprueba la presencia del original.
  3. Si el original está presente, el compilador entrega el código fuente de este archivo al proceso de transformación (que es una caja negra para el compilador).
  4. .processed.cs .processed.cs.map ( .cs .processed.cs, IDE)
  5. .processed.cs ( Example1.processed.cs) .
  6. ,



    a. original processed

    b. .cs .processed.cs
  7. , .processed.cs .


Aquellos. al agregar estos dos modificadores, pudimos organizar el soporte para las herramientas de transformación de código fuente a nivel de lenguaje, al igual que lo parcial hizo posible simplificar el soporte para la generación de código fuente. Aquellos. parial es la división de código horizontal, original / procesado es vertical.



Como yo lo veo, implementar el soporte original / procesado en el compilador es una semana de trabajo para dos pasantes en Microsoft (una broma, por supuesto, pero no está lejos de la verdad). En general, no existen dificultades fundamentales en esta tarea, desde el punto de vista del compilador es la manipulación de archivos y la invocación de procesos.



Se agregó una nueva característica en .NET 5: generadores de código fuenteque ya le permite generar nuevos archivos de código fuente durante la compilación y este es un movimiento en la dirección correcta. Desafortunadamente, solo le permite generar un nuevo código fuente, pero no modificar el existente. Entonces todavía estamos esperando.



Un ejemplo de un proceso similar. El usuario crea el archivo 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;
  }	
}


Se ejecuta para la compilación, si todo está bien y el compilador ve el modificador original, entonces le da el código fuente al proceso de transformación, que genera el archivo Example2.processed.cs (en el caso más simple, puede ser una copia exacta de Example2.cs con el original reemplazado por procesado) ...



En nuestro caso, asumiremos que el proceso de transformación agregó un aspecto de registro y el resultado se ve así:

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
  }
}


El siguiente paso es verificar las firmas. Las firmas _main_ son idénticas y satisfacen la condición de que las definiciones en original y procesada deben ser exactamente iguales.



En este ejemplo, agregué especialmente una oración más pequeña, este es el modificador procesado para métodos, propiedades y campos.



Marca métodos, propiedades y campos como disponibles solo para las clases con el modificador procesado y que se ignoran al comparar firmas. Esto se hace para la conveniencia de los desarrolladores de aspectos y le permite mover la lógica general a métodos separados para no crear redundancia de código innecesaria.



El compilador compiló este código y si todo está bien, tomó el código de bytes para continuar con el proceso.



Está claro que en este ejemplo hay cierta simplificación y en realidad la lógica puede ser más complicada (por ejemplo, cuando incluimos tanto original como parcial para una clase), pero esto no es una complejidad insuperable.



Funcionalidad IDE básica en un mundo perfecto



La compatibilidad para trabajar con el código fuente de los archivos .processed.cs en el IDE se encuentra principalmente en la navegación correcta entre las clases originales / procesadas y las transiciones durante la depuración paso a paso.



La segunda característica más importante del IDE (desde mi punto de vista) es ayudar a leer el código de las clases procesadas. Una clase procesada puede contener muchas piezas de código que han sido agregadas por varios aspectos. La implementación de una visualización similar al concepto de capas en un editor gráfico nos parece la opción más conveniente para lograr este objetivo. Nuestro plugin actual implementa algo similar y la respuesta de sus usuarios es bastante positiva.



Otra característica que ayudaría a introducir AOP en la vida cotidiana es la funcionalidad de refactorización. un usuario, resaltando una parte del código, podría decir "Extraer a plantilla AOP" y el IDE creó los archivos correctos, generó el código inicial y después de analizar el código del proyecto, sugirió candidatos para usar una plantilla de otras clases.



Bueno, la guinda del pastel sería el soporte para escribir plantillas de aspecto, por ejemplo, aplicando interactivamente un aspecto a una clase / método de su elección para que pueda evaluar el resultado final sobre la marcha, sin un ciclo de compilación explícito de su parte.



Estoy seguro de que si los creadores del resharper se hacen cargo del negocio, la magia está garantizada.



Escribir código de aspecto en un mundo perfecto



Parafraseando a TRIZ, la escritura de código ideal para la implementación de aspectos es la ausencia de escritura de código adicional que existe solo para apoyar los procesos de instrumentación.



En un mundo ideal, nos gustaría escribir código para el aspecto en sí, sin el esfuerzo de escribir lógica auxiliar para lograr este objetivo. Y este código sería una parte integral del proyecto en sí.



El segundo deseo es la capacidad de tener plug & play interactivo, es decir, habiendo escrito una plantilla, no necesitaríamos tomar pasos adicionales para que se use para la transformación. No fue necesario volver a compilar la herramienta, detectar sus errores, etc. Y también configurar opciones en proyectos para post-compilación.



Habiendo creado una plantilla y escribiendo un par de líneas, vería inmediatamente el resultado y si contiene errores, su detección y depuración se integraría en el proceso de aplicación de la plantilla, y no sería una parte separada que requiera un esfuerzo adicional por parte del programador.



Bueno, para que la sintaxis de la plantilla sea lo más cercana posible a la sintaxis del lenguaje C #, idealmente un complemento menor, más algunas palabras clave y marcadores de posición.



Nuestra implementación actual



Desafortunadamente, no vivimos en un mundo ideal, por lo que tenemos que reinventar las bicicletas y montarlas.



Inyección, compilación y depuración de código



Nuestro modelo actual es crear dos copias del proyecto. Uno es el original, con el que trabaja el programador, el segundo es el transformado, que se usa para la compilación y ejecución.



El escenario es algo como esto



  • , , ..
  • , , , .
  • , , , WPF , ..


Para la depuración, se lanza la segunda copia del IDE, se abre una copia formada por país del proyecto y funciona con la copia a la que se aplicó la transformación.



El proceso requiere cierta disciplina, pero de vez en cuando se convierte en un hábito y, en ciertos casos, este enfoque tiene algunas ventajas (por ejemplo, una compilación se puede lanzar e implementar en un servidor remoto, en lugar de trabajar con una máquina local). Además, la ayuda del complemento en VisualStudio facilita el proceso.



IDE



Usamos un complemento que se adapta a nuestras tareas y procesos específicos y el soporte para la implementación del código fuente es una parte bastante pequeña de sus capacidades.



Por ejemplo, la funcionalidad para mostrar capas, en un estilo similar a un editor gráfico, permite, por ejemplo, ocultar / mostrar capas de comentarios, por alcance (por ejemplo, para que solo los métodos públicos sean visibles), regiones. El código incrustado está rodeado de comentarios de un formato especial y también se pueden ocultar como una capa separada.



Otra posibilidad es mostrar una diferencia entre el archivo original y el transformado. dado que el IDE conoce la ubicación relativa de la copia del archivo en el proyecto, puede mostrar las diferencias entre los archivos originales y los generados por el país.



Además, el complemento advierte cuando se intenta realizar cambios en la copia generada por el país (para no perderlos durante la posterior transformación)



Configuración



Una tarea separada es establecer reglas de transformación, es decir a qué clases y métodos aplicaremos la transformación.



Usamos varios niveles.



El primer nivel es el archivo de configuración de nivel superior. Podemos establecer reglas dependiendo de la ruta en el sistema de archivos, patrones en el nombre de archivos, clases o métodos, alcances de clases, métodos o propiedades.



El segundo nivel es una indicación de la aplicación de las reglas de transformación a nivel de atributos de clases, métodos o campos.



El tercero a nivel del bloque de código y el cuarto es una indicación explícita para incluir los resultados de la transformación de la plantilla en un lugar específico del código fuente.



Plantillas



Históricamente, a los efectos de la generación automática, utilizamos plantillas en formato T4, por lo que era bastante lógico utilizar el mismo enfoque como plantillas para la transformación. Las plantillas T4 incluyen la capacidad de ejecutar código C # arbitrario, tienen una sobrecarga mínima y una buena expresividad.



Para aquellos que nunca han trabajado con T4, el análogo más simple sería presentar el formato ASPX, que en lugar de HTML genera código fuente en C # y no se ejecuta en IIS, sino como una utilidad separada con la salida del resultado a la consola (oa un archivo).



Ejemplos de



Para entender cómo funciona esto en la realidad, lo más sencillo es demostrar el código antes y después de la transformación y el código fuente de las plantillas que se utiliza durante la transformación. Demostraré las opciones más simples, pero el potencial solo está limitado por tu imaginación.



Ejemplo de código fuente antes de la transformación
// ##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}";
            }
        }
    }
}




Versión completa del código fuente después de la transformación
//------------------------------------------------------------------------------
// <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







Explicaciones y código de plantilla



Plantilla de comentario automático



// ##aspect=AutoComment


Si en el código fuente encontramos un comentario en un formato especial, entonces ejecutamos la plantilla especificada (en este caso, es AutoComment) e insertamos el resultado de la transformación en lugar de este comentario. En este ejemplo, tiene sentido insertar automáticamente un descargo de responsabilidad especial que advertirá al programador que el código en este archivo es el resultado de la transformación y no tiene sentido modificar este archivo directamente.



Código de plantilla 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>
//------------------------------------------------------------------------------


Las variables FileName, FileSha256, User, MachineName y Now se exportan a la plantilla desde el proceso de transformación.



Resultado de la transformación



//------------------------------------------------------------------------------
// <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 siguiente transformación se especifica como un atributo de la clase



[AopTemplate ("ClassLevelTemplateForMethods", NameFilter = "First")]



Este atributo indica que la plantilla debe aplicarse a todos los métodos de clase que contienen la palabra "First". El parámetro NameFilter es un patrón de expresión regular que se usa para determinar qué métodos incluir en la transformación.



Código de plantilla ClassLevelTemplateForMethods.t4



<#@ include file="AopCsharp.ttinclude" #>

// class level template
<#= MethodStart() #><#= MethodBody() #><#= MethodEnd() #>


Este es el ejemplo más simple que agrega un comentario // class level templateantes del código del método



Resultado de la transformación



// 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,
      };
}


Las siguientes transformaciones se especifican como atributos de método para demostrar múltiples transformaciones aplicadas al mismo método. Plantilla 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() #>


Plantilla 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() #>


MethodFinallyDemo.t4 Plantilla

<#@ include file="AopCsharp.ttinclude" #>

<#= MethodStart() #>
try
{
<#= MethodBody() #>
} 
finally 
{
	// whatever logic you need to include for a method
}

<#= MethodEnd() #>


Resultado de transformaciones

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 siguiente transformación se da para un bloque limitado a una construcción de uso



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


SecondDemoUsing.t4 Plantilla

<#@ include file="AopCsharp.ttinclude" #>

// second demo <#= ExtraTag #>

<#= StatementBody() #>


ExtraTag es una cadena que se pasa como parámetro. Esto puede resultar útil para los genéricos que pueden tener un comportamiento ligeramente diferente según los parámetros de entrada.



Resultado de la transformación



{
  // 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 siguiente transformación está especificada por los atributos de la clase NotifyPropertyChanged . Este es un ejemplo clásico, que junto con el ejemplo de registro se da en la mayoría de los ejemplos de programación orientada a aspectos.



[AopTemplate("NotifyPropertyChangedClass", Action = AopTemplaceAction.Classes)]

[AopTemplate("NotifyPropertyChanged", Action = AopTemplaceAction.Properties)]








La plantilla NotifyPropertyChangedClass.t4 se aplica al código de la clase
<#@ 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


Plantilla NotifyPropertyChanged.t4 aplicada a propiedades de clase
<#@ 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();
			}
		}
	}


Código original de clase y propiedades.

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

// ...
}


Resultado de la transformación

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 ejemplo de una plantilla para almacenar en caché los resultados de las propiedades, se especifica mediante los



[AopTemplate("CacheProperty", extraTag: "{ \"CacheKey\": \"name_of_cache_key\", \"ExpiresInMinutes\": 10 }")]



parámetros de la plantilla se especifican como atributo JSON. Si no hay parámetros explícitos, se utilizan los parámetros predeterminados.



Plantilla 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() #>			
		}
		<# } #>

	}


Fuente

[AopTemplate("CacheProperty", extraTag: "{ \"CacheKey\": \"name_of_cache_key\", \"ExpiresInMinutes\": 10 }")]
public string FullName
{
    get
    {
        return $"{FirstName} {LastName}";
    }
}


Resultado de la transformación para 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}";
        }
    }
}


La próxima llamada a la plantilla nuevamente desde el comentario.

// ##aspect="FullNameComment" extra data here


Plantilla FullNameComment.t4

<#@ include file="AopCsharp.ttinclude" #>

// FullNameComment <#= PropertyNode.Identifier #>


Muy similar a la plantilla AutoComment.t4, pero aquí demostramos el uso de PropertyNode. Además, los datos de "datos adicionales aquí" están disponibles para la plantilla FullNameComment.t4 a través del parámetro ExtraTag (pero en este ejemplo no los usamos, por lo que simplemente se ignoran)



Resultado de la transformación

// FullNameComment FullName


La siguiente transformación en el archivo que se especifica mediante el atributo



[AopTemplate("NotifyPropertyChanged", Action = AopTemplaceAction.Properties)]



AND es idéntica a la de la clase Person. El código fuente de la plantilla NotifyPropertyChanged.t4 ya se ha incluido anteriormente.



Resultado de la transformación

public class Customer : Person
{
    private double _creditScore;
    public double CreditScore
    {
        get
        {
            return _creditScore;
        }

        set
        {
            if (_creditScore != value)
            {
                _creditScore = value;
                NotifyPropertyChanged();
            }
        }
    }
}


Parte final



Aunque este artículo se centra en la programación orientada a aspectos, la técnica de transformación del código fuente es universal y, en principio, se puede utilizar para tareas que no están relacionadas con AOP.



Por ejemplo, se puede usar para inyección de dependencia, es decir cambiamos el código de creación de recursos dependiendo de los parámetros de construcción.



Plantilla 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;
	}

#>




En el código fuente (aquí se usa la característica de variable dinámica, que permite asignarlos a cualquier tipo), es decir para la expresividad, se nos ocurrió una nueva palabra clave.

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;


Durante la transformación, se utiliza la comparación GetGlobalSetting ("env") == "test" y, dependiendo de esta condición, se inyectará nuevo DataService () o nuevo MockDataService ().



Resultado de la transformación


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"];
}
// ...


O puede usar esta herramienta como un análisis estático "pobre" (pero es mucho, mucho más correcto implementar analizadores usando la funcionalidad nativa de Roslyn), analizamos el código para nuestras reglas y lo insertamos en el código fuente.



#error our error message here



Eso conducirá a un error en tiempo de compilación.



#warning our warning message here



Que servirá como advertencia en el IDE o al compilar.



Plantilla 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;
		}
	}
#>




Resultado de la transformación

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


O como una herramienta automática para localizar una aplicación, p. Ej. encuentre todas las cadenas en las clases y reemplácelas con el uso de los recursos apropiados.



Plantilla 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;
#>




Fuente


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;


En el archivo de recursos Demo.resx, hemos creado las siguientes líneas para un ejemplo

<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>


y el código generado automáticamente del archivo 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);
        }
    }
}


Resultado de la transformación (tenga en cuenta que la cadena interpolada se reemplazó por String.Format y se utilizó el recurso "Nombre {0} Apellido {1}"). Para las líneas que no existen en el archivo de recursos o que no coinciden con nuestro formato, se agrega un mensaje de error

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


Además, la herramienta de transformación te permite trabajar no solo con archivos C #, sino también con cualquier tipo de archivo (por supuesto, con ciertas restricciones). Si tiene un analizador que puede crear un AST para su idioma, puede reemplazar Roslyn con este analizador, modificar la implementación del controlador de código y funcionará. Desafortunadamente, la cantidad de bibliotecas con funcionalidades cercanas a Roslyn es muy limitada y su uso requiere mucho más esfuerzo. Además de C #, usamos transformaciones para proyectos de JavaScript y TypeScript, pero ciertamente no de forma tan completa como para C #.



Una vez más, repito que el código del ejemplo y las plantillas se dan como ilustración de las posibilidades de tal enfoque y, como dicen, el cielo es el límite.



Gracias por tu tiempo.



La parte principal de este artículo fue escrita hace un par de años, pero desafortunadamente, debido a ciertas razones, solo fue posible publicarlo ahora.



Nuestra herramienta original se desarrolló en .Net Framework, pero comenzamos a trabajar en una versión simplificada de código abierto bajo la licencia MIT para .Net Core. De momento, el resultado es completamente funcional y 90% listo, hay pequeñas mejoras, peinado del código, creación de documentación y ejemplos, pero sin todo esto será difícil entrar al proyecto, la idea en sí se verá comprometida y DX será negativo.



La persona que trabajó en su creación no pudo terminarlo antes de mudarse a otra empresa, por lo que antes de destinar recursos para continuar el trabajo, queremos mirar la reacción de la comunidad, ya que entendemos que lo que es adecuado en nuestro caso no necesariamente tiene demanda y es bastante posible, que este nicho se está llenando con alguna herramienta o enfoque alternativo para el desarrollo.



La idea misma de la herramienta es muy simple y el desarrollador dedicó un total de aproximadamente un mes a la implementación de una versión viable, por lo que creo que un programador con buenas calificaciones y experiencia con Roslyn podrá crear su propia versión específica en unos pocos días. Por el momento, el tamaño del código fuente del proyecto es de solo 150 KB, incluidos ejemplos y plantillas.



Me encantaría recibir críticas constructivas (las críticas no constructivas tampoco me molestarán, así que no lo dudes).



Gracias a Phil Rangin (fillpackart) para motivar la redacción del artículo. Reglas del canal "We Are Doomed"!



All Articles