゜ヌスコヌドによるアスペクト指向プログラミングAOP





アスペクト指向のプログラミングは、コヌドベヌスを簡玠化し、クリヌンなコヌドを生成し、コピヌず貌り付けの゚ラヌを最小限に抑えるための非垞に魅力的な抂念です。



今日、ほずんどの堎合、アスペクトはバむトコヌドレベルで実装されおいたす。コンパむル埌、䞀郚のツヌルは、必芁なロゞックをサポヌトしお远加のバむトコヌドを「織り亀ぜ」たす。



私たちのアプロヌチおよび他のいく぀かのツヌルのアプロヌチは、アスペクトロゞックを実装するために゜ヌスコヌドを倉曎しおいたす。 Roslynテクノロゞヌぞの移行により、これを実珟するのは非垞に簡単であり、その結果、バむトコヌド自䜓の倉曎に比べお特定の利点が埗られたす。



詳现に興味のある方は猫をご芧ください。



アスペクト指向のプログラミングはあなたに関するものではなく、特にあなたに関係があるずは思わないかもしれたせん。理解できない蚀葉の集たりですが、実際には、芋た目よりもはるかに簡単です。これは実際の補品開発の問題に関するものであり、産業開発に埓事しおいる堎合は、間違いなくそれを䜿甚するこずから利益を埗る。



特に、補品の機胜芁件が圢匏化されおいる䌁業レベルの䞭芏暡プロゞェクトでは。たずえば、芁件がある堎合がありたす。構成フラグを蚭定するずきに、すべおのパブリックメ゜ッドのすべおの入力パラメヌタをログに蚘録したす。たたは、すべおのプロゞェクトメ゜ッドに぀いお、このメ゜ッドの実行時間の特定のしきい倀を超えたずきにメッセヌゞを送信する通知システムを甚意したす。



これはAOPなしでどのように行われたすかハンマヌで打ち蟌たれ、最も重芁な郚分に察しおのみ実行されるか、新しいメ゜ッドを䜜成するずきに、隣接するメ゜ッドから同様のコヌドをコピヌしお貌り付け、付随するすべおのメ゜ッドを䜿甚したす。



AOPを䜿甚する堎合、プロゞェクトに適甚されおゞョブが完了するず、アドバむスが曞き蟌たれたす。ロゞックを少し曎新する必芁がある堎合は、アドバむスをもう䞀床曎新するず、次のビルドに適甚されたす。 AOPがない堎合、プロゞェクトコヌド党䜓で100,500回の曎新になりたす。



プラスは、そのような機胜が散らばっおいお、コヌドを読むずきに迷惑なノむズのように芋えるため、コヌドがsmallpoxを持っおいる人のように芋えなくなるこずです。



AOPをプロゞェクトに導入した埌、AOPがないず倢にも思わなかったものを実装し始めたす。これは、AOPが比范的小さな利点のように芋え、倧きなコストがかかるためです。 AOPを䜿甚するず、すべおが正反察で、比范的䜎コストで倧きなメリットがありたす同じレベルの努力のコストに察しお。



私の感じでは、アスペクト指向のプログラミングは、Java゚コシステムず比范しお.Net゚コシステムではあたり人気がありたせん。䞻な理由は、Javaの機胜ず品質に匹敵する無料のオヌプン゜ヌスツヌルがないこずだず思いたす。



PostSharpは同様の機胜ず利䟿性を提䟛したすが、プロゞェクトで䜿甚するために数癟ドルを支払うこずをいずわない人は倚くなく、コミュニティバヌゞョンの機胜は非垞に限られおいたす。もちろん、代替手段はありたすが、残念ながら、PostSharpのレベルには達しおいたせん。ツヌルの機胜を比范



できたす比范はPostSharpの所有者によっお行われたこずを芚えおおく必芁がありたすが、ある皋床の状況がわかりたす。



アスペクト指向プログラミングぞの道



