ROBOT PAYMENT TECH-BLOG

株式会社ROBOT PAYMENTのテックブログです

C#でアスペクト指向プログラミング(AOP)を実現する方法

こんにちは、決済サービスの開発を担当しているtaniguchikunです。 本記事ではアスペクト指向プログラミング(AOP)に関しての解説とサンプルコードを紹介したいと思います。

アスペクト指向プログラミングってなに?

大雑把に言いますとモジュール内の複数のメソッドで共通化できない処理を共通化する事を言います

アスペクト指向プログラミングはどのように使用されているのか?

主な使用方法はロギング処理や、特定のメソッドでスタブを返すようにする処理で使用される事が多いと思います。

どう実現するのか?

  • 絶対に必要になるクラス名を記載します
    • .Net Framework 4.8以前
      • MarshalByRefObject
      • RealProxy
      • ContextBoundObject → 実装次第では必要ないです
      • ProxyAttribute → 実装次第では必要ないです
    • .Net 5以降
      • DispatchProxy

下記の通り.Net Frameworkテクノロジーが.Netでは使えなくなっています。

learn.microsoft.com

どのように実装するのか?(.Net Framework 4.8以前)

AOP導入コード

void Main()
{
    Console.WriteLine($"------------- 実行(引数あり) -------------");
    
    var aaa = new TestClass();
    
    Console.WriteLine("");
    
    Console.WriteLine($"------------- aaa.TestMethod(123) 実行 -------------");
    aaa.TestMethod(123);
    
    Console.WriteLine("");
    
    Console.WriteLine($"------------- aaa.TestMethodaaaa(123, \"testaaaaaaaaa\") 実行 -------------");
    aaa.TestMethodaaaa(1234, "testaaaaaaaaa");

    Console.WriteLine("");

    Console.WriteLine($"------------- 実行(引数あり) -------------");

    var bbb = new TestClass(123456789);

    Console.WriteLine("");

    Console.WriteLine($"------------- aaa.TestMethod(123) 実行 -------------");
    bbb.TestMethod(321);

    Console.WriteLine("");

    Console.WriteLine($"------------- aaa.TestMethodaaaa(123, \"testbbbbbbbbb\") 実行 -------------");
    bbb.TestMethodaaaa(321, "testbbbbbbbbb");
}

public class TestClass : AOP
{
    public TestClass()
    {
        Console.WriteLine($"引数なしコンストラクター処理");
    }
    
    [IgnoreAop]
    public TestClass(int bbbbb)
    {
        Console.WriteLine($"引数ありコンストラクター処理");
    }
    
    public int TestMethod(int param)
    {
        Console.WriteLine($"{param}");
        return param;
    }

    public string TestMethodaaaa(int param)
    {
        Console.WriteLine($"{param}");
        return param.ToString();
    }

    public string TestMethodaaaa(int param, string osa)
    {
        Console.WriteLine($"{param} - {osa}");
        return param.ToString();
    }
}

[LogRealProxy]
public abstract class AOP : ContextBoundObject
{
    public class IgnoreAopAttribute : Attribute{}
}

public class LogRealProxyAttribute : ProxyAttribute
{
    public override MarshalByRefObject CreateInstance(Type serverType)
    {
        MarshalByRefObject target = base.CreateInstance(serverType);
        RealProxy rp;
        rp = new LogRealProxy(target, serverType);
        return rp.GetTransparentProxy() as MarshalByRefObject;
    }
}

public class LogRealProxy : CustomRealProxy
{
    public LogRealProxy(MarshalByRefObject target, Type serverType) : base(target, serverType) {}

    protected override void BeforeMethod(IMethodCallMessage call)
    {
        if ( call.MethodBase.GetCustomAttribute(typeof(AOP.IgnoreAopAttribute)) == null )
            Console.WriteLine($"{call.MethodBase.DeclaringType}.{call.MethodBase.Name} - ({string.Join(", ", call.Args.Select(a => a.ToString()))}) - 処理前");
    }

    protected override void AfterMethod(IMethodCallMessage call, IMessage response)
    {
        if (call.MethodBase.GetCustomAttribute(typeof(AOP.IgnoreAopAttribute)) == null)
        {
            string returnValue = "戻り値なし";
            returnValue = call.MethodBase.IsConstructor ? returnValue : Newtonsoft.Json.JsonConvert.SerializeObject(((ReturnMessage)response).ReturnValue);
            Console.WriteLine($"{call.MethodBase.DeclaringType}.{call.MethodBase.Name} - ({string.Join(", ", call.Args.Select(a => a.ToString()))}) - {returnValue} - 処理後");
        }
    }
}

