[C#] RealProxyの使い方のサンプルコード

C#でAOP(アスペクト指向)的な事をしたい時の選択肢としてRealProxyを使う方法があるので、RealProxy の使い方について調べてみた。

なんだか小難しいので適当にコード書いてみた。

MarshalByRefObject を使う場合/戻り値なし/例外ハンドル

RealProxy のコンストラクタに型を渡す必要があるが、 これは MarshalByRefObject 派生クラス または インターフェースを実装したクラスである必要がある。 インタフェース使ったほうが制限は少ないだろうけどいちいちインタフェース作るのが面倒で継承が可能な場合は MarshalByRefObject で十分か。 ここでは MarshalByRefObject を使ってみる。 プロキシを介して例外送出する可能性がある戻り値の無いメソッドを呼ぶ。

using System;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.Remoting.Messaging;
using System.Runtime.Remoting.Proxies;

class Program
{
  static void Main(string[] args)
  {
    // 1. RealProxy派生クラスのインスタンスを作り透過プロキシを取得
    EchoClass proxy =
      (EchoClass)new EchoProxy().GetTransparentProxy();

    // 2. Echoを呼び出すと、RealProxyTest#Invokeが呼び出される
    proxy.Echo("Hello", "World");

    Console.WriteLine();

    try
    {
      // 5. 例外が発生するパターン
      proxy.Echo();
    }
    catch (ArgumentException ex)
    {
      Console.WriteLine("Error: {0}", ex);
    }

    Console.ReadKey();
  }
}

// RealProxyのコンストラクタの引数にインタフェースとなる型を指定する必要がある。
// この型は MarshalByRefObject を継承するか、インタフェースを指定すること。
class EchoClass : MarshalByRefObject
{
  public void Echo(params object[] args)
  {
    if (args.Length != 0)
    {
      Console.WriteLine(string.Join("*", args));
    }
    else
    {
      throw new ArgumentException("empty", "args");
    }
  }
}

class EchoProxy : RealProxy
{
  EchoClass realObject = new EchoClass();

  public EchoProxy()
    : base(typeof(EchoClass)) // 透過プロキシのインタフェースを指定
  {
  }

  // プロキシがメッセージを受け取った時に呼ばれるメソッド。
  // ここで好きな処理を噛ませることができる。
  public override IMessage Invoke(IMessage msg)
  {
    IMethodMessage method = msg as IMethodMessage;

    Console.WriteLine("--- Start {0} ---", method.MethodName);
    try
    {
      // 3. 代理ではない本当の(?)オブジェクトのメソッド実行
      MethodInfo methodInfo = (MethodInfo)method.MethodBase;
      methodInfo.Invoke(realObject, method.Args);
    }
    catch (TargetInvocationException ex)
    {
      // 6. Echo内部で例外が発生すると
      // TargetInvocationExceptionが送出される。
      // Echo内部で発生した例外は ex.InnerException で取得。
      // ※Throw ex; だとスタックトレースが消える為
      //   例外は必ずReturnMessageで返すこと。
      return new ReturnMessage(ex.InnerException, (IMethodCallMessage)msg);
    }
    finally
    {
      Console.WriteLine("--- End {0} ---", method.MethodName);
    }

    // 4. 戻り値。今回のEchoには戻り値も出力も無いので、null, null 指定
    return new ReturnMessage(
      null, null, 0, method.LogicalCallContext, (IMethodCallMessage)msg);
  }
}

結果

--- Start Echo ---
Hello*World
--- End Echo ---

--- Start Echo ---
--- End Echo ---
Error: System.ArgumentException: empty
パラメーター名:args

Server stack trace:
   場所 EchoClass.Echo(Object[] args) 場所 c:\Users\minimalie\projects\real_proxy\Program.cs:行 50

Exception rethrown at [0]:
   場所 System.Runtime.Remoting.Proxies.RealProxy.HandleReturnMessage(IMessage reqMsg, IMessage retMsg)
   場所 System.Runtime.Remoting.Proxies.RealProxy.PrivateInvoke(MessageData& msgData, Int32 type)
   場所 EchoClass.Echo(Object[] args)
   場所 Program.Main(String[] args) 場所 c:\Users\minimalie\projects\real_proxy\Program.cs:行 26

インターフェースを使う場合/戻り値あり/出力あり

using System;
using System.Reflection;
using System.Runtime.Remoting.Messaging;
using System.Runtime.Remoting.Proxies;

class Program
{
  static void Main(string[] args)
  {
    IModify modifier =
      (IModify)new ToUpperProxy().GetTransparentProxy();

    // テスト1
    {
      string input = "hoge";
      string dest;
      bool result = modifier.Modify(input, out dest);
      Console.WriteLine("[TEST1] result:{0} in:{1} out:{2}",
        result, input, dest ?? "(null)");
    }

    // テスト2
    {
      string dest;
      bool result = modifier.Modify(null, out dest);
      Console.WriteLine("[TEST2] result:{0} in:(null) out:{1}",
        result, dest ?? "(null)");
    }

    Console.ReadKey();
  }
}

interface IModify
{
  bool Modify(string text, out string dest);
}

class ToUpper : IModify
{
  public bool Modify(string text, out string dest)
  {
    if (string.IsNullOrEmpty(text))
    {
      dest = null;
      return false;
    }
    else
    {
      dest = text.ToUpperInvariant();
      return true;
    }
  }
}

class ToUpperProxy : RealProxy
{
  ToUpper realObject = new ToUpper();

  public ToUpperProxy()
    : base(typeof(IModify)) // 透過プロキシのインタフェースを指定
  {
  }

  public override IMessage Invoke(IMessage msg)
  {
    IMethodMessage method = msg as IMethodMessage;
    object[] args = method.Args;

    MethodInfo methodInfo = (MethodInfo)method.MethodBase;
    object result = methodInfo.Invoke(realObject, args);

    return new ReturnMessage(
      result,
      args,
      args.Length,
      method.LogicalCallContext,
      (IMethodCallMessage)msg);
  }
}

結果

[TEST1] result:True in:hoge out:HOGE
[TEST2] result:False in:(null) out:(null)

ReturnMessageはまぁ細かいことは面倒なので調べてないけど、 こんな感じのテンプレコードだと思っておけばいいか。。

ProxyAttribute + ContextBoundObjext も使う方法(コンストラクタのインターセプト)

ProxyAttribute + ContextBoundObject を使うとコンストラクタにも割り込める。 ContextBoundObjectを継承する必要性についてはとりあえず考えない方がいい。

using System;
using System.Reflection;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Activation;
using System.Runtime.Remoting.Messaging;
using System.Runtime.Remoting.Proxies;
using System.Runtime.Remoting.Services;

class Program
{
    static void Main(string[] args)
    {
        // 1.EchoProxyAttributeを付けたEchoClassのコンストラクタを呼び出すと
        // ProxyAttribute派生クラスのCreateInstanceが代わりに呼び出される。
        Console.WriteLine("#1");
        EchoClass echoInstance = new EchoClass();

        // 4. echoInstanceは実際はEchoProxy
        Console.WriteLine("#4");
        echoInstance.Execute("Piyo");

        Console.ReadKey();
    }
}

sealed class EchoProxyAttribute : ProxyAttribute
{
    public override MarshalByRefObject CreateInstance(Type serverType)
    {
        // 2. ここでRealObjectの代わりに透過プロキシを生成
        Console.WriteLine("#2");
        MarshalByRefObject realObject = base.CreateInstance(serverType);
        var proxy = new EchoProxy((EchoClass)realObject);
        return (MarshalByRefObject)proxy.GetTransparentProxy();
    }
}

// ProxyAttribute派生クラスを属性に指定するクラスは
// ContextBoundObjectを継承している必要がある。
// 細かい理由は考えずとりあえずこういうものだと思っておいた方がいい。
[EchoProxyAttribute]
class EchoClass : ContextBoundObject
{
    public void Execute(string text)
    {
        Console.WriteLine(text);
    }
}

class EchoProxy : RealProxy
{
    // 最初のサンプルはここでインスタンス生成(newe EchoClass)してたけど
    // 今回の場合だとコンストラクタ呼び出しの前に
    // EchoProxyAttribute#CreateInstanceが呼び出されるため、
    // プロキシ内でRealObjectを作ると無限ループする。ので、コンストラクタでセット。
    private readonly EchoClass realObject = null;

    public EchoProxy(EchoClass realObject)
        : base(typeof(EchoClass))
    {
        this.realObject = realObject;
    }

    public override IMessage Invoke(IMessage msg)
    {
        var method = (IMethodCallMessage)msg;
        var constructor = method as IConstructionCallMessage;
        if (constructor != null)
        {
            // 3. コンストラクタ呼び出しのインターセプト
            Console.WriteLine("#3");
            RealProxy defaultProxy = RemotingServices.GetRealProxy(realObject);
            defaultProxy.InitializeServerObject(constructor);

            var transparentProxy = (MarshalByRefObject)this.GetTransparentProxy();
            return EnterpriseServicesHelper.CreateConstructionReturnMessage(
                constructor, transparentProxy);
        }
        else
        {
            // 5. Execute呼び出しのインターセプト
            Console.WriteLine("#5");
            MethodInfo methodInfo = (MethodInfo)method.MethodBase;
            methodInfo.Invoke(realObject, method.Args);

            return new ReturnMessage(
              null, null, 0, method.LogicalCallContext, (IMethodCallMessage)msg);
        }
    }
}

結果

#1
#2
#3
#4
#5
Piyo

プロパティやフィールドの扱い

RealProxy はプロパティやフィールドのアクセスも全部仲介するので これらに対するアクセスがあるなら別途実装する必要がある場合が多いかと思う。

MethodBase.IsSpecialNametrue のものはプロパティだったりコンパイラによって生成された特殊な関数なので これによって処理を分岐できる。

RemotingServices.ExecuteMessageを使う方法

System.Runtime.Remoting.RemotingServices.ExecuteMessage を使ってメソッドを呼び出す方法もあるのでこのサンプルコードもついでに書いておく(まだ書いてない)

Share
関連記事