私たちは小さなコンサルティング䌚瀟12人であり、私たちの仕事の最終結果は゜ヌスコヌドです。それら。゜ヌスコヌド、品質コヌドを䜜成するために報酬を受け取りたす。私たちは1぀の業界でしか働いおおらず、プロゞェクトの倚くには非垞に類䌌した芁件があり、その結果、゜ヌスコヌドもこれらのプロゞェクト間で非垞に類䌌しおいたす。



たた、リ゜ヌスが限られおいるため、私たちにずっお最も重芁なタスクの1぀は、コヌドを再利甚し、開発者を日垞的なタスクから救うツヌルを䜿甚する機胜です。



これを実珟する方法の1぀は、自動コヌド生成機胜を倚甚し、プロゞェクトずタスクに固有のVisualStudio甚のカスタムプラグむンずアナラむザヌをいく぀か䜜成するこずです。これにより、高いコヌド品質を維持しながら、プログラマヌの生産性を倧幅に向䞊させるこずができたした品質が向䞊したずさえ蚀えたす。



次の論理的なステップは、アスペクト指向のプログラミングの䜿甚を実装するずいうアむデアでした。いく぀かのアプロヌチずツヌルを詊したしたが、結果は私たちの期埅からはほど遠いものでした。これはRoslynテクノロゞヌのリリヌスず同時に行われ、ある時点で、自動コヌド生成ずRoslynの機胜を組み合わせるずいうアむデアがありたした。



わずか数週間で、楜噚のプロトタむプが䜜成され、私たちの気持ちによれば、このアプロヌチはより有望であるように思われたした。このツヌルの䜿甚方法ず曎新方法を䜕床か繰り返した結果、私たちの期埅は満たされ、予想をはるかに超えたず蚀えたす。䟿利なテンプレヌトのラむブラリを開発したした。ほずんどのプロゞェクトでこのアプロヌチを䜿甚しおおり、䞀郚のクラむアントもこれを䜿甚しお、ニヌズに合わせおテンプレヌトの開発を泚文しおいたす。



残念ながら、私たちのツヌルはただ理想からほど遠いので、説明を2぀の郚分に分けたいず思いたす。1぀は理想的な䞖界でのこの機胜の実装の芋方であり、2぀目はここでの方法です。



詳现に移る前に、簡単な説明をしたいず思いたす。この蚘事のすべおの䟋は、無関係な詳现で過負荷になるこずなく、アむデアを瀺すこずができるレベルに簡略化されおいたす。



完璧な䞖界でそれがどのように行われるか



私たちのツヌルを数幎間䜿甚した埌、私は理想的な䞖界に䜏んでいた堎合にこれをどのように機胜させたいかずいうビゞョンを持っおいたす。



理想的な䞖界の私のビゞョンでは、蚀語仕様は゜ヌスコヌド倉換の䜿甚を蚱可し、コンパむラずIDEのサポヌトがありたす。



このアむデアは、C蚀語仕様に「郚分的」修食子を含めるこずに觊発されたした。このかなり単玔な抂念耇数のファむルでクラス、構造、たたはむンタヌフェむスを定矩する機胜により、自動゜ヌスコヌド生成ツヌルのサポヌトが劇的に改善および簡玠化されたした。それら。これは、クラスの゜ヌスコヌドを耇数のファむルに氎平方向に分割するようなものです。 C蚀語を知らない人のために、小さな䟋。



Example1.aspxファむルに蚘述されおいる単玔なフォヌムがあるずしたす。

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


そしお、Example1.aspx.csファむルのカスタムロゞックたずえば、ボタンがクリックされたずきにボタンの色を赀に倉曎する



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


「partial」によっお提䟛される機胜の蚀語での存圚により、ツヌルキットはExample1.aspxファむルを解析し、Example1.aspx.designer.csファむルを自動的に生成できたす。



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


それら。曎新可胜なプログラマヌExample1.aspx.csによっおExamplePage1クラスのコヌドの䞀郚を1぀のファむルに栌玍し、自動生成されたツヌルキットによっおExample1.aspx.designer.csファむルの䞀郚を栌玍するこずができたす。コンパむラにずっお、最終的には1぀の䞀般的なクラスのように芋えたす



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


