Programação Orientada a Aspectos (AOP) via código-fonte





A programação orientada a aspectos é um conceito muito atraente para simplificar sua base de código, gerando código limpo e minimizando erros de copiar e colar.



Hoje, na maioria dos casos, os aspectos são implementados no nível do bytecode, ou seja, após a compilação, alguma ferramenta "entrelaça" um código de byte adicional com o suporte da lógica necessária.



Nossa abordagem (bem como a abordagem de algumas outras ferramentas) é modificar o código-fonte para implementar a lógica de aspecto. Com a transição para a tecnologia Roslyn, é muito fácil conseguir isso e o resultado oferece certas vantagens em relação à modificação do código de bytes em si.



Se você estiver interessado nos detalhes, consulte cat.



Você pode pensar que a programação orientada a aspectos não é sobre você e não o preocupa particularmente, apenas um monte de palavras incompreensíveis, mas na verdade é muito mais fácil do que parece, trata-se dos problemas de desenvolvimento de produto real e se você estiver envolvido no desenvolvimento industrial, então você definitivamente pode obter tirar proveito de usá-lo.



Principalmente em projetos de médio-grande porte de nível corporativo, onde os requisitos para a funcionalidade dos produtos são formalizados. Por exemplo, pode haver um requisito - ao definir o sinalizador de configuração, registre todos os parâmetros de entrada para todos os métodos públicos. Ou, para todos os métodos de projeto, ter um sistema de notificação que enviará uma mensagem quando um determinado limite do tempo de execução deste método for excedido.



Como isso é feito sem AOP? Ou é martelado e feito apenas para as partes mais importantes, ou ao escrever novos métodos, copia-se o código semelhante de métodos vizinhos, com todos os atendentes de um método semelhante.



Ao usar o AOP, um conselho é escrito uma vez que é aplicado ao projeto e o trabalho é concluído. Quando precisar atualizar um pouco a lógica, você atualizará novamente o conselho uma vez e ele será aplicado na próxima construção. Sem o AOP, são 100.500 atualizações em todo o código do projeto.



A vantagem é que seu código deixa de se parecer com uma pessoa que teve varíola, porque está repleto de tais funcionalidades e, ao ler o código, parece um ruído irritante.



Depois de introduzir o AOP em seu projeto, você começa a implementar coisas que nunca sonhou sem ele, porque parecia uma vantagem relativamente pequena, a um custo alto. Com AOP, tudo é exatamente o oposto, custos relativamente baixos e grandes benefícios (por um nível semelhante de custo de seus esforços).



Minha impressão é que a programação orientada a aspectos é significativamente menos popular no ecossistema .Net em comparação com o ecossistema Java. Acho que o principal motivo é a falta de ferramentas gratuitas e de código aberto que sejam comparáveis ​​à funcionalidade e qualidade do Java.



PostSharp oferece funcionalidade e conveniência semelhantes, mas poucos estão dispostos a pagar centenas de dólares para usá-lo em seus projetos, e a versão da comunidade é muito limitada em seus recursos. Claro que existem alternativas, mas infelizmente elas não atingiram o nível de PostSharp.



Você pode comparar as capacidades das ferramentas (deve-se ter em mente que a comparação foi feita pelo proprietário do PostSharp, mas dá uma ideia).



Nosso caminho para a programação orientada a aspectos



Somos uma pequena empresa de consultoria (12 pessoas) e o resultado final do nosso trabalho é o código fonte. Essa. somos pagos para criar código-fonte, código de qualidade. Trabalhamos apenas em um setor e muitos de nossos projetos têm requisitos muito semelhantes e, como resultado, o código-fonte também é bastante semelhante entre esses projetos.



E como temos recursos limitados, para nós, uma das tarefas mais importantes é a capacidade de reutilizar código e usar ferramentas que salvam o desenvolvedor de tarefas rotineiras.