public abstract class CustomRealProxy : RealProxy
{
    protected MarshalByRefObject instance;

    protected CustomRealProxy(MarshalByRefObject instance, Type serverType) : base(serverType)
    {
        this.instance = instance;
    }

    public override IMessage Invoke(IMessage request)
    {
        IMessage response = null;

        IMethodCallMessage call = (IMethodCallMessage)request;

        IConstructionCallMessage ctor = call as IConstructionCallMessage;

        BeforeMethod(call);
        if (ctor != null)
        {
            RealProxy rp = RemotingServices.GetRealProxy(this.instance);
            rp.InitializeServerObject(ctor);
            MarshalByRefObject tp = this.GetTransparentProxy() as MarshalByRefObject;
            response = EnterpriseServicesHelper.CreateConstructionReturnMessage(ctor, tp);
        }
        else
        {
            response = Invoke(call);
        }
        AfterMethod(call, response);

        return response;
    }

    private IMessage Invoke(IMethodCallMessage call)
    {
        ReturnMessage response;

        response = RemotingServices.ExecuteMessage(this.instance as MarshalByRefObject, call) as ReturnMessage;

        if (response.Exception != null)
        {
            throw response.Exception;
        }

        return response;
    }

    protected abstract void BeforeMethod(IMethodCallMessage call);
    
    protected abstract void AfterMethod(IMethodCallMessage call, IMessage response);
}

結果は下記になります

------------- 実行(引数あり) -------------
UserQuery+TestClass..ctor - () - 処理前
引数なしコンストラクター処理
UserQuery+TestClass..ctor - () - 戻り値なし - 処理後

------------- aaa.TestMethod(123) 実行 -------------
UserQuery+TestClass.TestMethod - (123) - 処理前
123
UserQuery+TestClass.TestMethod - (123) - 123 - 処理後

------------- aaa.TestMethodaaaa(123, "testaaaaaaaaa") 実行 -------------
UserQuery+TestClass.TestMethodaaaa - (1234, testaaaaaaaaa) - 処理前
1234 - testaaaaaaaaa
UserQuery+TestClass.TestMethodaaaa - (1234, testaaaaaaaaa) - "1234" - 処理後

------------- 実行(引数あり) -------------
引数ありコンストラクター処理

------------- aaa.TestMethod(123) 実行 -------------
UserQuery+TestClass.TestMethod - (321) - 処理前
321
UserQuery+TestClass.TestMethod - (321) - 321 - 処理後

------------- aaa.TestMethodaaaa(123, "testbbbbbbbbb") 実行 -------------
UserQuery+TestClass.TestMethodaaaa - (321, testbbbbbbbbb) - 処理前
321 - testbbbbbbbbb
UserQuery+TestClass.TestMethodaaaa - (321, testbbbbbbbbb) - "321" - 処理後

どのように実装するのか?(.Net 5以降)

DispatchProxyクラスは制約が多く、既存コードを少し直しての導入には向きません。 AOP導入コード

void Main()
{
    Console.WriteLine($"------------- 実行(引数あり) -------------");
    
    var aaa = LogDispaychProxy<ITestClass>.Create(new TestClass());
    
    Console.WriteLine("");
    
    Console.WriteLine($"------------- aaa.TestMethod(123) 実行 -------------");
    aaa.TestMethod(123);
    
    Console.WriteLine("");
    
    Console.WriteLine($"------------- aaa.TestMethodaaaa(123, \"testaaaaaaaaa\") 実行 -------------");
    aaa.TestMethodaaaa(1234, "testaaaaaaaaa");

    Console.WriteLine("");

    Console.WriteLine($"------------- 実行(引数あり) -------------");

    var bbb = LogDispaychProxy<ITestClass>.Create(new TestClass(123456789));

    Console.WriteLine("");

    Console.WriteLine($"------------- aaa.TestMethod(123) 実行 -------------");
    bbb.TestMethod(321);

    Console.WriteLine("");

    Console.WriteLine($"------------- aaa.TestMethodaaaa(123, \"testbbbbbbbbb\") 実行 -------------");
    bbb.TestMethodaaaa(321, "testbbbbbbbbb");
}

public interface ITestClass {
    public int TestMethod(int param);
    public string TestMethodaaaa(int param);
    public string TestMethodaaaa(int param, string osa);
}