IMyInterfaceむンタヌフェヌスの継承の定矩を含む䟋を䜿甚するず、最終結果が異なるファむルからのクラス定矩の組み合わせであるこずがわかりたす。



パヌシャルなどの機胜がなく、コンパむラがすべおのクラスコヌドを1぀のファむルにのみ保存する必芁がある堎合、自動生成をサポヌトするために必芁な䞍䟿さず远加のゞェスチャを想定できたす。



したがっお、私の考えは、蚀語仕様に2぀の远加の修食子を含めるこずです。これにより、゜ヌスコヌドにアスペクトを簡単に埋め蟌むこずができたす。



最初の修食子はオリゞナルであり、倉換できるはずのクラス定矩に远加したす。



2番目は凊理され、これが゜ヌス倉換ツヌルによっお取埗された最終的なクラス定矩であり、バむトコヌドを生成するためにコンパむラによっお受け入れられる必芁があるこずを瀺しおいたす。



シヌケンスはこんな感じ



  1. ナヌザヌは、.csファむルに元の修食子を含むクラスの゜ヌスコヌドを操䜜したす䟋Example1.cs
  2. コンパむル時に、コンパむラは゜ヌスコヌドの正確さをチェックし、クラスが正垞にコンパむルされた堎合は、元のコヌドの存圚をチェックしたす。
  3. オリゞナルが存圚する堎合、コンパむラはこのファむルの゜ヌスコヌドを倉換プロセスに枡したすこれはコンパむラのブラックボックスです。
  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 .


それら。これら2぀の修食子を远加するこずで、゜ヌスコヌド生成のサポヌトを郚分的に簡玠化できるように、蚀語レベルで゜ヌスコヌド倉換ツヌルのサポヌトを敎理するこずができたした。それら。 parialは氎平方向のコヌド分割であり、元の/凊理されたものは垂盎方向です。



私が芋おいるように、コンパむラで元の/凊理されたサポヌトを実装するこずは、Microsoftの2人のむンタヌンにずっお1週間の䜜業ですもちろん冗談ですが、真実からそう遠くはありたせん。抂しお、このタスクには基本的な問題はありたせん。コンパむラヌの芳点からは、ファむルの操䜜ずプロセスの呌び出しです。



.NET 5では、新しい機胜が远加されたした-゜ヌスコヌドゞェネレヌタヌこれにより、コンパむル䞭に新しい゜ヌスコヌドファむルを生成できたす。これは正しい方向ぞの動きです。残念ながら、新しい゜ヌスコヌドを生成するこずはできたすが、既存の゜ヌスコヌドを倉曎するこずはできたせん。だから私たちはただ埅っおいたす。



同様のプロセスの䟋。ナヌザヌがファむル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;
  }	
}


コンパむルのために実行され、すべおに問題がなく、コンパむラが元の修食子を確認するず、倉換プロセスに゜ヌスコヌドが枡され、Example2.processed.csファむルが生成されたす最も単玔なケヌスでは、元のファむルが凊理枈みに眮き換えられたExample2.csの正確なコピヌになりたす。 ..。



この堎合、倉換プロセスでロギングアスペクトが远加され、結果は次のようになるず想定したす。

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


次のステップは、眲名を確認するこずです。 _main_眲名は同䞀であり、元の定矩ず凊理された定矩が完党に同じでなければならないずいう条件を満たす。



この䟋では、特別にもう1぀の小さな文を远加したした。これは、メ゜ッド、プロパティ、およびフィヌルドの凊理枈み修食子です。



メ゜ッド、プロパティ、およびフィヌルドを、凊理された修食子を持぀クラスでのみ䜿甚可胜であり、眲名を比范するずきに無芖されるものずしおマヌクしたす。これはアスペクト開発者の䟿宜のために行われ、䞍芁なコヌドの冗長性を䜜成しないように、䞀般的なロゞックを個別のメ゜ッドに移動できたす。



コンパむラはこのコヌドをコンパむルし、すべおが問題ない堎合は、バむトコヌドを䜿甚しおプロセスを続行したした。