Para conseguir isso, uma das maneiras é fazer uso extensivo dos recursos de geração automática de código e também criar vários plug-ins e analisadores personalizados para Visual Studio específicos para nossos projetos e tarefas. Isso permitiu aumentar significativamente a produtividade dos programadores, mantendo a alta qualidade do código (pode-se até dizer que a qualidade aumentou).



O próximo passo lógico foi a ideia de implementar o uso de programação orientada a aspectos. Tentamos várias abordagens e ferramentas, mas o resultado ficou longe de nossas expectativas. Isso coincidiu no tempo com o lançamento da tecnologia Roslyn, e em um determinado momento tivemos a ideia de combinar as capacidades de geração automática de código e Roslyn.



Em apenas algumas semanas, um protótipo do instrumento foi criado e, de acordo com nossos sentimentos, essa abordagem parecia mais promissora. Depois de várias iterações no uso e atualização desta ferramenta, podemos afirmar que nossas expectativas foram atendidas e até mais do que esperávamos. Desenvolvemos uma biblioteca de templates úteis e utilizamos esta abordagem na maioria dos nossos projetos e alguns dos nossos clientes também a utilizam e até encomendam o desenvolvimento de templates para as suas necessidades.



Infelizmente, nossa ferramenta ainda está longe do ideal, então eu gostaria de dividir a descrição em duas partes, a primeira é como vejo a implementação dessa funcionalidade em um mundo ideal e a segunda é como isso é feito aqui.



Antes de entrarmos em detalhes, gostaria de fazer uma pequena explicação - todos os exemplos neste artigo foram simplificados a um nível que permite que você mostre a ideia, sem ser sobrecarregado com detalhes irrelevantes.



Como isso seria feito em um mundo perfeito



Depois de vários anos usando nossa ferramenta, tenho uma visão de como gostaria que isso funcionasse se vivêssemos em um mundo ideal.



Na minha visão de um mundo ideal, as especificações da linguagem permitem o uso de transformações de código-fonte e há compilador e suporte IDE.



A ideia foi inspirada pela inclusão do modificador "parcial" na especificação da linguagem C #. Esse conceito bastante simples (a capacidade de definir uma classe, estrutura ou interface em vários arquivos) melhorou e simplificou drasticamente o suporte de ferramentas para geração automática de código-fonte. Essa. é uma espécie de divisão horizontal do código-fonte de uma classe entre vários arquivos. Para quem não conhece a linguagem C #, um pequeno exemplo.



Suponha que temos um formulário simples descrito no arquivo Example1.aspx

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


E lógica personalizada (por exemplo, alterar a cor de um botão para vermelho quando ele é clicado) no arquivo Example1.aspx.cs



public partial class ExamplePage1 : System.Web.UI.Page, IMyInterface
{
  protected void btnSubmit_Click(Object sender, EventArgs e) 
  {
    btnSubmit.Color = Color.Red;
  }
}


A presença no idioma dos recursos fornecidos por "parcial" permite que o kit de ferramentas analise o arquivo Example1.aspx e gere automaticamente o arquivo Example1.aspx.designer.cs.



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


Essa. temos a capacidade de armazenar uma parte do código para a classe ExamplePage1 em um arquivo pelo programador atualizável (Example1.aspx.cs) e a parte no arquivo Example1.aspx.designer.cs pelo kit de ferramentas gerado automaticamente. Para o compilador, no final, parece uma classe geral



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 o exemplo com a definição de herança da interface IMyInterface, você pode ver que o resultado final é uma combinação de definições de classe de arquivos diferentes.



Se não houver funcionalidade como parcial e o compilador exigir o armazenamento de todo o código da classe em apenas um arquivo, podemos assumir a inconveniência e os gestos adicionais necessários para suportar a geração automática.



Consequentemente, minha ideia é incluir dois modificadores adicionais na especificação da linguagem, o que tornará mais fácil incorporar aspectos no código-fonte.



O primeiro modificador é original e o adicionamos à definição da classe que deve poder ser transformada.



A segunda é processada e simboliza que esta é a definição final da classe que foi obtida pela ferramenta de transformação da fonte e que deve ser aceita pelo compilador para gerar o bytecode.



