kazuk は null に触れてしまった

C# / .NET 系技術ネタ縛りでお送りしております

ASP.NET IHttpModule での HttpApplication イベントのハンドリングに割り込む(1)


元ネタはすでに修正済みだったって事でNo.1が切腹してましたが、こんな方法もあるよというアプローチとその周辺にまつわる ASP.NET Core の闇についてちょっと書いてみます。

まず、ASP.NET において IHttpModule が HttpApplication のどのイベントをハンドルしているのか調べる方法はあるのか、イベントハンドリングに割り込む前にここから話を進めましょう。

回答としては存在します。 HttpApplication.Events プロパティ このプロパティの説明を見る通りで、EventHandlerList を舐めればイベントハンドラを列挙する事が出来ます。

「EventHandlerList を舐めれば」が可能であればこれだけで話は済みますが、そんなに簡単には行かんのです。

まず、EventHandlerList は object をキーとしてDelegateが取得できますが、キーとして使う object がどっからも取れんという事、少なくとも普通の手段ではこれを取る事はできません。リフレクションを使わない限りには。

どうせリフレクションを使うなら致死量以上の毒はいくら飲んでも同じ事なので EventHandlerList を徹底的に暴いておくのも良いでしょう。というわけで、以下の拡張メソッドが出来上がります。

using System;
using System.Collections.Generic;
using System.Linq;
using System.ComponentModel;
using System.Reflection;
using System.Diagnostics.Contracts;

namespace Kazuk.Dark
{
    /// <summary>
    /// EventHandlerList相手のリフレクションを隠ぺいします
    /// </summary>
    public static class EventHandlerListExtention
    {
        private const BindingFlags NonPublicInstance =
            BindingFlags.NonPublic | BindingFlags.Instance;

        private static readonly Lazy<FieldInfo> _headField = 
            new Lazy<FieldInfo>(
                () => typeof(EventHandlerList)
                      .GetField("head", NonPublicInstance));

        private static bool _initialized;
        private static FieldInfo _keyField;
        private static FieldInfo _handlerField;
        private static FieldInfo _nextField;

        /// <summary>
        /// EventHandlerList の内容をDictionaryに変換します
        /// </summary>
        /// <param name="eventHandlerList"></param>
        /// <returns></returns>
        public static Dictionary<object, Delegate> 
            ToDictionary(this EventHandlerList eventHandlerList)
        {
            Contract.Requires<ArgumentNullException>(eventHandlerList!=null);

            var item = _headField.Value.GetValue(eventHandlerList);
            if (item==null) return new Dictionary<object, Delegate>();
            if (!_initialized)
            {
                Type tEntry = item.GetType();
                _keyField = tEntry.GetField("key", NonPublicInstance);
                _handlerField = tEntry.GetField("handler", NonPublicInstance);
                _nextField = tEntry.GetField("next", NonPublicInstance);
                _initialized = true; 
            }
            var result = new Dictionary<object, Delegate>();
            while (item !=null)
            {
                result.Add(
                    _keyField.GetValue(item),
                    (Delegate)_handlerField.GetValue(item));
                item = _nextField.GetValue(item);
            }
            return result;
        }
    }
}

やってる事がリフレクションという決しておすすめできないテクニックなので説得力ないかもですが、こういう事は一か所に集中させてあまり散らさないほうが良いという事でこういう拡張メソッドで集中させるのが良いという訳です。

これによって HttpApplication.Events に格納されたイベントハンドラにアクセスする事が簡単にできる様になりました。 それと同時に EventHandlerList のRemoveHandler / AddHandler を使えばイベントハンドラの差し替えが出来そうに見えます。

結果的にはこのアプローチはうまく行かないのですが、とりあえずコード化してみましょう。それがみなさんを ASP.NET Core の闇に招待する方法だからです。

やろうとする事を単純にする為に HttpApplication.Init をオーバライドして BeginRequest をハンドルします。これを EventHandlerList から検索し RemoveHandler / AddHandler で差し替えますが検索ロジックを作り上げるのも面倒なのでデバッガへトレースメッセージを送りつけるだけで何もしないロジックを間に挟み込む事にします。(何しろ比較しようにも困る object と、 Delegate が出てくるわけで上手くいかない事にそこまでやってられません。)