この䟋ではいく぀かの単玔化があり、実際にはロゞックがより耇雑になる可胜性があるこずは明らかですたずえば、1぀のクラスに元のクラスず郚分的なクラスの䞡方を含める堎合が、これは克服できない耇雑さではありたせん。



完璧な䞖界での基本的なIDE機胜



IDEでの.processed.csファむルの゜ヌスコヌドの操䜜のサポヌトは、䞻に、元の/凊理されたクラスずステップバむステップのデバッグ䞭の遷移ずの間の正しいナビゲヌションにありたす。



IDEの2番目に重芁な機胜私の芳点からは、凊理されたクラスのコヌドの読み取りを支揎するこずです。 Processedクラスには、いく぀かの偎面によっお远加された倚くのコヌドを含めるこずができたす。グラフィック゚ディタのレむダヌの抂念に䌌たディスプレむの実装は、この目暙を達成するための最も䟿利なオプションのように思われたす。私たちの珟圚のプラグむンは同様のものを実装しおおり、そのナヌザヌからの反応は非垞に肯定的です。



AOPを日垞生掻に導入するのに圹立぀もう1぀の機胜は、リファクタリング機胜です。ナヌザヌがコヌドの䞀郚を匷調衚瀺するず、「Extract To AOP Template」ず蚀うこずができ、IDEは正しいファむルを䜜成し、初期コヌドを生成し、プロゞェクトコヌドを分析した埌、他のクラスのテンプレヌトを䜿甚する候補を提案したした。



さお、ケヌキの䞊のアむシングは、アスペクトテンプレヌトを䜜成するためのサポヌトになりたす。たずえば、遞択したクラス/メ゜ッドにアスペクトをむンタラクティブに適甚しお、明瀺的なコンパむルサむクルなしで、その堎で最終結果を評䟡できたす。



リシャヌパヌの䜜成者が事業を匕き継ぐず、魔法が保蚌されるず確信しおいたす。



完璧な䞖界でアスペクトコヌドを曞く



TRIZを蚀い換えるず、アスペクトを実装するための理想的なコヌドの蚘述は、むンストルメンテヌションプロセスをサポヌトするためだけに存圚する远加のコヌドを蚘述しないこずです。



理想的な䞖界では、その目暙を達成するためのヘルパヌロゞックを䜜成する手間をかけずに、アスペクト自䜓をコヌディングしたいず考えおいたす。そしお、このコヌドはプロゞェクト自䜓の䞍可欠な郚分になりたす。



2番目の芁望は、むンタラクティブなプラグアンドプレむを実珟できるこずです。テンプレヌトを䜜成したら、それを倉換に䜿甚するために远加の手順を実行する必芁はありたせん。ツヌルを再コンパむルしたり、゚ラヌをキャッチしたりする必芁はありたせんでした。たた、コンパむル埌のプロゞェクトでオプションを構成したす。



テンプレヌトを䜜成しお数行曞くず、すぐに結果が衚瀺されたす。゚ラヌが含たれおいる堎合、それらの怜出ずデバッグはテンプレヌトを適甚するプロセスに統合され、プログラマヌの远加の䜜業を必芁ずする別個の郚分ではありたせん。



テンプレヌトの構文がC蚀語の構文にできるだけ近くなるように、理想的にはマむナヌなアドオンに加えお、いく぀かのキヌワヌドずプレヌスホルダヌを远加したす。



珟圚の実装



残念ながら、私たちは理想的な䞖界に䜏んでいないので、自転車を再発明しお乗る必芁がありたす。



コヌドむンゞェクション、コンパむル、デバッグ



珟圚のモデルは、プロゞェクトの2぀のコピヌを䜜成するこずです。1぀はオリゞナルのもので、プログラマヌが䜿甚するもの、もう1぀は倉換されたもので、コンパむルず実行に䜿甚されたす。



シナリオはこんな感じ



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


デバッグのために、IDEの2番目のコピヌが起動され、プロゞェクトの囜別のコピヌが開かれ、倉換が適甚されたコピヌで機胜したす。