A sequência é mais ou menos assim



  1. O usuário trabalha com o código-fonte da classe que contém o modificador original no arquivo .cs (por exemplo, Example1.cs)
  2. Ao compilar, o compilador verifica a exatidão do código-fonte, e se a classe foi compilada com sucesso, ele verifica a presença do original
  3. Se o original estiver presente, o compilador fornece o código-fonte desse arquivo para o processo de transformação (que é uma caixa preta para o 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 .


Essa. Ao adicionar esses dois modificadores, fomos capazes de organizar o suporte para ferramentas de transformação de código-fonte no nível da linguagem, da mesma forma que parcial nos permitiu simplificar o suporte para geração de código-fonte. Essa. parial é a divisão de código horizontal, original / processado é vertical.



A meu ver, implementar o suporte original / processado no compilador é uma semana de trabalho para dois estagiários da Microsoft (uma piada, é claro, mas não está longe da verdade). De modo geral, não há dificuldades fundamentais nesta tarefa, do ponto de vista do compilador é a manipulação de arquivos e a invocação de processos.



Um novo recurso foi adicionado ao .NET 5 - geradores de código-fonteque já permite gerar novos arquivos de código-fonte durante a compilação e este é um movimento na direção certa. Infelizmente, ele apenas permite que você gere um novo código-fonte, mas não modifica o existente. Então, ainda estamos esperando.



Um exemplo de processo semelhante. O usuário cria o arquivo 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;
  }	
}


É executado para compilação, se tudo estiver ok e o compilador vir o modificador original, então ele fornece o código-fonte para o processo de transformação, que gera o arquivo Example2.processed.cs (no caso mais simples, pode ser apenas uma cópia exata de Example2.cs com original substituído por processado) ...



Em nosso caso, vamos assumir que o processo de transformação adicionou um aspecto de registro e o resultado se parece com este:

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


A próxima etapa é verificar as assinaturas. As assinaturas _main_ são idênticas e satisfazem a condição de que as definições no original e processadas devem ser exatamente as mesmas.



Neste exemplo, adicionei especialmente mais uma pequena frase, este é o modificador processado para métodos, propriedades e campos.



Ele marca métodos, propriedades e campos como disponíveis apenas para classes com o modificador processado e que são ignorados ao comparar assinaturas. Isso é feito para a conveniência dos desenvolvedores de aspectos e permite que você mova a lógica geral para métodos separados, de modo a não criar redundância de código desnecessária.



O compilador compilou este código e se tudo estiver ok, então pega o código de byte para continuar o processo.



É claro que neste exemplo há alguma simplificação e na realidade a lógica pode ser mais complicada (por exemplo, quando incluímos original e parcial para uma classe), mas isso não é uma complexidade intransponível.



Funcionalidade IDE básica em um mundo perfeito



O suporte para trabalhar com o código-fonte dos arquivos .processed.cs no IDE está principalmente na navegação correta entre as classes e transições originais / processadas durante a depuração passo a passo.



O segundo recurso mais importante do IDE (do meu ponto de vista) é ajudar na leitura do código das classes processadas. Uma classe Processed pode conter muitos trechos de código que foram adicionados por vários aspectos. A implementação de um display semelhante ao conceito de camadas em um editor gráfico parece-nos a opção mais conveniente para atingir esse objetivo. Nosso plugin atual implementa algo semelhante e a resposta de seus usuários é bastante positiva.



Outra função que ajudaria a introduzir o AOP na vida cotidiana é a funcionalidade de refatoração. o usuário, destacando uma parte do código, poderia dizer "Extrair para modelo AOP" e o IDE criava os arquivos corretos, gerava o código inicial e, após analisar o código do projeto, sugeria candidatos para a utilização de um modelo de outras classes.



Bem, a cereja no topo do bolo seria o suporte para escrever templates de aspectos, por exemplo, aplicando interativamente um aspecto a uma classe / método de sua escolha para que você possa avaliar o resultado final na hora, sem um ciclo de compilação explícito de sua parte.