public class TestClass : LogDispaychProxy<ITestClass>, ITestClass
{
    public TestClass()
    {
        Console.WriteLine($"引数なしコンストラクター処理");
    }
    
    public TestClass(int bbbbb)
    {
        Console.WriteLine($"引数ありコンストラクター処理");
    }
    
    public int TestMethod(int param)
    {
        Console.WriteLine($"{param}");
        return param;
    }

    public string TestMethodaaaa(int param)
    {
        Console.WriteLine($"{param}");
        return param.ToString();
    }

    public string TestMethodaaaa(int param, string osa)
    {
        Console.WriteLine($"{param} - {osa}");
        return param.ToString();
    }
}

public static class CustomDispaychProxyExtention
{
    
}

public class LogDispaychProxy<T> : CustomDispaychProxy<T, LogDispaychProxy<T>> where T : class
{
    protected override void BeforeMethod(MethodInfo targetMethod, object[] args)
    {
        Console.WriteLine($"{targetMethod.DeclaringType.Name}.{targetMethod.Name}({string.Join(",", args.Select(param => param.ToString()))}) 処理を実行します。");
    }

    protected override void AfterMethod(MethodInfo targetMethod, object[] args, object result)
    {
        Console.WriteLine($"{targetMethod.DeclaringType.Name}.{targetMethod.Name}({string.Join(",", args.Select(param => param.ToString()))}) (戻り値: {result}) 処理を実行します。");
    }
}

public class CustomDispaychProxy<T,Y> : DispatchProxy
where T : class
where Y : CustomDispaychProxy<T,Y>
{
    protected object _instance;

    public static T Create(T instance)
    {
        var proxy = Create<T, Y>() as Y;
        proxy.SetInstance(instance);

        return proxy as T;
    }

    public void SetInstance(T instance)
    {
        _instance = instance;
    }

    protected override object Invoke(MethodInfo targetMethod, object[] args)
    {
        try
        {
            BeforeMethod(targetMethod, args);
            var result = targetMethod.Invoke(_instance, args);
            AfterMethod(targetMethod, args, result);

            return result;
        }
        catch (Exception ex) when (ex is TargetInvocationException)
        {
            ExceptionMethod(targetMethod, args, ex);
            throw ex.InnerException ?? ex;
        }
    }

    protected virtual void ExceptionMethod(MethodInfo targetMethod, object[] args, Exception ex)
    {
        Console.WriteLine($"{targetMethod.DeclaringType.Name}.{targetMethod.Name}] 例外が発生");
        Console.WriteLine($"{ex.ToString()}");
    }

    protected virtual void BeforeMethod(MethodInfo targetMethod, object[] args)
    {
        throw new Exception("継承してご使用ください");
    }

    protected virtual void AfterMethod(MethodInfo targetMethod, object[] args, object result)
    {
        throw new Exception("継承してご使用ください");
    }
}

結果は下記になります

------------- 実行(引数あり) -------------
引数なしコンストラクター処理

------------- aaa.TestMethod(123) 実行 -------------
ITestClass.TestMethod(123) 処理を実行します。
123
ITestClass.TestMethod(123) (戻り値: 123) 処理を実行します。

------------- aaa.TestMethodaaaa(123, "testaaaaaaaaa") 実行 -------------
ITestClass.TestMethodaaaa(1234,testaaaaaaaaa) 処理を実行します。
1234 - testaaaaaaaaa
ITestClass.TestMethodaaaa(1234,testaaaaaaaaa) (戻り値: 1234) 処理を実行します。

------------- 実行(引数あり) -------------
引数ありコンストラクター処理

------------- aaa.TestMethod(123) 実行 -------------
ITestClass.TestMethod(321) 処理を実行します。
321
ITestClass.TestMethod(321) (戻り値: 321) 処理を実行します。

------------- aaa.TestMethodaaaa(123, "testbbbbbbbbb") 実行 -------------
ITestClass.TestMethodaaaa(321,testbbbbbbbbb) 処理を実行します。
321 - testbbbbbbbbb
ITestClass.TestMethodaaaa(321,testbbbbbbbbb) (戻り値: 321) 処理を実行します。

まとめ

開発する上では便利ですがパフォーマンスを下げてしまう事が多い為、出来れば設計段階で上手く折り合いをつけて使用されることをお勧めします。



We are hiring!!

ROBOT PAYMENTでは一緒に働く仲間を募集しています!!!

speakerdeck.com
www.robotpayment.co.jp
🎉twitter採用担当アカウント開設!🎉どんどん情報発信していきます!!