このプロセスには特定の芏埋が必芁ですが、時々それが習慣になり、堎合によっおはこのアプロヌチにはいく぀かの利点がありたすたずえば、ロヌカルマシンで䜜業する代わりに、ビルドを起動しおリモヌトサヌバヌにデプロむできたす。さらに、VisualStudioのプラグむンのヘルプにより、プロセスが簡玠化されたす。



IDE



特定のタスクずプロセスに合わせお調敎されたプラグむンを䜿甚しおおり、゜ヌスコヌドの実装のサポヌトはその機胜のかなり小さな郚分です。



たずえば、グラフィカル゚ディタに䌌たスタむルでレむダヌを衚瀺する機胜を䜿甚するず、たずえば、スコヌプたずえば、パブリックメ゜ッドのみが衚瀺されるようにする、領域ごずにコメントレむダヌを非衚瀺/衚瀺できたす。埋め蟌たれたコヌドは特別な圢匏のコメントで囲たれおおり、別のレむダヌずしお非衚瀺にするこずもできたす。



別の可胜性は、元のファむルず倉換されたファむルの違いを瀺すこずです。 IDEはプロゞェクト内のファむルのコピヌの盞察的な堎所を知っおいるため、元のファむルず囜で生成されたファむルの違いを衚瀺できたす。



たた、プラグむンは、囜で生成されたコピヌに倉曎を加えようずするず譊告したすその埌の再倉換䞭にそれらが倱われないようにするため



構成



別のタスクは、倉換ルヌルを蚭定するこずです。どのクラスずメ゜ッドに倉換を適甚したすか。



いく぀かのレベルを䜿甚したす。



最初のレベルは、最䞊䜍の構成ファむルです。ファむルシステムのパス、ファむル名のパタヌン、クラスたたはメ゜ッド、クラスのスコヌプ、メ゜ッドたたはプロパティに応じおルヌルを蚭定できたす。



2番目のレベルは、クラス、メ゜ッド、たたはフィヌルドの属性のレベルでの倉換ルヌルの適甚を瀺したす。



コヌドブロックのレベルでの3番目ず4番目は、゜ヌスコヌドの特定の堎所でのテンプレヌトの倉換の結果を含めるこずを明瀺的に瀺しおいたす。



テンプレヌト



歎史的に、自動生成の目的で、T4圢匏のテンプレヌトを䜿甚するため、倉換甚のテンプレヌトず同じアプロヌチを䜿甚するこずは非垞に論理的でした。T4テンプレヌトには、任意のCコヌドを実行する機胜が含たれおおり、オヌバヌヘッドが最小限で、衚珟力が優れおいたす。



T4を䜿甚したこずがない人にずっお、最も簡単な類䌌点はASPX圢匏を提瀺するこずです。これは、HTMLの代わりにCで゜ヌスコヌドを生成し、IISではなく、結果をコン゜ヌルたたはファむルに出力する別個のナヌティリティずしお実行されたす。



の䟋



これが実際にどのように機胜するかを理解するために、最も簡単なこずは、倉換の前埌のコヌドず、倉換䞭に䜿甚されるテンプレヌトの゜ヌスコヌドを瀺すこずです。最も簡単なオプションを瀺したすが、可胜性はあなたの想像力によっおのみ制限されたす。



倉換前のサンプル゜ヌスコヌド
// ##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}";
            }
        }
    }
}




倉換埌の゜ヌスコヌドのフルバヌゞョン
//------------------------------------------------------------------------------
// <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







説明ずテンプレヌトコヌド



自動コメントテンプレヌト



// ##aspect=AutoComment


゜ヌスコヌドで特別な圢匏のコメントが芋぀かった堎合は、指定されたテンプレヌトこの堎合はAutoCommentを実行し、このコメントの代わりに倉換結果を挿入したす。この䟋では、このファむルのコヌドが倉換の結果であり、このファむルを盎接倉曎するこずは意味がないこずをプログラマヌに譊告する特別な免責事項を自動的に挿入するこずは理にかなっおいたす。



テンプレヌトコヌド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>
//------------------------------------------------------------------------------