Tenho certeza de que, se os criadores do resharper assumirem o controle do negócio, a mágica estará garantida.



Escrevendo código de aspecto em um mundo perfeito



Parafraseando TRIZ, a escrita ideal de código para a implementação de aspectos é a ausência de escrita de código adicional que existe apenas para suportar os processos de ferramentas.



Em um mundo ideal, gostaríamos de escrever código para o aspecto em si, sem o esforço de escrever lógica auxiliar para atingir esse objetivo. E esse código seria parte integrante do próprio projeto.



O segundo desejo é a capacidade de ter plug & play interativo, ou seja, tendo escrito um modelo, não precisaríamos realizar etapas adicionais para que ele fosse usado para transformação. Não houve necessidade de recompilar a ferramenta, detectar seus erros, etc. E também configurar opções em projetos para pós-compilação.



Depois de criar um modelo e escrever algumas linhas, veria imediatamente o resultado e, se contiver erros, sua detecção e depuração seriam integradas ao processo de aplicação do modelo, e não seriam uma parte separada que requer esforço adicional do programador.



Bem, para que a sintaxe do modelo seja o mais próximo possível da sintaxe da linguagem C #, de preferência um complemento menor, mais algumas palavras-chave e espaços reservados.



Nossa implementação atual



Infelizmente, não vivemos em um mundo ideal, então temos que reinventar as bicicletas e pedalá-las.



Injeção, compilação e depuração de código



Nosso modelo atual é criar duas cópias do projeto. Um é o original, aquele com o qual o programador trabalha, o segundo é o transformado, que é usado para compilação e execução.



O cenário é mais ou menos assim



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


Para depuração, a segunda cópia do IDE é iniciada, uma cópia do projeto formada por país é aberta e funciona com a cópia à qual a transformação foi aplicada.



O processo requer uma certa disciplina, mas de vez em quando se torna um hábito e, em certos casos, essa abordagem tem algumas vantagens (por exemplo, um build pode ser iniciado e implantado em um servidor remoto, em vez de trabalhar com uma máquina local). Além disso, a ajuda do plugin no VisualStudio simplifica o processo.



IDE



Usamos um plugin que é feito sob medida para nossas tarefas e processos específicos e o suporte para a implementação do código-fonte é uma parte bastante pequena de seus recursos.



Por exemplo, a funcionalidade de exibição de camadas, em um estilo semelhante a um editor gráfico, permite, por exemplo, ocultar / mostrar camadas de comentários, por escopo (por exemplo, para que apenas métodos públicos sejam visíveis), regiões. O código incorporado é cercado por comentários de um formato especial e eles também podem ser ocultados como uma camada separada.



Outra possibilidade é mostrar uma diferença entre o arquivo original e o transformado. como o IDE conhece a localização relativa da cópia do arquivo no projeto, ele pode exibir as diferenças entre os arquivos originais e os gerados pelo país.



Além disso, o plug-in avisa ao tentar fazer alterações na cópia gerada pelo país (para não perdê-las durante a retransformação subsequente)



Configuração



Uma tarefa separada é definir as regras de transformação, ou seja, a quais classes e métodos aplicaremos a transformação.



Usamos vários níveis.



O primeiro nível é o arquivo de configuração de nível superior. Podemos definir regras dependendo do caminho no sistema de arquivos, padrões no nome de arquivos, classes ou métodos, escopos de classes, métodos ou propriedades.



O segundo nível é uma indicação da aplicação das regras de transformação ao nível dos atributos de classes, métodos ou campos.



O terceiro no nível do bloco de código e o quarto é uma indicação explícita para incluir os resultados da transformação do modelo em um local específico no código-fonte.



Modelos



Historicamente, para fins de geração automática, usamos modelos no formato T4, por isso era bastante lógico usar a mesma abordagem dos modelos para transformação. Os modelos T4 incluem a capacidade de executar código C # arbitrário, têm sobrecarga mínima e boa expressividade.



