nuits.jp blog

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

【連載】ASP.NET Web API を使おう:第6回 Castle.Core DynamicProxy で AOP

本エントリーは連載「ASP.NET Web APIを使おう」の第3回となります。連載の目次はこちら。

www.nuits.jp

さて今回はWeb APIでもAspect Oriented Programing(AOP)をする為、Simple Injector 上でCastle.CoreのDynamicProxyを利用する方法を解説します。実際のところ、Web API に依存する内容ではないため、例えばWPFなどでもSimple Injectorを利用するなら、この方法はそのまま使えます。

という訳で早速始めましょう。

1. 前提条件

本連載で作成してきたコードが存在する前提で説明を進めますが、なくても理解できるよう記載しています。

また以下の環境の元記載しています。

  • Visual Studio 2017 Version 15.5.6
  • .NET Framework 4.7.1
  • Castle.Core 4.2.1

2. 目的

さて、ここではAOPの詳細は割愛しますが、何に使う物かさっぱりだと読むのも辛いと思いますので、何をしたいのか簡単に説明しておきます。

例えばWeb APIを作成する場合、提供する(ほとんど)全てのAPIに、ほぼ同じ要求が求めらるでしょう。

  • ユーザー認証
  • トランザクション管理
  • ロギング処理
  • 例外処理

などです。これらを地道に実装すると、全てのAPIのメソッドが、共通のコードとtry-catchの山になってしまいます。当然全てのメソッドで上にあげた共通処理をすべてテストする必要もあります。AOPを利用することでこれらの問題を解決する事ができるます。

具体的には、メソッド呼び出しをインターセプトしてメソッドの呼び出し前後に共通処理を編み込む感じです。

詳細はこちらに解説を書いていますので、よかったらご覧ください。

qiita.com

ここではSimple Injector と Castle.Core の DynamicProxy を組み合わせて実現します。

3. NuGetパッケージの適用

では実際に実現する方法を説明していきましょう。まずはNuGetから Castle.Core をインストールします。

ソリューション エクスプローラーで HelloWebApi プロジェクトを右クリックし「NuGet パッケージの管理(N)...」を選択します。

f:id:nuitsjp:20180215105338p:plain

開かれた NuGet パッケージマネージャー上で「参照」を選択し、検索テキストボックスに「Castle.Core」を入力し、検索結果から選択して「インストール」を押下してください。

f:id:nuitsjp:20180215171415p:plain

4. インターセプターの実装

メソッド呼び出しをフックして、メソッドの実行前後に共通処理を織り込むためのインターセプターを作成します。

HelloWebApi プロジェクトの Controllers フォルダに、ControllerInterceptorクラスを作成してください。

f:id:nuitsjp:20180215172911p:plain

そして、つぎのように実装しましょう。

public class ControllerInterceptor : IInterceptor
{
    public void Intercept(IInvocation invocation)
    {
        Debug.WriteLine($"Type:{invocation.TargetType.Name} Method:{invocation.Method.Name} Before");
        invocation.Proceed();
        Debug.WriteLine($"Type:{invocation.TargetType.Name} Method:{invocation.Method.Name} After");
    }
}

ControllerInterceptor が適用されたクラスのメソッドが呼び出される前に Intercept メソッドが呼ばれ、invocation の Proceed メソッドを呼ぶことで対象のメソッドを継続実行します。

つまり前述のコードではメソッドの実行前後に、対象メソッドのクラスとメソッドの名称をデバッグコンソールに出力するようになっています。

5. Controllerにインターセプターを適用する

HelloWebApi プロジェクトの App_Start フォルダにある SimpleInjectorWebApiInitializer クラスを開いてください。そこに、つぎのようなフィールドとメソッドを追加します。

private static readonly ProxyGenerator Generator = new ProxyGenerator();

private static readonly Func<Type, object, IInterceptor, object> CreateProxy =
    (p, t, i) => Generator.CreateClassProxyWithTarget(p, t, i);

