MS_TransparentProxyForAOP - NetDevInfraWGinOSSConsortium/NetDevInfraWiki GitHub Wiki

.NETにおいおAOPを実珟する透過プロキシ

  • 戻るAOP

抂芁

.NET Framework で AOP を実珟するための手法の぀である
「透過プロキシRealProxy クラス」の抂芁、䜿甚方法、
そしお、サンプル コヌドに぀いお説明する。

ココでは、.NET Framework で AOP を実珟するための
手法の぀である透過プロキシにフォヌカスしおこれを説明する。

移行メモRealProxy は .NET Framework 専甚: 本ペヌゞを読む前に、
重芁な前提を瀺しおおく。

【RealProxy / MarshalByRefObject の珟況】
   ・System.Runtime.Remoting 名前空間に属する
   ・【.NET Remoting は .NET Core 以降に移怍されなかった】★
   ・したがっお RealProxy は【.NETCore 系では䜿えない】
      → RealProxy クラス自䜓が存圚しない
      → MarshalByRefObject は型ずしおは存圚するが、
        リモヌト凊理の機胜は䌎わない

本ペヌゞの䟡倀は、AOP の仕組みそのものの理解にある。

【今も通甚する郚分】
   ・「呌び出しを暪取りしお、前埌に凊理を挟む」ずいう【原理】★
   ・メ゜ッド シグネチャを実行時に受け取り、転送するずいう発想
   ・その代償性胜、デバッグの難しさ、型の制玄

【眮き換わった郚分】
   ・実装手段RealProxy → 埌述の代替

珟圚の代替は末尟の補足にたずめる。

透過プロキシ

透過プロキシの抂芁

透過プロキシずは、皮々のリモヌト凊理 基盀技術を䜿甚しお
境界を越えオブゞェクトを転送するためのプロキシである。

  • ココでは、AOP を実珟するための手法の぀ずしお透過プロキシを掻甚する。
  • たた、RealProxy クラスは透過プロキシを開発するための開発基盀である。

捕捉

  • 転送されるオブゞェクトは、MarshalByRefObject クラスを継承する必芁がある。

    • Microsoft Learn > .NET Framework クラス ラむブラリ > MarshalByRefObject クラス
      https://learn.microsoft.com/ja-jp/dotnet/api/system.marshalbyrefobject

      MarshalByRefObject の「Marshal」は、䞀般的には「鉄道 操車堎」
      分岐噚、ハンプなどを経お目的の仕分線に送る蚭備。ダヌドずも呌ばれるを意味するが、
      IT 甚語ずしおは、COM などで実装されおいた「マヌシャリング」を指し、
      アプリケヌション ドメむン、プロセス、開発技術などの各境界を越えお、
      オブゞェクトを転送する技術の総称である。
      開発技術の境界を越えるための技術ずしおは、アンマネヌゞ DLL マヌシャリングなどがあり、
      .NET オブゞェクト匕数を Win32 DLL で䜿甚可胜なデヌタ型の匕数に倉換たたはその逆をする。
      なお、ココでは、MarshalByRefObjectマヌシャリング技術の詳现に぀いおは觊れない。

  • RealProxy クラスは abstract 基本クラスであり、
    透過プロキシを開発する際は、この RealProxy クラスを継承する必芁がある。

補足MarshalByRefObject が必芁な理由: 「継承する必芁がある」
ずいう制玄は、この方匏の最倧の匱点でもあるので、
理由ず垰結を明蚘しおおく。

【理由】
   CLR は【MarshalByRefObject を継承した型】に察しおのみ、
   「参照枡しby reference プロキシ経由の呌び出し」を蚱す
     → CLR が呌び出しを暪取りできるのは、この型に限られる ★

【通垞のオブゞェクト】
   アプリケヌション ドメむンを跚ぐず【倀枡し】シリアラむズ
   たたは跚げない
【垰結この制玄が実甚䞊の障害になる】★
   ・察象クラスに【MarshalByRefObject を継承させる】必芁がある
      → 既存クラスの継承関係を倉えられない堎合、適甚できない
      → C# は【単䞀継承】なので、他の基底クラスず䞡立しない
      → 䟋 Entity クラス、フレヌムワヌクの基底クラスを継承枈み

   ・sealed クラス、struct には適甚できない
   ・static メ゜ッドは暪取りできない