Para quem nunca trabalhou com T4, o análogo mais simples seria apresentar o formato ASPX, que em vez de HTML gera código-fonte em C # e é executado não no IIS, mas como um utilitário separado com saída do resultado para o console (ou para um arquivo).



Exemplos de



Para entender como isso funciona na realidade, o mais simples é demonstrar o código antes e depois da transformação e o código-fonte dos templates que são usados ​​durante a transformação. Vou demonstrar as opções mais simples, mas o potencial é limitado apenas pela sua imaginação.



Amostra de código-fonte antes da transformação
// ##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}";
            }
        }
    }
}




Versão completa do código-fonte após a transformação
//------------------------------------------------------------------------------
// <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







Explicações e código do modelo



Modelo de comentário automático



// ##aspect=AutoComment


Se no código-fonte encontrarmos um comentário em um formato especial, então executamos o modelo especificado (neste caso, é o AutoComment) e inserimos o resultado da transformação em vez deste comentário. Neste exemplo, faz sentido inserir automaticamente um aviso de isenção especial que avisará o programador que o código neste arquivo é o resultado da transformação e não faz sentido modificar este arquivo diretamente.



Código do modelo 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>
//------------------------------------------------------------------------------


As variáveis ​​FileName, FileSha256, User, MachineName e Now são exportadas para o modelo do processo de transformação.



Resultado da transformação



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


A seguinte transformação é especificada como um atributo da classe



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



Este atributo sinaliza que o modelo deve ser aplicado a todos os métodos de classe que contêm a palavra "First". O parâmetro NameFilter é um padrão de expressão regular usado para determinar quais métodos incluir na transformação.



Código do modelo ClassLevelTemplateForMethods.t4



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

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


Este é o exemplo mais simples que adiciona um comentário // class level templateantes do



resultado da transformação do código do método



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


As seguintes transformações são especificadas como atributos de método para demonstrar várias transformações aplicadas ao mesmo método. LogExceptionMethod.t4 Template



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


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


Modelo MethodFinallyDemo.t4

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

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

<#= MethodEnd() #>


Resultado das transformações

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


A seguinte transformação é dada para um bloco limitado a uma construção em 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}");
    }
}


Modelo SecondDemoUsing.t4

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

// second demo <#= ExtraTag #>

<#= StatementBody() #>


ExtraTag é uma string passada como parâmetro. Isso pode ser útil para genéricos que podem ter um comportamento ligeiramente diferente dependendo dos parâmetros de entrada.



Resultado da transformação



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


A seguinte transformação é especificada pelos atributos da classe NotifyPropertyChanged.Este é um exemplo clássico, que junto com o exemplo de registro é dado na maioria dos exemplos de programação orientada a aspectos.



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

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








Modelo NotifyPropertyChangedClass.t4 aplicado ao código da classe
<#@ 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


Modelo NotifyPropertyChanged.t4 aplicado às propriedades da 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();
			}
		}
	}


Código original da classe e propriedades

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

// ...
}


Resultado da transformação

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

// ...
}


Um exemplo de um modelo para resultados de propriedade de armazenamento em cache, ele é especificado pelos



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



parâmetros do modelo são especificados como atributo JSON. Se não houver parâmetros explícitos, os parâmetros padrão serão usados.



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

	}


Fonte

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


Resultado da transformação 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}";
        }
    }
}


A próxima chamada para o modelo novamente a partir do comentário

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


Modelo FullNameComment.t4

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

// FullNameComment <#= PropertyNode.Identifier #>


Muito semelhante ao modelo AutoComment.t4, mas aqui demonstramos o uso de PropertyNode. Além disso, os dados "dados extras aqui" estão disponíveis para o modelo FullNameComment.t4 por meio do parâmetro ExtraTag (mas neste exemplo não os usamos, então eles são simplesmente ignorados)



Resultado da transformação

// FullNameComment FullName


A seguinte transformação no arquivo é especificada pelo atributo



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



AND , que é idêntico ao da classe Person. O código-fonte do modelo NotifyPropertyChanged.t4 já foi incluído acima.



Resultado da transformação

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

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


Parte final