public static void InterceptWith<TInterceptor>(this Container c,
    Predicate<Type> predicate)
    where TInterceptor : class, IInterceptor
{
    c.ExpressionBuilt += (s, e) =>
    {
        if (predicate(e.RegisteredServiceType))
        {
            var interceptorExpression =
                c.GetRegistration(typeof(TInterceptor), true).BuildExpression();

            e.Expression = Expression.Convert(
                Expression.Invoke(Expression.Constant(CreateProxy),
                    Expression.Constant(e.RegisteredServiceType, typeof(Type)),
                    e.Expression,
                    interceptorExpression),
                e.RegisteredServiceType);
        }
    };
}

コンテナへ型が登録され、登録された型が条件に合致した場合*1にイベントをハンドルするように設定しています。

ハンドルしたら、Simple Injector がその型を注入する為にインスタンスを生成するタイミングで、その型のプロキシーとして動作する Castle.Core の DynamicProxy を生成するよう、式木を使って定義していしています。

つづいて、同クラスの InitializeContainer メソッドの先頭に、つぎのように1行追加してください。

private static void InitializeContainer(Container container)
{
    container.InterceptWith<ControllerInterceptor>(type => type.Name.EndsWith("Controller")); // 追加した行
    container.Register<IEmployeeRepository, EmployeeRepository>(Lifestyle.Scoped);
}

コンテナへ登録された型の内、型名がControllerで終わるクラスに対してControllerInterceptorを適用するよう指定しています。

さあこれでコントローラーの全てのメソッドの呼び出しにたいして、共通処理を織り込めるようになりました。では実行して動作を確認してみましょう。

Type:EmployeesController Method:ExecuteAsync Before
Type:EmployeesController Method:Get Before
Type:EmployeesController Method:Get After
Type:EmployeesController Method:ExecuteAsync After

...何かちょっと想定と異なります。

呼び出したメソッドはGetメソッドなので、そのログだけが出力される想定ですが、実際にはさらにそのメソッドを包むように ExecuteAsync メソッドのログが出力されています。

つまりWeb APIのでは個別のメソッドが呼び出される前に、Controller の親クラスの ExecuteAsync が呼ばれ、その中から対象のメソッドが実行される、という事です。

これでも良いケースもあるでしょうが、ExecuteAsync はインターセプトしたくないというケースも多いでしょう。

という事で、指定したメソッドのみをインターセプトするように機能を追加します。

6. 指定メソッドのみインターセプトするよう機能追加

InterceptAttribute クラスを作成して、それが宣言されたメソッドだけインターセプトするようにします。

まずは HelloWebApi プロジェクトの Controllers フォルダに、InterceptAttribute クラスを作成してください。

f:id:nuitsjp:20180215174953p:plain

コードはつぎの通りです。

public sealed class InterceptAttribute : Attribute
{
}

つづいて、ControllerInterceptor クラスの Intercept メソッドをつぎのように修正します。

public void Intercept(IInvocation invocation)
{
    if (invocation.Method.GetCustomAttribute<InterceptAttribute>() != null)
    {
        Debug.WriteLine($"Type:{invocation.TargetType.Name} Method:{invocation.Method.Name} Before");
        invocation.Proceed();
        Debug.WriteLine($"Type:{invocation.TargetType.Name} Method:{invocation.Method.Name} After");
    }
    else
    {
        invocation.Proceed();
    }
}

InterceptAttribute が宣言されたメソッドのみ、インターセプトするように処理を分岐しています。

つづいて EmployeesController クラスの Get メソッドに InterceptAttribute を宣言します。

public class EmployeesController : ApiController
{
    ...

    // GET api/<controller>
    [Intercept]
    public virtual IEnumerable<Employee> Get()
    {
        ...

それでは動かしてみましょう。

Type:EmployeesController Method:Get Before
Type:EmployeesController Method:Get After

正しく Get メソッドだけインターセプトされたことが確認できました。

これで今回は以上になります。

次回はデータベース接続について解説したいと思います。

*1:predicateがtrueを返した場合