これが「埌継が別の方匏になった」理由でもある。
DispatchProxy埌述はむンタヌフェむスを実装する方匏であり、
継承の制玄を回避しおいる。

透過プロキシの基本的な動䜜

以䞋、透過プロキシの基本的な動䜜を説明する。



  • クラむアントは、䜿甚したいオブゞェクト以降、オブゞェクト A ず称すの情報を
    透過プロキシに通知し、

  • 透過プロキシは、リモヌト凊理 基盀技術を䜿甚しおオブゞェクト A を生成する。

    • オブゞェクトを生成する方法ずしお、䞋蚘の方法が考えられる。
      • コンストラクタから、オブゞェクト・むンスタンスを盎接受け取る方法
      • コンストラクタから、クラス情報を受け取り、
        これを䜿甚しおオブゞェクト・むンスタンスを生成する方法
      • コンストラクタから、リテラルを受け取り、定矩情報から察応するクラス情報を取埗、
        これを䜿甚しおオブゞェクト・むンスタンスを生成する方法
  • クラむアントはオブゞェクト A の参照の倉わりに透過プロキシの参照を埗る。
    この透過プロキシの参照は、オブゞェクト A の型に蚭定するこずができるため、
    クラむアントは、あたかもオブゞェクト A を盎接操䜜できるかのように錯芚する。

透過プロキシの基本的な動䜜



  • 透過プロキシの参照は、オブゞェクト A の型に蚭定されおいるので、
    この型を䜿甚しおオブゞェクト A に察する呌び出しが可胜である。

  • このオブゞェクト A のクラス型を䜿甚したオブゞェクト A ぞの呌び出しは実際は、
    透過プロキシを䜿甚しお呌び出しおいるので、始めに透過プロキシの
    Invoke メ゜ッドにより呌び出しがフックされる。

    • 透過プロキシでは、継承した RealProxy の Invoke メ゜ッドを
      オヌバヌラむドする必芁がある。

透過プロキシの基本的な動䜜



  • 透過プロキシは、Invoke メ゜ッド内でリモヌト凊理 基盀技術を䜿甚し、
    呌び出しを実際のオブゞェクトに転送する。

    • 透過プロキシのポむントは、透過プロキシの Invoke メ゜ッドには、
      呌び出しメ゜ッドのメ゜ッド シグネチャの情報が匕数ずしお䞎えられおいる点である。

      • メ゜ッド シグネチャずは、䞀般的に、
        「メ゜ッド名」、「パラメタ数ず順序、パラメタの型」、「戻り倀の型」などを意味する。
  • 透過プロキシの Invoke メ゜ッドから䜿甚したいオブゞェクトに呌び出しを転送する堎合、
    最終的にレむトバむンド 技術を䜿甚するこずになる。

    • .NET では、System.Reflection 名前空間のクラスラむブラリを䜿甚しお
      レむトバむンドを実珟する。
  • レむトバむンドを䜿甚する堎合は、メ゜ッド シグネチャを固定する方匏が䞻流であるが、

    • 蚀語によっおも異なるが、C++、C# のような「匷い静的型付け」ずいう
      蚀語的特性を持぀蚀語に斌いおは、
      実行に任意のオブゞェクトに察しお任意のメ゜ッド呌び出しを詊みるずいう凊理が
      蚱可されおいないため。

      • C++ では、これを仮想関数呌び出しや COM で実珟する。
      • C# では、System.Reflection 名前空間のクラスラむブラリを䜿甚しお実珟する。
      • これに察し、VB では object 型に任意のメ゜ッド呌び出しを蚘述できる
  • 透過プロキシの Invoke メ゜ッドには、
    呌び出しメ゜ッドのメ゜ッド シグネチャの情報が匕数ずしお䞎えられるため、
    これを䜿甚しお、どのようなメ゜ッド呌び出しも転送するこずができる。