Embora este artigo se concentre na programação orientada a aspectos, a técnica de transformação do código-fonte é universal e, em princípio, pode ser usada para tarefas não relacionadas ao AOP.



Por exemplo, pode ser usado para injeção de dependência, ou seja, mudamos o código de criação de recursos dependendo dos parâmetros de construção.



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

#>




No código-fonte (aqui o recurso de variável dinâmica é usado, o que permite que sejam atribuídos a qualquer tipo), ou seja, para expressividade, nós meio que criamos uma nova palavra-chave.

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 a transformação, a comparação GetGlobalSetting ("env") == "test" é usada e, dependendo dessa condição, um novo DataService () ou um novo MockDataService () será injetado.



Resultado da transformação


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 você pode usar essa ferramenta como uma análise estática do "pobre homem" (mas é muito, muito mais correto implementar analisadores usando a funcionalidade nativa do Roslyn), analisamos o código de nossas regras e o inserimos no código-fonte.



#error our error message here



Isso levará a um erro de tempo de compilação.



#warning our warning message here



Que servirá como um aviso no IDE ou durante a compilação.



Modelo 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 da transformação

#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 como uma ferramenta automática para localizar um aplicativo, ou seja, encontre todas as strings nas classes e substitua-as com o uso dos recursos apropriados.



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




Fonte


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;


No arquivo de recurso Demo.resx, criamos as seguintes linhas para um exemplo

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


e o código gerado automaticamente do arquivo 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 da transformação (observe que a string interpolada foi substituída por String.Format e o recurso "Nome {0} Sobrenome {1}" foi usado). Para linhas que não existem no arquivo de recurso ou não correspondem ao nosso formato, uma mensagem de erro é adicionada

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


Além disso, a ferramenta de transformação permite que você trabalhe não apenas com arquivos C #, mas também com qualquer tipo de arquivo (claro, com certas restrições). Se você tiver um analisador que pode construir um AST para sua linguagem, poderá substituir Roslyn por este analisador, ajustar a implementação do manipulador de código e ele funcionará. Infelizmente, o número de bibliotecas com funcionalidade perto de Roslyn é muito limitado e seu uso requer muito mais esforço. Além do C #, usamos transformações para projetos JavaScript e TypeScript, mas certamente não de forma tão abrangente quanto para C #.



Mais uma vez, repito que o código do exemplo e os modelos são dados como ilustração das possibilidades de tal abordagem e, como dizem, o céu é o limite.



Obrigado pelo seu tempo.



A maior parte deste artigo foi escrita há alguns anos, mas infelizmente, por alguns motivos, só foi possível publicá-la agora.



Nossa ferramenta original é desenvolvida no .Net Framework, mas começamos a trabalhar em uma versão de código aberto simplificada sob a licença do MIT para .Net Core. No momento, o resultado está totalmente funcional e 90% pronto, há pequenas melhorias, penteado do código, criação de documentação e exemplos, mas sem tudo isso será difícil entrar no projeto, a ideia em si ficará comprometida e DX ficará negativo.



Quem trabalhou na sua criação não conseguiu terminar antes de se mudar para outra empresa, por isso antes de alocar recursos para dar continuidade ao trabalho, queremos ver a reação da comunidade, pois entendemos que o que é adequado no nosso caso não está necessariamente em demanda e é bem possível, que este nicho está sendo preenchido por alguma ferramenta alternativa ou abordagem de desenvolvimento.



A própria ideia da ferramenta é muito simples e o desenvolvedor gastou um total de cerca de um mês na implementação de uma versão funcional, então acho que um programador com boas qualificações e experiência com Roslyn pode criar sua própria versão específica em poucos dias. No momento, o tamanho do código-fonte do projeto é apenas cerca de 150 KB, incluindo exemplos e modelos.



Eu ficaria feliz em receber críticas construtivas (críticas não construtivas também não me aborrecerão, então não hesite).



Graças a Phil Rangin (fillpackart) para motivação ao escrever o artigo. Regras do canal "We Are Doomed"!



All Articles