倉数FileName、FileSha256、User、MachineName、およびNowは、倉換プロセスからテンプレヌトに゚クスポヌトされたす。



倉換結果



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


次の倉換は、クラスの属性ずしお指定されたす



[AopTemplate "ClassLevelTemplateForMethods"、NameFilter = "First"]



この属性は、テンプレヌトを「First」ずいう単語を含むすべおのクラスメ゜ッドに適甚する必芁があるこずを瀺したす。NameFilterパラメヌタヌは、倉換に含めるメ゜ッドを決定するために䜿甚される通垞の匏パタヌンです。



テンプレヌトコヌドClassLevelTemplateForMethods.t4



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

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


これは、// class level templateメ゜ッドコヌドの



倉換結果の前にコメントを远加する最も簡単な䟋です。



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


次の倉換は、同じメ゜ッドに適甚される耇数の倉換を瀺すために、メ゜ッド属性ずしお指定されおいたす。 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テンプレヌト

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

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

<#= MethodEnd() #>


倉換の結果

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


次の倉換は、usingコンストラクトに限定されたブロックに察しお行われたす。



using (new AopTemplate("SecondDemoUsing", extraTag: "test extra"))
{
    customers = people.Select(s => new Customer()
    {
        FirstName = s.FirstName,
        LastName = s.LastName,
        Age = s.Age,
    });

    foreach (var customer in customers)
    {
        Console.Out.WriteLine($"SecondDemo: 2 {customer.FirstName} {customer.LastName}");
    }
}


SecondDemoUsing.t4テンプレヌト

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

// second demo <#= ExtraTag #>

<#= StatementBody() #>


ExtraTagは、パラメヌタヌずしお枡される文字列です。これは、入力パラメヌタに応じお動䜜がわずかに異なる可胜性があるゞェネリックに圹立ちたす。



倉換結果



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


次の倉換は、 NotifyPropertyChangedクラスの属性によっお指定されたす。これは叀兞的な䟋であり、ログの䟋ずずもに、アスペクト指向プログラミングのほずんどの䟋で瀺されおいたす。



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

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








クラスコヌドに適甚されるNotifyPropertyChangedClass.t4テンプレヌト
<#@ 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


クラスプロパティに適甚されるNotifyPropertyChanged.t4テンプレヌト
<#@ 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();
			}
		}
	}


クラスずプロパティの元のコヌド

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

// ...
}


倉換結果

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

// ...
}


プロパティの結果をキャッシュするためのテンプレヌトの䟋



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



。テンプレヌトパラメヌタで指定され、JSON属性ずしお指定されたす。明瀺的なパラメヌタヌがない堎合は、デフォルトのパラメヌタヌが䜿甚されたす。



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

	}


゜ヌス

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


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


コメントからのテンプレヌトぞの次の呌び出し

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


FullNameComment.t4テンプレヌト

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

// FullNameComment <#= PropertyNode.Identifier #>


AutoComment.t4テンプレヌトず非垞に䌌おいたすが、ここではPropertyNodeの䜿甚法を瀺したす。たた、「ここにある远加デヌタ」デヌタは、ExtraTagパラメヌタヌを介しおFullNameComment.t4テンプレヌトで䜿甚できたすただし、この䟋では䜿甚しないため、単に無芖されたす



倉換結果

// FullNameComment FullName






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



AND 属性で指定されるファむル内の次の倉換は、Personクラスの倉換ず同じです。NotifyPropertyChanged.t4テンプレヌトの゜ヌスコヌドはすでに䞊蚘に含たれおいたす。



倉換結果

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

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


最埌の郚分



この蚘事ではアスペクト指向のプログラミングに焊点を圓おおいたすが、゜ヌスコヌド倉換手法は普遍的であり、原則ずしお、AOPに関連しないタスクに䜿甚できたす。



たずえば、䟝存関係の泚入に䜿甚できたす。ビルドパラメヌタに応じおリ゜ヌス䜜成コヌドを倉曎したす。



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

#>




゜ヌスコヌドここでは動的倉数機胜が䜿甚されおおり、任意のタむプに割り圓おるこずができたす、぀たり 衚珟力を高めるために、新しいキヌワヌドを考え出したした。

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;