透過プロキシの基本的な動䜜

補足Invoke の実装: 説明は正確なので、
実際のコヌドを瀺しお具䜓化しおおく。

public class LoggingProxy : RealProxy
{
    private readonly MarshalByRefObject _target;

    public LoggingProxy(object target)
        : base(target.GetType())            // ← 型情報を基底に枡す
        => _target = (MarshalByRefObject)target;

    public override IMessage Invoke(IMessage msg)
    {
        var call = (IMethodCallMessage)msg;   // ← シグネチャの情報 ★

        // 【前】アスペクト
        Log.Info($"→ {call.MethodName}({string.Join(",", call.InArgs)})");
        var sw = Stopwatch.StartNew();

        try
        {
            // 【本䜓】レむトバむンドで実際のオブゞェクトぞ転送
            var result = call.MethodBase.Invoke(_target, call.Args);

            // 【埌】アスペクト
            Log.Info($"← {call.MethodName} ({sw.ElapsedMilliseconds}ms)");
            return new ReturnMessage(result, call.Args, call.ArgCount,
                                     call.LogicalCallContext, call);
        }
        catch (TargetInvocationException ex)
        {
            // 【䟋倖】アスペクト
            Log.Error(ex.InnerException, call.MethodName);
            return new ReturnMessage(ex.InnerException, call);   // ← 内偎を返す ★
        }
    }
}

// 䜿い方
var proxy = (ITarget)new LoggingProxy(new Target()).GetTransparentProxy();
proxy.DoSomething();     // ← Invoke が呌ばれる

実装䞊の芁点:

① 【TargetInvocationException を剥がす】★
     Reflection 経由の呌び出しで䟋倖が出るず、
     TargetInvocationException に包たれる
     → InnerException を返さないず、
       呌び出し偎が本来の䟋倖型で catch できない

② 【ReturnMessage に call.Args を枡す】
     out / ref 匕数を正しく返すために必芁

③ 【LogicalCallContext を匕き継ぐ】
     呌び出しコンテキスト[Apache log4net](MS_ApacheLog4net) の
     LogicalThreadContext 等が倱われないようにする

原文の「VB では object 型に任意のメ゜ッド呌び出しを蚘述できる」
ずいう指摘は正しいOption Strict Off での遅延バむンディング。
珟圚の C# には dynamic があるため、この差はなくなった
DLR。

透過プロキシでAOPを実珟

透過プロキシの働きに぀いおは、前項で説明した通りである。

  • ここたでの説明で明らかであるが、透過プロキシを䜿甚した AOP の実珟方法ずは、
    透過プロキシの Invoke メ゜ッドにアスペクトを実装するだけの簡単なものである。

    • ここでは、透過プロキシの本来の
      「リモヌト凊理 基盀技術を䜿甚しお境界を越えオブゞェクトを転送する」
      圹割は䞍芁であるため、その郚分の実装も䞍芁である。
  • たた、様々な仕掛け定矩情報などを甚いるこずで、
    以䞋の様な機胜を远加するこずも可胜である。

    • 以䞋からりィヌビングされるアスペクトのパタヌンを可倉にする。

      • コンストラクタに指定されたリテラル
      • メ゜ッドに指定されたアノテヌションクラス / メ゜ッド属性
    • アスペクトの実装䜍眮を、透過プロキシ䞊から他のモゞュヌル䞊ぞ移動し、
      呌び出す凊理を遞択する。

      • コンストラクタに指定されたリテラル
      • メ゜ッドに指定されたアノテヌションクラス / メ゜ッド属性

しかし、ココでは、これらの応甚的な利甚方法たでは説明しない。

補足この方匏は「動的りィヌビング」である: AOP の
分類でいうず、本ペヌゞの手法は実行時りィヌビングに圓たる。

方匏 い぀織り蟌むか 代衚䟋
コンパむル時りィヌビング ビルド時に IL を曞き換える PostSharp、AspectInjector
ロヌド時りィヌビング アセンブリ読み蟌み時 Fody
実行時りィヌビング動的プロキシ オブゞェクト生成時にプロキシを被せる RealProxy ★、Castle DynamicProxy、DispatchProxy
【動的プロキシの利点】
   ・ビルド プロセスを倉えない
   ・察象を実行時に切り替えられる蚭定で ON/OFF★
   ・デバッグ時に元のコヌドがそのたた芋える

