Pemrograman Berorientasi Aspek (AOP) melalui kode sumber





Pemrograman berorientasi aspek adalah konsep yang sangat menarik untuk menyederhanakan basis kode Anda, menghasilkan kode yang bersih, dan meminimalkan kesalahan salin-tempel.



Saat ini, dalam banyak kasus, aspek diimplementasikan pada level bytecode, mis. setelah kompilasi, beberapa alat "menjalin" kode byte tambahan dengan dukungan logika yang diperlukan.



Pendekatan kami (serta pendekatan beberapa alat lainnya) adalah memodifikasi kode sumber untuk mengimplementasikan logika aspek. Dengan transisi ke teknologi Roslyn, sangat mudah untuk mencapai ini, dan hasilnya memberikan keuntungan tertentu dibandingkan modifikasi kode byte itu sendiri.



Jika Anda tertarik dengan detailnya, silakan lihat kucing.



Anda mungkin berpikir bahwa pemrograman berorientasi aspek bukan tentang Anda dan tidak terlalu mengkhawatirkan Anda, hanya sekumpulan kata-kata yang tidak dapat dipahami, tetapi sebenarnya jauh lebih mudah daripada yang terlihat, ini tentang masalah pengembangan produk nyata dan jika Anda terlibat dalam pengembangan industri, maka Anda pasti bisa mendapatkan mendapatkan keuntungan dari menggunakannya.



Terutama dalam proyek menengah-besar di tingkat perusahaan, di mana persyaratan untuk fungsionalitas produk diformalkan. Misalnya, mungkin ada persyaratan - saat menyetel flag konfigurasi, catat semua parameter input untuk semua metode publik. Atau untuk semua metode proyek memiliki sistem pemberitahuan yang akan mengirim pesan ketika ambang waktu eksekusi tertentu dari metode ini terlampaui.



Bagaimana ini dilakukan tanpa AOP? Entah itu dipalu dan dilakukan hanya untuk bagian yang paling penting, atau saat menulis metode baru, salin-tempel kode serupa dari metode tetangga, dengan semua metode yang menyertainya, berjalan.



Saat menggunakan AOP, saran ditulis setelah diterapkan pada proyek dan pekerjaan selesai. Ketika Anda perlu memperbarui logika sedikit, Anda akan memperbarui saran sekali dan itu akan diterapkan pada build berikutnya. Tanpa AOP, itu berarti 100.500 pembaruan di seluruh kode proyek.



Kelebihannya adalah kode Anda berhenti terlihat seperti orang yang pernah menderita cacar, karena fakta bahwa kode tersebut dipenuhi dengan fungsionalitas seperti itu dan ketika membaca kode itu terlihat seperti suara yang mengganggu.



Setelah menerapkan AOP ke dalam proyek Anda, Anda mulai menerapkan hal-hal yang tidak pernah Anda impikan tanpanya, karena tampaknya keuntungan yang relatif kecil, dengan biaya tinggi. Dengan AOP, semuanya justru sebaliknya, biaya yang relatif rendah dan manfaat yang besar (untuk tingkat biaya yang sama dari upaya Anda).



Dalam pengalaman saya, dalam ekosistem .Net, pemrograman berorientasi aspek secara signifikan kurang populer dibandingkan dengan ekosistem Java. Menurut saya alasan utamanya adalah kurangnya alat gratis dan sumber terbuka yang sebanding dengan fungsionalitas dan kualitas Java.



PostSharp menyediakan fungsionalitas dan kenyamanan yang serupa, tetapi tidak banyak yang bersedia membayar ratusan dolar untuk menggunakannya dalam proyek mereka, dan versi komunitas sangat terbatas kemampuannya. Tentu ada alternatif, tapi sayangnya mereka belum mencapai level PostSharp.



Anda dapat membandingkan kemampuan alat-alat (harus diingat bahwa perbandingan dibuat oleh pemilik PostSharp, tetapi ini memberikan gambaran).



Jalan kami menuju pemrograman berorientasi aspek



