ROBOT PAYMENT TECH-BLOG

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

インターセプターでトレースログを自動化してみる(ASP.NET Core + DryIoc)

こんにちは、こんばんは、おはようございます。 ペイメントシステム課エンジニアの川上です。

ログ出力、してますかー!? どんなに完ぺきなコードを書いたつもりでも「そんなバカな!?」という不具合が起こるのが世の常です。 そんなときに強い味方となるログですね。 メソッドの入り口と出口で「start」「end」とかチマチマ書いてデバッグした経験のある方もいるのではないでしょうか。 それ、インターセプターで解決できるかもしれません。

なお今回はDIコンテナ(IoCコンテナ)について基本的な知識がある方を対象としています。 「コンストラクタにインスタンスが入ってくるヤツでしょ?」という程度の理解で十分です。 もしも「DIコンテナ?なにそれおいしいの?」状態の方は、以下のリンクを先に読んでいただくと幸せになれるかもしれません。 依存関係の逆転 Dependency injection in ASP.NET Core また動作確認やスクリーンショットには Visual Studio 2022 v17.3.3 を使用しています。 それではいってみましょう!!

プロジェクトを準備しよう

まずは Visual Studio を起動して「新しいプロジェクトの作成」を選択します。 次に「ASP.NET Core Web アプリ」のテンプレートを選択して新しいプロジェクトを作成します。 特に変更する箇所はないので次へ次へと進むと、最初の画面が表示されます。

DIしよう

まずはDIするためのインターフェイスと実装を用意します。 今回はサンプルなので、出力ウィンドウに文字列を書き出すだけの簡単な実装にします。

[IOperation.cs]
namespace WebApplication1
{
    public interface IOperation
    {
        void AddUser();

        void DelUser();
    }
}
[OperationImpl.cs]
using System.Diagnostics;

namespace WebApplication1
{
    public class OperationImpl : IOperation
    {
        public void AddUser()
        {
            Debug.WriteLine("ユーザーを追加する。");
        }

        public void DelUser()
        {
            Debug.WriteLine("ユーザーを削除する。");
        }
    }
}

DIコンテナに登録します。 とりあえずシングルトンでいいかな。

[Program.cs]
using WebApplication1;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

// DIコンテナに登録します
builder.Services.AddSingleton<IOperation, OperationImpl>();

var app = builder.Build();
(以降、変更がないので省略)

次にインスタンスを取り出します。 デバッグ実行すると最初に表示される Index.cshtml のコードビハインドを書き換えます。 テンプレートで用意されている ILogger は不要なので、これを先ほど用意したインターフェイスに書き換えてしまいましょう。 さらに DI されたインスタンスのメソッドを呼び出します。 ファイル全体としては以下のようになります。

[Index.cshtml.cs]
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace WebApplication1.Pages
{
    public class IndexModel : PageModel
    {
        public IndexModel(IOperation operation)
        {
            operation.AddUser();
            operation.DelUser();
        }
    }
}    

これで準備完了です。 F5 キーを押してデバッグ実行を開始してみましょう。 はい、いいカンジですね。 出力ウィンドウに、OperationImpl.cs で実装したデバッグメッセージが出力されていることが確認できました。 DI 成功です!おめでとう!!

インターセプトしよう

ここまでで出来上がったコードをベースに、Interceptor を使えるように改造していきましょう。 ASP.NET Core 標準の DI コンテナは Microsoft謹製の Microsoft.Extensions.DependencyInjection なのですが、こちらで Interceptor を使う方法は見つかりませんでした。 おそらくサポートしていないのだと思います。 ということで、今回は DI コンテナに DryIoc を使用します。 まずソリューションエクスプローラーでプロジェクト名を右クリックして、「NuGet パッケージの管理」をクリックします。 「参照」タブで「dryioc」と入力すると候補が表示されるので、DryIoc.Microsoft.DependencyInjection をインストールしてください。 また Interceptor を使用する際に Castle.Core も必要なので、こちらも「NuGet パッケージの管理」からインストールしておいてください。 「インストール済み」タブが以下のようになれば準備OKです。

次に拡張メソッドを作成します。 サンプルなので公式サイトの実装をそのまま流用させていただきました。

[DryIocInterception.cs]
using Castle.DynamicProxy;
using DryIoc.ImTools;
using DryIoc;

