Skip to content

MS_TransparentProxyForAOP

nishi_74322014 edited this page Aug 21, 2026 · 1 revision

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

  • 戻る(AOP

概要

.NET Framework で AOP を実現するための手法の1つである
「透過プロキシ(RealProxy クラス)」の概要、使用方法、
そして、サンプル コードについて説明する。

ココでは、.NET Framework で AOP を実現するための
手法の1つである透過プロキシにフォーカスしてこれを説明する。

移行メモ(RealProxy は .NET Framework 専用): 本ページを読む前に、
重要な前提を示しておく。

【RealProxy / MarshalByRefObject の現況】
   ・System.Runtime.Remoting 名前空間に属する
   ・【.NET Remoting は .NET Core 以降に移植されなかった】★
   ・したがって RealProxy は【.NET(Core 系)では使えない】
      → RealProxy クラス自体が存在しない
      → MarshalByRefObject は型としては存在するが、
        リモート処理の機能は伴わない

本ページの価値は、AOP の仕組みそのものの理解にある。

【今も通用する部分】
   ・「呼び出しを横取りして、前後に処理を挟む」という【原理】★
   ・メソッド シグネチャを実行時に受け取り、転送するという発想
   ・その代償(性能、デバッグの難しさ、型の制約)

【置き換わった部分】
   ・実装手段(RealProxy → 後述の代替)

現在の代替は末尾の補足にまとめる。

透過プロキシ

透過プロキシの概要

透過プロキシとは、種々のリモート処理 基盤技術を使用して
境界を越えオブジェクトを転送するためのプロキシである。

  • ココでは、AOP を実現するための手法の1つとして透過プロキシを活用する。
  • また、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(後述)はインターフェイスを実装する方式であり、
継承の制約を回避している。

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

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

(1)

  • クライアントは、使用したいオブジェクト(以降、オブジェクト A と称す)の情報を
    透過プロキシに通知し、

  • 透過プロキシは、リモート処理 基盤技術を使用してオブジェクト A を生成する。

    • オブジェクトを生成する方法として、下記の方法が考えられる。
      • コンストラクタから、オブジェクト・インスタンスを直接受け取る方法
      • コンストラクタから、クラス情報を受け取り、
        これを使用してオブジェクト・インスタンスを生成する方法
      • コンストラクタから、リテラルを受け取り、定義情報から対応するクラス情報を取得、
        これを使用してオブジェクト・インスタンスを生成する方法
  • クライアントはオブジェクト A の参照の変わりに透過プロキシの参照を得る。
    この透過プロキシの参照は、オブジェクト A の型に設定することができるため、
    クライアントは、あたかもオブジェクト A を直接操作できるかのように錯覚する。

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

(2)

  • 透過プロキシの参照は、オブジェクト A の型に設定されているので、
    この型を使用してオブジェクト A に対する呼び出しが可能である。

  • このオブジェクト A のクラス型を使用したオブジェクト A への呼び出しは(実際は)、
    透過プロキシを使用して呼び出しているので、始めに透過プロキシの
    Invoke メソッドにより呼び出しがフックされる。

    • 透過プロキシでは、継承した RealProxy の Invoke メソッドを
      オーバーライドする必要がある。

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

(3)

  • 透過プロキシは、Invoke メソッド内でリモート処理 基盤技術を使用し、
    呼び出しを実際のオブジェクトに転送する。

    • 透過プロキシのポイントは、透過プロキシの Invoke メソッドには、
      呼び出しメソッドのメソッド シグネチャの情報が引数として与えられている点である。

      • メソッド シグネチャとは、一般的に、
        「メソッド名」、「パラメタ数と順序、パラメタの型」、「戻り値の型」などを意味する。
  • 透過プロキシの Invoke メソッドから使用したいオブジェクトに呼び出しを転送する場合、
    最終的にレイトバインド 技術を使用することになる。

    • .NET では、System.Reflection 名前空間のクラスライブラリを使用して
      レイトバインドを実現する。
  • レイトバインドを使用する場合は、メソッド シグネチャを固定する方式が主流であるが、

    • 言語によっても異なるが、C++、C# のような「強い静的型付け」という
      言語的特性を持つ言語に於いては、
      実行に任意のオブジェクトに対して任意のメソッド呼び出しを試みるという処理が
      許可されていないため。

      • C++ では、これを仮想関数呼び出しや COM で実現する。
      • C# では、System.Reflection 名前空間のクラスライブラリを使用して実現する。
      • これに対し、VB では object 型に任意のメソッド呼び出しを記述できる
  • 透過プロキシの Invoke メソッドには、
    呼び出しメソッドのメソッド シグネチャの情報が引数として与えられるため、
    これを使用して、どのようなメソッド呼び出しも転送することができる。

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

補足(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 透過プロキシ1(オブジェクト A のインスタンスを渡すバージョン
PR2.cs 透過プロキシ2(オブジェクト A のクラス型情報を渡すバージョン
MyInt.cs 参照型を引数に使用した場合の動作検証に用いるクラス
Program.cs 各種テスト ケースを実行するエントリポイント メソッド

ターゲット クラス

ターゲット クラスは、本書で説明した「オブジェクト A」、
すなわち、クライアントが使用したいオブジェクトのことである。

実装のポイントは、MarshalByRefObject クラスを継承することであり、
その他は任意の実装が可能である。
サンプル プログラムでは、様々なシグネチャのメソッドの呼び出しをテストするために、
様々なシグネチャのメソッドをターゲット クラスに実装してある。

透過プロキシ

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

コンストラクタ

また、透過プロキシ1、2で、コンストラクタの実装が若干異なる。
異なる点は、ターゲット クラスのオブジェクト インスタンスを生成する方法である。

  • 透過プロキシ1
    コンストラクタから、ターゲット クラスのオブジェクト インスタンスを直接受け取る。

  • 透過プロキシ2
    コンストラクタからクラス情報を受け取り、
    これを使用してターゲット クラスのオブジェクト インスタンスを生成する。

Invokeメソッド

透過プロキシ1、2とも Invoke メソッドの実装は同じである。

補足(MyInt.cs「参照型を引数に使用した場合の動作検証」の意図): この
テスト ケースが用意されているのは、プロキシ経由の呼び出しで
引数の扱いが変わりうる
ためと読める。重要な論点なので補足する。

【アプリケーション ドメインを跨ぐ場合(本来の用途)】
   ・MarshalByRefObject を継承した型 → 参照が渡る(プロキシ経由)
   ・[Serializable] な型             → 【値がコピーされる】★
   ・どちらでもない型                 → 例外になる

 → 参照型を引数に渡しても、【呼ばれた側での変更が
   呼び出し元に反映されない】ことがある

【同一ドメイン内で AOP として使う場合】
   ・実際には同じヒープ上のオブジェクトなので、参照はそのまま渡る
   ・ただし ReturnMessage の作り方を誤ると
     out / ref の反映が失われる

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

補足(現在 .NET で AOP を実現する方法): RealProxy が使えない
.NET Core 以降の選択肢をまとめる。

手段 内容 評価
DispatchProxy 標準 APIインターフェイスに対する動的プロキシ ★ RealProxy の正統な後継
Castle DynamicProxy クラスにも適用可(virtual メソッド)。実績が豊富 最も高機能
DI コンテナのデコレーター ScrutorDecorate 単純明快
ミドルウェア / フィルター 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開発

NetDevInfraWiki

マイクロソフト系技術情報 Wiki
Open 棟梁 Wiki

(未着手)

開発基盤部会 Wiki

移行管理: DONETODO

Clone this wiki locally