Kami adalah perusahaan konsultan kecil (12 orang) dan hasil akhir dari pekerjaan kami adalah kode sumber. Itu. kami dibayar untuk membuat kode sumber, kode kualitas. Kami hanya bekerja di satu industri dan banyak proyek kami memiliki persyaratan yang sangat mirip dan akibatnya, kode sumber juga sangat mirip di antara proyek-proyek ini.



Dan karena kami terbatas dalam sumber daya, maka bagi kami, salah satu tugas terpenting adalah kemampuan untuk menggunakan kembali kode dan menggunakan alat yang menyelamatkan pengembang dari tugas rutin.



Untuk mencapai ini, salah satu caranya adalah dengan menggunakan kapabilitas pembuatan kode otomatis, dan juga membuat beberapa plugin dan penganalisis kustom untuk Visual Studio khusus untuk proyek dan tugas kami. Ini memungkinkan untuk secara signifikan meningkatkan produktivitas pemrogram, sambil mempertahankan kualitas kode yang tinggi (bahkan dapat dikatakan bahwa kualitasnya menjadi lebih tinggi).



Langkah logis berikutnya adalah ide untuk mengimplementasikan penggunaan pemrograman berorientasi aspek. Kami mencoba beberapa pendekatan dan alat, tetapi hasilnya jauh dari harapan kami. Ini bertepatan dengan diluncurkannya teknologi Roslyn, dan pada saat tertentu kami memiliki ide untuk menggabungkan kemampuan pembuatan kode otomatis dan Roslyn.



Hanya dalam beberapa minggu, prototipe instrumen telah dibuat dan, menurut perasaan kami, pendekatan ini tampak lebih menjanjikan. Setelah beberapa kali pengulangan tentang cara menggunakan dan memperbarui alat ini, kami dapat mengatakan bahwa harapan kami terpenuhi dan bahkan lebih dari yang kami harapkan. Kami telah mengembangkan perpustakaan templat yang berguna dan menggunakan pendekatan ini di sebagian besar proyek kami, dan beberapa klien kami juga menggunakannya dan bahkan memesan pengembangan templat untuk kebutuhan mereka.



Sayangnya, alat kami masih jauh dari ideal, jadi saya ingin membagi deskripsi menjadi dua bagian, yang pertama adalah bagaimana saya melihat penerapan fungsi ini di dunia yang ideal dan yang kedua adalah bagaimana hal itu dilakukan di sini.



Sebelum kita beralih ke detailnya, saya ingin membuat penjelasan kecil - semua contoh dalam artikel ini telah disederhanakan ke tingkat yang memungkinkan Anda untuk menampilkan ide, tanpa dibebani dengan detail yang tidak relevan.



Bagaimana itu akan dilakukan di dunia yang sempurna



Setelah beberapa tahun menggunakan alat kami, saya memiliki visi tentang bagaimana saya ingin ini bekerja jika kita hidup di dunia yang ideal.



Dalam visi saya tentang dunia yang ideal, spesifikasi bahasa memungkinkan penggunaan transformasi kode sumber, dan ada kompiler dan dukungan IDE.



Ide ini terinspirasi oleh penyertaan pengubah "parsial" dalam spesifikasi bahasa C #. Konsep yang agak sederhana ini (kemampuan untuk mendefinisikan kelas, struktur atau antarmuka dalam beberapa file) telah secara dramatis meningkatkan dan menyederhanakan dukungan alat untuk pembuatan kode sumber otomatis. Itu. Ini adalah semacam pemisahan horizontal dari kode sumber kelas antara beberapa file. Bagi yang belum paham bahasa C #, contoh kecilnya.



Misalkan kita memiliki formulir sederhana yang dijelaskan di file Example1.aspx

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


Dan logika khusus (misalnya, mengubah warna tombol menjadi merah saat diklik) di file Example1.aspx.cs



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


Kehadiran dalam bahasa kemampuan yang disediakan oleh "parsial" memungkinkan toolkit untuk mengurai file Example1.aspx dan secara otomatis menghasilkan file Example1.aspx.designer.cs.



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