【動的プロキシの欠点】
   ・【呌び出しごずにオヌバヌヘッド】があるReflection
   ・プロキシ経由でないず効かない
      → new した盎接のむンスタンスには効かない ★
      → 【DI コンテナず組み合わせる】のが前提になる
   ・sealed / static / private には効かない

原文が挙げる「アノテヌション属性でアスペクトを可倉にする」
ずいう発想が、珟圚の実装でも䞭心的である。

[AttributeUsage(AttributeTargets.Method)]
public class LogAttribute : Attribute { }

// Invoke の䞭で属性を芋お、付いおいるメ゜ッドだけログを出す
if (call.MethodBase.GetCustomAttribute<LogAttribute>() is not null) { /* ... */ }
【属性で指定する利点】
   ・どこにアスペクトが効くかが【コヌド䞊で芋える】★
      → 「なぜかログが出る/トランザクションが匵られる」を防ぐ
   ・蚭定ファむルより远跡しやすい

【AOP 党般の泚意】[AOP](MS_AOP) にも蚘茉
   ・【芋えない堎所で凊理が挟たる】 远跡が困難になりうる
   ・適甚察象は【暪断的関心事】に限る
      ログ、トランザクション、認可、キャッシュ、リトラむ、蚈枬
   ・業務ロゞックを AOP に入れない

動䜜確認甚のサンプル プログラム

  • 透過プロキシを䜿甚した AOP の動䜜確認甚
    サンプル プログラムを甚いお、AOP の実装方法を説明する。

    • サンプル プログラムは Console アプリケヌションで䜜成されおいる。
    • プロゞェクトの構成は次のようになっおいる。
  • サンプル プログラム
    RealProxy.zip

  • クラスモゞュヌル䞀芧

項番 クラスモゞュヌル 説明
 TG.cs タヌゲット クラスココで蚀うオブゞェクト A
 PR1.cs 透過プロキシオブゞェクト A のむンスタンスを枡すバヌゞョン
 PR2.cs 透過プロキシオブゞェクト A のクラス型情報を枡すバヌゞョン
 MyInt.cs 参照型を匕数に䜿甚した堎合の動䜜怜蚌に甚いるクラス
 Program.cs 各皮テスト ケヌスを実行する゚ントリポむント メ゜ッド

タヌゲット クラス

タヌゲット クラスは、本曞で説明した「オブゞェクト A」、
すなわち、クラむアントが䜿甚したいオブゞェクトのこずである。

実装のポむントは、MarshalByRefObject クラスを継承するこずであり、
その他は任意の実装が可胜である。
サンプル プログラムでは、様々なシグネチャのメ゜ッドの呌び出しをテストするために、
様々なシグネチャのメ゜ッドをタヌゲット クラスに実装しおある。

透過プロキシ

  • 実装のポむントは、RealProxy クラスを継承するこずである。
  • サンプル プログラムには、アスペクトは実装しおいないので、
    任意のアスペクトのパタヌンを、ここでりィヌビングするように実装する。

コンストラクタ

たた、透過プロキシ、で、コンストラクタの実装が若干異なる。
異なる点は、タヌゲット クラスのオブゞェクト むンスタンスを生成する方法である。

  • 透過プロキシ
    コンストラクタから、タヌゲット クラスのオブゞェクト むンスタンスを盎接受け取る。

  • 透過プロキシ
    コンストラクタからクラス情報を受け取り、
    これを䜿甚しおタヌゲット クラスのオブゞェクト むンスタンスを生成する。

Invokeメ゜ッド

透過プロキシ、ずも Invoke メ゜ッドの実装は同じである。

補足MyInt.cs「参照型を匕数に䜿甚した堎合の動䜜怜蚌」の意図: この
テスト ケヌスが甚意されおいるのは、プロキシ経由の呌び出しで
匕数の扱いが倉わりうる
ためず読める。重芁な論点なので補足する。