public override void Init()
{
    base.Init();

    BeginRequest += OnMvcApplicationBeginRequest;

    var eventDic = Events.ToDictionary();
    foreach (var ev in eventDic)
    {
        object key = ev.Key;
        Delegate handler = ev.Value;
        Events.RemoveHandler(key, handler);
        Events.AddHandler(key, WrapHandler((EventHandler) handler));
    }
}

private Delegate WrapHandler(EventHandler eventHandler)
{
    EventHandler wrapedHandler = (s, e) =>
    {
        Debug.WriteLine("Begin event");
        try
        {
            eventHandler(s, e);
        }
        finally
        {
            Debug.WriteLine("End event");
        }
    };
    return wrapedHandler;
}

void OnMvcApplicationBeginRequest(object sender, EventArgs e)
{
    Debug.WriteLine("BeginRequest");
}

これを動かした結果のデバッガ出力は以下。

image

見事に BeginRequest しか出ません。

なぜかというと ASP.NET Core は EventHandlerList を使ったイベントの発行をすでにやってないから、内部的にもっと高速にイベントを発行できる仕組みを用意しているからです。結果として EventHandlerList を操作した所で知った事なし。

実際にどうなってるのかは ASP.NET Core ソースを見てもらうのが一番ですが、 BeginRequest の event の add では HttpApplication のinternal メソッド AddSyncEventHookup によってSyncEventExecutionStepが構築され、PipelineModuleStepContainer に保持されます。

要するに見た目として .NET Framework の event / delegate によって処理が制御されている様に見えて全く異なるメカニズムで処理が実行されているわけです。

理由としては単純にパフォーマンスでしょう。Delegate の呼び出し等はdelegate の差し先が変わる事等を色々考慮すると JIT での最適化について色々と制約が生まれてしまいます、個々のイベントハンドラの呼び出しでは微々たるロスかもしれませんがアプリケーション全体としては非常に大きなパフォーマンスペナルティを生み出すわけでこれを避ける為に ASP.NET Core チームはそれなりに仕事しています。

ここまでのコードが全然無駄に見えるかもしれませんが、ちゃんと使いますのでもう少し先に進みましょう。

EventHandlerList によるイベント格納を操作する事で割り込む事は出来ない事は確かですが、 ハンドラを正規のaddメソッドを使ってちゃんと add してあげれば良いのですからこれをする方法を考えましょう。

単純に言えば EventHandlerList から取得した EventHandler を add するのは簡単です+= で渡すだけですから。同様にイベントハンドラの解除も簡単ですね –= でremove を呼べば良いわけです。

難点としてはさっき避けて通ったEventHandlerList のキー object を結局取るしかないよねって所、そんなもんは単純にリフレクションで取ればいいんだからいいやですね。

それ以外の問題としては一つは remove して add するとイベントの発行順が変わってしまうという事、これも全部を remove add する事で順序を維持すれば良いですね。

で、 Async EventHandlers が問題として残ります。例えば AddOnBeginRequestAsync 等は EventHandlerList には入らないし、 remove 方法がありません。

EventHandlerList に入らないって事はどうやって取得するの?には決まってんだろ、いわせんな恥ずかしい。って事にしてください。

結果として remove 方法が無いのをどうするって事だけが課題として残ります。

ここで HttpApplication が sealed でなく単純にnew可能なクラスである為、Fake に登録させるという方法があります。

長ったらしくなりそうなので、回を分けるという事にしました。Fake にイベントハンドラ登録させて Fake を舐めてイベントハンドラに割り込むのは次回という事で、ばいばーい、また来週、またみてねー!?(来週には書くという予告?)

広告

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト / 変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト / 変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト / 変更 )

Google+ フォト

Google+ アカウントを使ってコメントしています。 ログアウト / 変更 )

%s と連携中

%d人のブロガーが「いいね」をつけました。