Itu. kita memiliki kemampuan untuk menyimpan sebagian kode untuk kelas ExamplePage1 dalam satu file oleh programmer yang dapat diperbarui (Example1.aspx.cs) dan sebagian dalam file Example1.aspx.designer.cs oleh toolkit yang dibuat secara otomatis. Bagi kompiler, pada akhirnya, ini terlihat seperti satu kelas umum



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


Menggunakan contoh dengan definisi pewarisan antarmuka IMyInterface, Anda dapat melihat bahwa hasil akhirnya adalah kombinasi definisi kelas dari file yang berbeda.



Jika kita kekurangan fungsionalitas seperti parsial dan compiler harus menyimpan semua kode kelas hanya dalam satu file, maka kita dapat menganggap ketidaknyamanan dan isyarat tambahan yang diperlukan untuk mendukung pembuatan otomatis.



Oleh karena itu, ide saya adalah memasukkan dua pengubah tambahan dalam spesifikasi bahasa, yang akan mempermudah penyematan aspek ke dalam kode sumber.



Pengubah pertama adalah asli dan kami menambahkannya ke definisi kelas yang seharusnya dapat diubah.



Yang kedua diproses dan melambangkan bahwa ini adalah definisi kelas terakhir yang diperoleh oleh alat transformasi sumber dan harus diterima oleh kompiler untuk menghasilkan bytecode.



Urutannya kira-kira seperti ini



  1. Pengguna bekerja dengan kode sumber kelas yang berisi pengubah asli dalam file .cs (misalnya Example1.cs)
  2. Saat mengompilasi, kompilator memeriksa kebenaran kode sumber, dan jika kelas berhasil dikompilasi, ia memeriksa keberadaan yang asli.
  3. Jika yang asli ada, maka kompilator memberikan kode sumber dari file ini ke proses transformasi (yang merupakan kotak hitam untuk kompilator).
  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 .


Itu. Dengan menambahkan dua pengubah ini, kami dapat mengatur dukungan untuk alat transformasi kode sumber di tingkat bahasa, seperti sebagian memungkinkan kami untuk menyederhanakan dukungan untuk pembuatan kode sumber. Itu. parial adalah pemisahan kode horizontal, asli / diproses adalah vertikal.



Seperti yang saya lihat, mengimplementasikan dukungan asli / yang diproses dalam kompiler adalah pekerjaan selama seminggu untuk dua orang magang di Microsoft (lelucon tentu saja, tetapi tidak jauh dari kebenaran). Secara umum, tidak ada kesulitan mendasar dalam tugas ini, dari sudut pandang kompiler itu adalah manipulasi file dan pemanggilan proses.



Sebuah fitur baru telah ditambahkan di .NET 5 - generator kode sumberyang memungkinkan Anda untuk menghasilkan file kode sumber baru selama kompilasi dan ini adalah gerakan ke arah yang benar. Sayangnya, ini hanya memungkinkan Anda membuat kode sumber baru, tetapi tidak dapat mengubah yang sudah ada. Jadi kami masih menunggu.



Contoh proses serupa. Pengguna membuat file 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;
  }	
}


Berjalan untuk kompilasi, jika semuanya baik-baik saja dan kompilator melihat pengubah aslinya, maka ia memberikan kode sumber ke proses transformasi, yang menghasilkan file Example2.processed.cs (dalam kasus yang paling sederhana, ini hanya bisa menjadi salinan persis dari Example2.cs dengan aslinya diganti dengan diproses) ...



Dalam kasus kami, kami akan menganggap bahwa proses transformasi telah menambahkan aspek logging dan hasilnya terlihat seperti ini:

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


Langkah selanjutnya adalah memverifikasi tanda tangan. Tanda tangan _main_ identik dan memenuhi ketentuan bahwa definisi dalam bahasa asli dan yang diproses harus sama persis.



Dalam contoh ini, saya secara khusus menambahkan satu kalimat kecil lagi, ini adalah pengubah yang diproses untuk metode, properti, dan bidang.