倉換䞭に、比范GetGlobalSetting "env"== "test"が䜿甚され、この条件に応じお、新しいDataServiceたたは新しいMockDataServiceのいずれかが挿入されたす。



倉換結果


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


たたは、このツヌルを「貧乏人」の静的分析ずしお䜿甚するこずもできたすただし、Roslynのネむティブ機胜を䜿甚しおアナラむザヌを実装する方がはるかに正確です。ルヌルのコヌドを分析し、゜ヌスコヌドに挿入したす。



#error our error message here



これにより、コンパむル時゚ラヌが発生したす。



#warning our warning message here



これは、IDEたたはコンパむル時に譊告ずしお機胜したす。



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




倉換結果

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


たたは、アプリケヌションをロヌカラむズするための自動ツヌルずしお、぀たり クラス内のすべおの文字列を芋぀けお、適切なリ゜ヌスを䜿甚しおそれらを眮き換えたす。



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




゜ヌス


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;


たずえば、リ゜ヌスファむルDemo.resxには、次の行を䜜成したした。

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


および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);
        }
    }
}


倉換結果補間された文字列がString.Formatに眮き換えられ、「名{0}最埌の名前{1}」リ゜ヌスが䜿甚されたこずに泚意しおください。リ゜ヌスファむルに存圚しない行、たたはフォヌマットず䞀臎しない行の堎合、゚ラヌメッセヌゞが远加されたす

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


さらに、倉換ツヌルを䜿甚するず、Cファむルだけでなく、任意のファむルタむプもちろん、特定の制限付きを操䜜できたす。ご䜿甚の蚀語のASTを構築できるパヌサヌがある堎合は、Roslynをこのパヌサヌに眮き換え、コヌドハンドラヌの実装を埮調敎するず、機胜したす。残念ながら、Roslynに近い機胜を備えたラむブラリの数は非垞に限られおおり、それらを䜿甚するにはさらに倚くの劎力が必芁です。 Cに加えお、JavaScriptおよびTypeScriptプロゞェクトには倉換を䜿甚したすが、Cほど包括的ではありたせん。



繰り返しになりたすが、䟋ずテンプレヌトのコヌドは、そのようなアプロヌチの可胜性を説明するために提䟛されおおり、圌らが蚀うように、空が限界です。



埡時間ありがずうございたす。



この蚘事の䞻芁郚分は数幎前に曞かれたしたが、残念ながら、特定の理由により、珟圚しか公開できたせんでした。



元のツヌルは.NetFrameworkで開発されたしたが、.NetCoreのMITラむセンスの䞋で簡略化されたオヌプン゜ヌスバヌゞョンの䜜業を開始したした。珟時点では、結果は完党に機胜し、90の準備ができおおり、マむナヌな改善、コヌドのヘアスタむル、ドキュメントず䟋の䜜成がありたすが、これらすべおがないずプロゞェクトに参加するのが難しく、アむデア自䜓が損なわれ、DXはネガティブになりたす。



制䜜に携わった人は、他の䌚瀟に移る前に完成させるこずができなかったので、リ゜ヌスを割り圓おお䜜業を続ける前に、コミュニティの反応を芋おみたいず思いたす。このニッチは、開発ぞの代替ツヌルたたはアプロヌチによっお埋められおいるこず。



ツヌルのアむデア自䜓は非垞にシンプルで、開発者は実行可胜なバヌゞョンの実装に合蚈玄1か月を費やしたので、Roslynの優れた資栌ず経隓を持぀プログラマヌは、数日で独自の特定のバヌゞョンを䜜成できるず思いたす。珟時点では、プロゞェクトの゜ヌスコヌドのサむズは、䟋ずテンプレヌトを含めお玄150KBです。



私は建蚭的な批刀を受けおうれしいです非建蚭的な批刀も私を動揺させないので、躊躇しないでください。



Phil Ranginに感謝したすfillpackart蚘事を曞く動機付けのため。チャンネル「WeAreDoomed」ルヌル



All Articles