namespace WebApplication1
{
    public static class DryIocInterception
    {
        private static readonly DefaultProxyBuilder _proxyBuilder = new DefaultProxyBuilder();

        public static void Intercept<TService, TInterceptor>(this IRegistrator registrator, object serviceKey = null)
            where TInterceptor : class, IInterceptor
        {
            var serviceType = typeof(TService);

            Type proxyType;
            if (serviceType.IsInterface)
                proxyType = _proxyBuilder.CreateInterfaceProxyTypeWithTargetInterface(
                    serviceType, ArrayTools.Empty<Type>(), ProxyGenerationOptions.Default);
            else if (serviceType.IsClass)
                proxyType = _proxyBuilder.CreateClassProxyTypeWithTarget(
                    serviceType, ArrayTools.Empty<Type>(), ProxyGenerationOptions.Default);
            else
                throw new ArgumentException(
                    $"Intercepted service type {serviceType} is not a supported, cause it is nor a class nor an interface");

            registrator.Register(serviceType, proxyType,
                made: Made.Of(pt => pt.PublicConstructors().FindFirst(ctor => ctor.GetParameters().Length != 0),
                    Parameters.Of.Type<IInterceptor[]>(typeof(TInterceptor[]))),
                setup: Setup.DecoratorOf(useDecorateeReuse: true, decorateeServiceKey: serviceKey));
        }
    }
}

ようやく本題となるインターセプターの実装です。 今回はメソッド実行時に開始ログと終了ログを挿入するだけの簡単な処理なので、名前を LoggingInterceptor としました。 またインターセプターで出力するメッセージが分かりやすくなるように、先頭に星マークを付けてみました。

[LoggingInterceptor.cs]
using Castle.DynamicProxy;
using System.Diagnostics;

namespace WebApplication1
{
    public class LoggingInterceptor : IInterceptor
    {
        public void Intercept(IInvocation invocation)
        {
            Debug.WriteLine($"★ {invocation.Method.Name} を開始します。");
            invocation.Proceed();
            Debug.WriteLine($"★ {invocation.Method.Name} を終了します。");
        }
    }
}

あと少しでゴールです!! Program.cs にて DryIoc のインスタンスを作成し、インターセプターを登録した後、DryIoc をASP.NET Core のDIコンテナとして登録します。

var container = new Container();
container.Register<LoggingInterceptor>(Reuse.Singleton);
builder.Host.UseServiceProviderFactory(new DryIocServiceProviderFactory(container));

また、最初に作成した IOperation インターフェイスと、インターセプターとして実装した LoggingInterceptor を紐づけるコードを書きます。

container.Intercept<IOperation, LoggingInterceptor>();

ファイル全体としては以下のようになります。

[Program.cs]
using WebApplication1;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

// DryIocにInterceptorを登録してASP.NET CoreのDIコンテナーにします。
var container = new Container();
container.Register<LoggingInterceptor>(Reuse.Singleton);
builder.Host.UseServiceProviderFactory(new DryIocServiceProviderFactory(container));

// コンテナに追加するサービスとInterceptorを紐付けます。
container.Intercept<IOperation, LoggingInterceptor>();

// DIコンテナに登録します
builder.Services.AddSingleton<IOperation, OperationImpl>();

var app = builder.Build();
(以降、変更がないので省略)

以上で完成です。

実行してみよう

ようやくここまでたどり着きました。 それでは F5 キーを押してデバッグ実行してみましょう!!

はい、出力ウィンドウには以下のように出力されました。

★ AddUser を開始します。
ユーザーを追加する。
★ AddUser を終了します。
★ DelUser を開始します。
ユーザーを削除する。
★ DelUser を終了します。

機能実装した OperationImpl.cs には存在しない、「~開始します。」「~終了します。」が出力されていることが分かりますね。これはDIコンテナがインスタンスをインジェクションする際に、処理を横取り(=インターセプト)することで実現しています。 うまくいきました、成功です!!

終わりに

今回はログ出力に焦点を絞って解説しましたが、例えば「public メソッドなら例外処理を挿入する」といった使い方もできます。 インターセプターはAOP(Aspect Oriented Programming = アスペクト志向プログラミング)を実現する手段でもありますので、このような実装が可能であることを知っておくだけでもシステム設計の役に立つと思います。

ではまた!!



We are hiring!!

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

speakerdeck.com
www.robotpayment.co.jp