Ini menandai metode, properti, dan bidang sebagai hanya tersedia untuk kelas dengan pengubah yang diproses dan yang diabaikan saat membandingkan tanda tangan. Ini dilakukan untuk kenyamanan pengembang aspek dan memungkinkan Anda untuk memindahkan logika umum ke metode terpisah agar tidak membuat redundansi kode yang tidak perlu.



Kompilator mengkompilasi kode ini dan jika semuanya baik-baik saja, maka mengambil kode byte untuk melanjutkan proses.



Jelas bahwa dalam contoh ini ada beberapa penyederhanaan dan pada kenyataannya logikanya mungkin lebih rumit (misalnya, ketika kita memasukkan asli dan parsial untuk kelas yang sama), tetapi ini bukan kompleksitas yang tidak dapat diatasi.



Fungsionalitas IDE dasar di dunia yang sempurna



Dukungan untuk bekerja dengan kode sumber file .processed.cs di IDE terutama pada navigasi yang benar antara kelas asli / yang diproses dan transisi selama proses debug langkah demi langkah.



Fitur terpenting kedua dari IDE (dari sudut pandang saya) adalah membantu dalam membaca kode kelas yang diproses. Kelas Diproses dapat berisi banyak potongan kode yang telah ditambahkan oleh beberapa aspek. Implementasi tampilan yang mirip dengan konsep lapisan dalam editor grafis bagi kami merupakan pilihan yang paling nyaman untuk mencapai tujuan ini. Plugin kami saat ini menerapkan sesuatu yang serupa dan respon dari penggunanya cukup positif.



Fitur lain yang akan membantu memperkenalkan AOP ke dalam kehidupan sehari-hari adalah fungsi refactoring. pengguna, menyorot bagian kode, dapat mengatakan "Ekstrak Ke Template AOP" dan IDE membuat file yang benar, membuat kode awal dan setelah menganalisis kode proyek, menyarankan kandidat untuk menggunakan template dari kelas lain.



Nah, lapisan gula pada kue akan mendukung penulisan template aspek, misalnya, menerapkan aspek secara interaktif ke kelas / metode pilihan Anda, sehingga Anda dapat mengevaluasi hasil akhir dengan cepat, tanpa siklus kompilasi eksplisit di pihak Anda.



Saya yakin jika pencipta resharper mengambil alih bisnis, maka keajaibannya terjamin.



Menulis kode aspek di dunia yang sempurna



Untuk memparafrasekan TRIZ, penulisan kode yang ideal untuk mengimplementasikan aspek adalah tidak adanya penulisan kode tambahan yang hanya ada untuk mendukung proses instrumentasi.



Dalam dunia yang ideal, kami ingin menulis kode untuk aspek itu sendiri, tanpa usaha menulis logika pembantu untuk mencapai tujuan ini. Dan kode ini akan menjadi bagian integral dari proyek itu sendiri.



Keinginan kedua adalah kemampuan untuk memiliki plug & play interaktif, mis. setelah menulis template, kita tidak perlu mengambil langkah tambahan agar dapat digunakan untuk transformasi. Tidak perlu mengkompilasi ulang alat tersebut, menangkap kesalahannya, dll. Dan juga konfigurasikan opsi dalam proyek untuk pasca kompilasi.



Setelah membuat template dan menulis beberapa baris, saya akan langsung melihat hasilnya dan jika terdapat error, deteksi dan debuggingnya akan diintegrasikan ke dalam proses penerapan template, dan bukan menjadi bagian terpisah yang memerlukan upaya tambahan dari programmer.



Nah, agar sintaks template sedekat mungkin dengan sintaks bahasa C #, idealnya sedikit add-on, ditambah beberapa kata kunci dan placeholder.



Implementasi kami saat ini



Sayangnya, kita tidak hidup di dunia yang sempurna, jadi kita harus menemukan kembali sepeda dan mengendarainya.



Injeksi kode, kompilasi, dan debugging



Model kami saat ini adalah membuat dua salinan proyek. Yang pertama adalah yang asli yang digunakan programmer, yang kedua diubah, yang digunakan untuk kompilasi dan eksekusi.



