nuits.jp blog

C#, Xamarin, WPFを中心に書いています。Microsoft MVP for Development Technologies。

XamarinでもAOPしたい! Cauldron.Interception.Fody編

前回XamarinでAOPするにはIL弄るライブラリを自作するしかないと言ったな?
あれは嘘だ。Cauldron.Interception.Fodyを使えばできる。

www.nuget.org

なんてこった…まぁ皆さんには朗報ですしょう。私は赤面の限りですが。

という訳で、今回はCauldron.Interception.Fodyを使ってXamarinでAOPをする方法(正確にはメソッド呼び出しをインターセプトする方法)を紹介したいと思います。

サンプル概要

さて、今回作るのは非常に簡単なサンプルです。MainPageでメソッドを呼び出し、メソッドの呼び出しをDebug.WriteLineでログ出力するだけのものです。
実際のプロジェクトでは

  • 想定される例外処理(通信エラーとか)
  • トランザクション管理
  • 認証や認可処理
  • ロギング処理(最近はGoogle Analyticsでよく使われる機能の分析とかしないのかな?良く分かりません)

なんかを統一的にアスペクトとして織り込む(Weaving)などに使えるでしょう。
便利ですよ?

下準備

とりあえずつぎの手順で下準備をしてください。

  1. Xamarin.Formsプロジェクトを新たに作成する(ここではXFodyAppという名称にしました)
  2. NuGetパッケージからCauldron.Interception.Fodyを適用する(共通コードのプロジェクトにだけ適用すればOKです)
    NuGet Gallery | Cauldron.Interception.Fody 2.0.16
  3. XFodyAppプロジェクト(共通コードプロジェクト)にFodyWeavers.xmlファイルを作成する

FodyWeavers.xmlにはつぎの内容を記載してください。

<?xml version="1.0" encoding="utf-8" ?>
<Weavers>
  <Cauldron.Interception/> 
</Weavers>

これで下準備は完了です。

インターセプターを作成する

メソッドの呼び出しをインターセプトし、ログ出力するクラスを作成します。
InterceptorAttributeクラスを作成し、次のように実装しましょう。

[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
public class InterceptorAttribute : Attribute, IMethodInterceptor
{
    private MethodBase _methodbase;
    public void OnEnter(Type declaringType, object instance, MethodBase methodbase, object[] values)
    {
        _methodbase = methodbase;
        Debug.WriteLine($"OnEnter() declaringType:{declaringType} instance:{instance} methodbase.Name:{_methodbase.Name} args:{string.Join(", ", values)}");
    }

    public void OnException(Exception e)
    {
    }

    public void OnExit()
    {
        Debug.WriteLine($"OnExit() methodbase.Name:{_methodbase.Name}");
    }
}

メソッドの呼び出しをインターセプトする属性クラスです。
とはいってもDebugにログを出力しているだけです。

インターセプターを織り込む

ちょっと雑ですが、MainPage.xaml.csにメソッドを追加して、そこに先のインターセプターを織り込みましょう。
MainPage.xaml.csを次のように実装してください。

public partial class MainPage : ContentPage
{
    public MainPage()
    {
        InitializeComponent();
        Add(1, 2);
    }

    [Interceptor]
    private int Add(int left, int right)
    {
        var result = left + right;
        Debug.WriteLine($"MainPage#Add() result:{result}");
        return result;
    }
}

AddメソッドにInterceptorを宣言しています。 これを実行すると、デバッグウィンドウに次のように表示されます。

f:id:nuitsjp:20171206230411p:plain

ちゃんとインターセプトできていますね! ちなみにインターセプターを織り込まれたメソッドは次のコードと等価なようにILを編集されているそうです。

public int Add()
{
    InterceptorAttribute interceptorAttribute = new InterceptorAttribute("Any valid attribute parameter types");

    try
    {
        interceptorAttribute.OnEnter(typeof(MainPage), this, MethodBase.GetMethodFromHandle(methodof(MainPage.Add()).MethodHandle, typeof(MainPage).TypeHandle), new object[0]);
        var result = left + right;
        Debug.WriteLine($"MainPage#Add() result:{result}");
        return result;
    }
    catch (Exception e)
    {
        interceptorAttribute.OnException(e);
        throw;
    }
    finally
    {
        interceptorAttribute.OnExit();
    }
}

さぁこれで心置きなくXamarinでもAOPライフが送れ…るといいんですが…

実はちょっと仕様が気に入らない

インターセプターの仕様が実はちょっと気に入りません。
インターセプターは次のようにOnEntry、OnExit、OnExceptionの三つのメソッドを実装します。

public class InterceptorAttribute : Attribute, IMethodInterceptor
{
    public void OnEnter(Type declaringType, object instance, MethodBase methodbase, object[] values)
    {
    }

    public void OnExit()
    {
    }

    public void OnException(Exception e)
    {
    }
}

実際、別にこれで実装できないことはないのですが、3つほど不満があります。

  1. インターセプターはChain of Responsibility的に複数のインターセプターを連結して適用できる仕組みが欲しい
  2. インターセプトはCastle.CoreのDynamicProxyのような仕組みでインターセプトしたい
  3. 例外をキャッチして後処理したら上には投げないみたいなこともできない

ちなみに、2.の具体例はつぎのような感じに実装したいのです。

public class Interceptor : IInterceptor
{
    public void Intercept(IInvocation invocation)
    {
        using(var connection = new SQLConnection(...))
        {
            connection.Open();
            invocation.Proceed();
        }
    }
}

connectionを、織り込んださきにどう渡すのか?という解決の容易な課題はありますが、このようにインターセプターでトランザクション管理をして、ちゃんとusing句で記載したいなという思いがあります。
もちろん前処理と後処理を別々のメソッドで呼び出されてもできないわけじゃないんですが、少なくとも単機能のインターセプターを組み合わせて、複数の非機能要件を実現するようなことはCauldron.Interception.Fodyではできません。また3.にも記載したように例外を握りつぶすような実装もできません。
できないなら…
作るしかないよね!

ってことで、25日のAdvend Calender自分的本番までには間に合わせたいなあ。でもIL全然わからんのですよね…間に合うかなあ。

というわけで、今日はここまで。
それではまた!