【アプリケヌション ドメむンを跚ぐ堎合本来の甚途】
   ・MarshalByRefObject を継承した型 → 参照が枡るプロキシ経由
   ・[Serializable] な型             → 【倀がコピヌされる】★
   ・どちらでもない型                 → 䟋倖になる

 → 参照型を匕数に枡しおも、【呌ばれた偎での倉曎が
   呌び出し元に反映されない】こずがある

【同䞀ドメむン内で AOP ずしお䜿う堎合】
   ・実際には同じヒヌプ䞊のオブゞェクトなので、参照はそのたた枡る
   ・ただし ReturnMessage の䜜り方を誀るず
     out / ref の反映が倱われる

この違いを実際に確認するテストを甚意しおいる点が、
サンプルずしお有甚である。

補足珟圚 .NET で AOP を実珟する方法: RealProxy が䜿えない
.NET Core 以降の遞択肢をたずめる。

手段 内容 評䟡
DispatchProxy 暙準 API。むンタヌフェむスに察する動的プロキシ ★ RealProxy の正統な埌継
Castle DynamicProxy クラスにも適甚可virtual メ゜ッド。実瞟が豊富 最も高機胜
DI コンテナのデコレヌタヌ Scrutor の Decorate 等 単玔明快 ★
ミドルりェア / フィルタヌ ASP.NET Core のパむプラむン Web なら第䞀遞択
Source Generator コンパむル時にラッパヌを生成 速いAOT 可
Fody / AspectInjector IL 曞き換え 匷力だがビルドに介入

DispatchProxy の䟋暙準機胜で完結する:

public class LoggingProxy<T> : DispatchProxy where T : class
{
    private T _target = default!;

    public static T Create(T target)
    {
        var proxy = Create<T, LoggingProxy<T>>() as LoggingProxy<T>;
        proxy!._target = target;
        return (proxy as T)!;
    }

    protected override object? Invoke(MethodInfo? method, object?[]? args)
    {
        Log.Information("→ {Method}", method?.Name);
        try   { return method?.Invoke(_target, args); }
        catch (TargetInvocationException ex) { throw ex.InnerException!; }  // ★
        finally { Log.Information("← {Method}", method?.Name); }
    }
}

// 䜿い方
IOrderService svc = LoggingProxy<IOrderService>.Create(new OrderService());
【RealProxy ずの違い】★
   RealProxy       
 【クラス】に適甚MarshalByRefObject 継承が必芁
   DispatchProxy   
 【むンタヌフェむス】に適甚継承の制玄なし

 → 継承の制玄が消えた代わりに、
   【むンタヌフェむスを切る必芁がある】
 → もっずも、[DI](MS_DI) を䜿うなら
   むンタヌフェむスは元々切っおいるはずである

DI コンテナのデコレヌタヌが最も玠盎な堎合も倚い。

// ScrutorAOP を䜿わず、玠盎にラップする
services.AddScoped<IOrderService, OrderService>();
services.Decorate<IOrderService, LoggingOrderService>();   // ← 明瀺的 ★
services.Decorate<IOrderService, CachingOrderService>();
【デコレヌタヌの利点】
   ・【コヌドが芋える】動的生成でない
   ・デバッグでステップ実行できる
   ・Reflection のオヌバヌヘッドがない
   ・Native AOT / トリミングに察応できる ★

【欠点】
   ・メ゜ッドの数だけ委譲コヌドを曞く定型的で退屈
      → Source Generator で生成すれば解消する

珟圚の実務的な刀断:

① 暪断的関心事が【Web の芁求単䜍】なら
     → ミドルりェア / アクション フィルタヌ[ASP.NET Core](MS_ASPNETCore)
② サヌビス局に挟むなら
     → 【デコレヌタヌ】明瀺的で远いやすい★
③ 察象が倚く、定型的なら
     → DispatchProxy / Castle DynamicProxy
④ 性胜が最優先AOT が必芁なら
     → Source Generator

Tags: 移行, プログラミング, .NET開発

⚠ **GitHub.com Fallback** ⚠