Skenarionya kira-kira seperti ini



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


Untuk debugging, salinan kedua dari IDE diluncurkan, salinan proyek yang dibentuk negara dibuka dan bekerja dengan salinan yang mana transformasi diterapkan.



Prosesnya membutuhkan disiplin tertentu, tetapi dari waktu ke waktu menjadi kebiasaan dan dalam kasus tertentu pendekatan ini memiliki beberapa keuntungan (misalnya, build dapat diluncurkan dan diterapkan ke server jarak jauh, alih-alih bekerja dengan mesin lokal). Ditambah bantuan dari plugin di VisualStudio menyederhanakan prosesnya.



IDE



Kami menggunakan plugin yang disesuaikan untuk tugas dan proses khusus kami, dan dukungan untuk implementasi kode sumber adalah sebagian kecil dari kemampuannya.



Misalnya, fungsionalitas untuk menampilkan lapisan, dengan gaya yang mirip dengan editor grafis, memungkinkan, misalnya, untuk menyembunyikan / menampilkan lapisan komentar, menurut cakupan (misalnya, sehingga hanya metode publik yang terlihat), kawasan. Kode yang disematkan dikelilingi oleh komentar dengan format khusus dan mereka juga dapat disembunyikan sebagai lapisan terpisah.



Kemungkinan lain adalah untuk menunjukkan perbedaan antara file asli dan file hasil transformasi. karena IDE mengetahui lokasi relatif dari salinan file dalam proyek tersebut, IDE dapat menampilkan perbedaan antara file asli dan yang dibuat oleh negara.



Selain itu, plugin memperingatkan ketika mencoba membuat perubahan pada salinan yang dibuat oleh negara (agar tidak kehilangannya selama transformasi ulang berikutnya)



Konfigurasi



Tugas terpisah adalah menetapkan aturan transformasi, mis. ke kelas dan metode mana kita akan menerapkan transformasi.



Kami menggunakan beberapa level.



Tingkat pertama adalah file konfigurasi tingkat atas. Kita dapat menetapkan aturan bergantung pada jalur pada sistem file, pola dalam nama file, kelas atau metode, cakupan kelas, metode atau properti.



Tingkat kedua merupakan indikasi penerapan aturan transformasi pada tingkat atribut kelas, metode, atau bidang.



Yang ketiga di level blok kode dan yang keempat adalah indikasi eksplisit untuk memasukkan hasil transformasi template di tempat tertentu dalam kode sumber.



Template



Secara historis, untuk tujuan pembuatan otomatis, kami menggunakan templat dalam format T4, jadi cukup logis untuk menggunakan pendekatan yang sama seperti templat untuk transformasi. Template T4 menyertakan kemampuan untuk mengeksekusi kode C # sewenang-wenang, memiliki overhead minimal dan ekspresi yang baik.



Bagi mereka yang belum pernah bekerja dengan T4, analog yang paling sederhana adalah menampilkan format ASPX, yang bukannya HTML menghasilkan kode sumber dalam C # dan dijalankan bukan di IIS, tetapi sebagai utilitas terpisah dengan mengeluarkan hasilnya ke konsol (atau ke file).



Contoh dari



Untuk memahami bagaimana ini bekerja pada kenyataannya, hal yang paling sederhana adalah dengan mendemonstrasikan kode sebelum dan sesudah transformasi dan kode sumber dari template yang digunakan selama transformasi. Saya akan mendemonstrasikan opsi paling sederhana, tetapi potensinya hanya dibatasi oleh imajinasi Anda.



Contoh kode sumber sebelum transformasi
// ##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 lengkap dari kode sumber setelah transformasi
//------------------------------------------------------------------------------
// <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







Penjelasan dan kode template



Template AutoComment



// ##aspect=AutoComment


Jika dalam kode sumber kami menemukan komentar dalam format khusus, maka kami menjalankan template yang ditentukan (dalam hal ini adalah AutoComment) dan memasukkan hasil transformasi sebagai ganti komentar ini. Dalam contoh ini, masuk akal untuk memasukkan penafian khusus secara otomatis yang akan memperingatkan programmer bahwa kode dalam file ini adalah hasil transformasi dan tidak masuk akal untuk mengubah file ini secara langsung.



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


Variabel FileName, FileSha256, User, MachineName, dan Now diekspor ke template dari proses transformasi.



Hasil transformasi



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


Transformasi berikut ditetapkan sebagai atribut kelas



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



Atribut ini memberi sinyal bahwa template harus diterapkan ke semua metode kelas yang berisi kata "Pertama". Parameter NameFilter adalah pola ekspresi reguler yang digunakan untuk menentukan metode mana yang akan disertakan dalam transformasi.



Kode template ClassLevelTemplateForMethods.t4



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

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


Ini adalah contoh paling sederhana yang menambahkan komentar // class level templatesebelum kode metode



hasil Transformasi



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


Transformasi berikut ditetapkan sebagai atribut metode untuk mendemonstrasikan beberapa transformasi yang diterapkan ke metode yang sama. Template 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() #>


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 Template

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

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

<#= MethodEnd() #>


Hasil transformasi

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


Transformasi berikut diberikan untuk blok yang terbatas pada konstruksi penggunaan



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 Template

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

// second demo <#= ExtraTag #>

<#= StatementBody() #>


ExtraTag adalah string yang dikirimkan sebagai parameter. Ini dapat berguna untuk obat generik yang dapat memiliki perilaku yang sedikit berbeda bergantung pada parameter masukan.



Hasil transformasi



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


Transformasi berikut ditentukan oleh atribut kelas NotifyPropertyChanged . Ini adalah contoh klasik, yang bersama dengan contoh logging diberikan di sebagian besar contoh pemrograman berorientasi aspek.



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

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








Template NotifyPropertyChangedClass.t4 diterapkan ke kode kelas
<#@ 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


Template NotifyPropertyChanged.t4 diterapkan ke properti kelas
<#@ 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();
			}
		}
	}


Kode asli kelas dan properti

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

// ...
}


Hasil transformasi

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

// ...
}


Contoh template untuk hasil properti caching, ditentukan oleh



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



parameter Template ditentukan sebagai atribut JSON. Jika tidak ada parameter eksplisit, parameter default digunakan.



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

	}


Sumber

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


Hasil transformasi untuk 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}";
        }
    }
}


Panggilan berikutnya ke template lagi dari komentar

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


FullNameComment.t4 Template

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

// FullNameComment <#= PropertyNode.Identifier #>


Sangat mirip dengan template AutoComment.t4, tetapi di sini kami mendemonstrasikan penggunaan PropertyNode. Selain itu, data "data tambahan di sini" tersedia untuk template FullNameComment.t4 melalui parameter ExtraTag (tetapi dalam contoh ini kami tidak menggunakannya, sehingga diabaikan begitu saja)



Hasil transformasi

// FullNameComment FullName


Transformasi berikut dalam file ditentukan oleh atribut



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



AND yang identik dengan kelas Person. Kode sumber untuk template NotifyPropertyChanged.t4 telah disertakan di atas.



Hasil transformasi

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

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


Bagian terakhir



Meskipun artikel ini berfokus pada pemrograman berorientasi aspek, teknik transformasi kode sumber bersifat universal dan, pada prinsipnya, dapat digunakan untuk tugas-tugas yang tidak terkait dengan AOP.



Misalnya, dapat digunakan untuk injeksi ketergantungan, mis. kami mengubah kode pembuatan sumber daya tergantung pada parameter build.



DependencyInjection.t4 Template
<#@ 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;
	}

#>




Dalam kode sumber (di sini fitur variabel dinamis digunakan, yang memungkinkannya untuk ditetapkan ke jenis apa pun), mis. untuk ekspresi, kami datang dengan kata kunci baru.

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;


Selama transformasi, perbandingan GetGlobalSetting ("env") == "test" digunakan dan bergantung pada kondisi ini, DataService () baru atau MockDataService () baru akan dimasukkan.



Hasil transformasi


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


Atau Anda dapat menggunakan alat ini sebagai analisis statis "orang miskin" (tetapi jauh lebih tepat untuk mengimplementasikan penganalisis menggunakan fungsionalitas asli Roslyn), kami menganalisis kode untuk aturan kami dan memasukkannya ke dalam kode sumber.



#error our error message here



Hal itu akan menyebabkan kesalahan waktu kompilasi.



#warning our warning message here



Yang akan berfungsi sebagai peringatan di IDE atau saat kompilasi.



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




Hasil transformasi

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


Atau sebagai alat otomatis untuk melokalkan aplikasi, mis. temukan semua string di kelas dan ganti dengan penggunaan sumber daya yang sesuai.



ResourceReplacer.t4 template
<#@ 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;
#>




Sumber


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;


Dalam file sumber daya Demo.resx, misalnya, kami telah membuat baris berikut

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


dan kode yang dibuat otomatis dari file 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);
        }
    }
}


Hasil transformasi (perhatikan bahwa string yang diinterpolasi diganti dengan String.Format dan sumber daya "Nama Depan {0} Nama Belakang {1}" digunakan). Untuk baris yang tidak ada di file sumber daya atau tidak cocok dengan format kami, pesan kesalahan ditambahkan

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


Selain itu, alat transformasi memungkinkan Anda bekerja tidak hanya dengan file C #, tetapi juga dengan jenis file apa pun (tentu saja, dengan batasan tertentu). Jika Anda memiliki parser yang dapat membangun AST untuk bahasa Anda, maka Anda dapat mengganti Roslyn dengan parser ini, mengubah implementasi penangan kode dan itu akan berhasil. Sayangnya, jumlah pustaka dengan fungsionalitas yang mendekati Roslyn sangat terbatas dan penggunaannya membutuhkan lebih banyak usaha. Selain C #, kami menggunakan transformasi untuk proyek JavaScript dan TypeScript, tetapi tentu saja tidak sekomprehensif C #.



Sekali lagi, saya ulangi bahwa contoh kode dan templat diberikan sebagai ilustrasi kemungkinan pendekatan semacam itu dan, seperti yang mereka katakan - langit adalah batasnya.



Terima kasih atas waktu Anda.



Bagian utama dari artikel ini ditulis beberapa tahun yang lalu, tetapi sayangnya, untuk beberapa alasan, baru dapat diterbitkan sekarang.



Alat asli kami dikembangkan pada .Net Framework, tetapi kami mulai mengerjakan versi open source yang disederhanakan di bawah lisensi MIT untuk .Net Core. Saat ini, hasilnya berfungsi penuh dan 90% siap, ada perbaikan kecil, gaya rambut kode, pembuatan dokumentasi dan contoh, tetapi tanpa semua ini akan sulit untuk memasuki proyek, idenya sendiri akan dikompromikan dan DX akan menjadi negatif.



Orang yang mengerjakan pembuatannya tidak dapat menyelesaikannya sebelum pindah ke perusahaan lain, jadi sebelum mengalokasikan sumber daya untuk melanjutkan pekerjaan, kami ingin melihat reaksi masyarakat, karena kami memahami bahwa apa yang sesuai dalam kasus kami belum tentu diminati dan sangat mungkin, bahwa ceruk ini sedang diisi oleh beberapa alat atau pendekatan alternatif untuk pengembangan.



Ide alat ini sangat sederhana dan pengembang menghabiskan total sekitar satu bulan untuk mengimplementasikan versi yang bisa diterapkan, jadi menurut saya seorang programmer dengan kualifikasi dan pengalaman yang baik dengan Roslyn akan dapat membuat versi spesifiknya sendiri dalam beberapa hari. Saat ini, ukuran source code project hanya sekitar 150 KB, termasuk contoh dan template.



Saya akan senang menerima kritik yang membangun (kritik yang tidak membangun juga tidak akan membuat saya kesal, jadi jangan ragu).



Terima kasih untuk Phil Rangin (fillpackart) untuk motivasi dalam menulis artikel. Aturan saluran "We Are Doomed"!



All Articles