ASP.NET MVC拡張非同期アクション機能(下)

15635 ワード

この文書は上下2つの部分に分かれており、『Extend ASP.NET MVC for Asynchronous Action』からすべての内容を入手することもできます.

アクションメソッドの実行


同期アクションを実行するSyncMvcHandlerの場合、その実装は非常に簡単で直接的です.
public class SyncMvcHandler : IHttpHandler, IRequiresSessionState
{
    public SyncMvcHandler(
        IController controller,
        IControllerFactory controllerFactory,
        RequestContext requestContext)
    {
        this.Controller = controller;
        this.ControllerFactory = controllerFactory;
        this.RequestContext = requestContext;
    }

    public IController Controller { get; private set; }
    public RequestContext RequestContext { get; private set; }
    public IControllerFactory ControllerFactory { get; private set; }

    public virtual bool IsReusable { get { return false; } }

    public virtual void ProcessRequest(HttpContext context)
    {
        try
        {
            this.Controller.Execute(this.RequestContext);
        }
        finally
        {
            this.ControllerFactory.ReleaseController(this.Controller);
        }
    }
}

非同期アクションについては,フレームワークのデフォルト実装,すなわち単一メソッド呼び出しをどのように2つのメソッド(BeginXx/EndXxx)呼び出しに変換するかを考えていた.自分で新しいActionInvokerを実現したいと思っていましたが、特にフレームワークの既存の機能(ActionFilter、ActionSelectorなど)を維持したい場合、最も省力的な方法はControllerActionInvokerを継承し、フレームワークが実現した様々な補助方法を使用することかもしれません.しかし、フレームワークコードを分析した後、多重化も非常に困難であることを発見しました.例えば、ControllerActionInvokerは、この方法がActionの根拠の一つであることを判定しました.この方法はActionResultタイプまたはそのサブクラスを返します.これは、IAsyncResultを返すBeginXxx方法を直接取得するためにこの方法を使用することができないことを意味します.同様に、EndXxxメソッドを検索するには、Abcという非同期Actionを要求するときに、EndAbcを検索根拠として既存のメソッドにクエリーする必要があるかもしれません.しかし、EndAbcという同期Actionに対して直接要求がある場合はどうすればいいのでしょうか.
これらの問題があるため、私は昨年、非同期Actionを実現しようとしたとき、ActionInvoker全体をほとんど書き直しました.その複雑さは明らかです.また、その実現はいくつかの特殊な状況の処理に対して依然として友好的ではなく、開発者がある程度妥協する必要がある.この実装はTechED 2008 ChinaのSessionで発表されたとき、私はそれが私を満足させることができないことを認め、生産環境に投入しないことをお勧めします.現在の実現は、問題全体を順調に解決した.理論的には「完璧」ではないが、少し譲歩した.
このような多くの問題をもたらす原因は,フレームワーク内部の重要な設計,すなわち単一のActionメソッド呼び出しから「APMに適合する」二段式呼び出しに転換することにある.など、問題解決の鍵を感じていますか?そう、それは「APMに合っている」ということです.APMは私たちに1つの行為をBeginXxxとEndXxxの2つの方法に分けるように要求したが、ASP.NET MVCフレームワークでは、ActionResultオブジェクトを返すしかありません.では、なぜメソッドの参照、つまり依頼オブジェクトをこのオブジェクトに含まないのでしょうか.これは正統なAPM署名に合わないが、完全に可能ではないか.
public class AsyncActionResult : ActionResult
{
    public AsyncActionResult(
        IAsyncResult asyncResult,
        Func<IAsyncResult, ActionResult> endDelegate)
    {
        this.AsyncResult = asyncResult;
        this.EndDelegate = endDelegate;
    }

    public IAsyncResult AsyncResult { get; private set; }

    public Func<IAsyncResult, ActionResult> EndDelegate { get; private set; }

    public override void ExecuteResult(ControllerContext context)
    {
        context.Controller
            .SetAsyncResult(this.AsyncResult)
            .SetAsyncEndDelegate(this.EndDelegate);
    }
}

ActionメソッドでBeginXxxメソッドを呼び出すことができるため、AsyncActionResultではBeginメソッドが返すIAsyncResultと、EndXxxメソッドに対する別の参照を保持するだけです.AsyncActionResultのExecuteResultメソッドには、AsyncMvcHandlerのEndProcessRequestメソッドで再取得して使用するための2つのオブジェクトが保存されます.「慣例」によれば、開発者がActionメソッドでAsyncActionResultを返すのに便利な拡張方法を定義する必要があります.具体的な実装は非常に簡単で、ここでは非同期Actionの作成方法を示します.
[AsyncAction]
public ActionResult AsyncAction(AsyncCallback asyncCallback, object asyncState)
{
    SqlConnection conn = new SqlConnection("...;Asynchronous Processing=true");
    SqlCommand cmd = new SqlCommand("WAITFOR DELAY '00:00:03';", conn);
    conn.Open();

    return this.Async(
        cmd.BeginExecuteNonQuery(asyncCallback, asyncState),
        (ar) =>
        {
            int value = cmd.EndExecuteNonQuery(ar);
            conn.Close();
            return this.View();
        });
}

これでAsyncMvcHandlerにも秘密はないようだ.
public class AsyncMvcHandler : IHttpAsyncHandler, IRequiresSessionState
{
    public AsyncMvcHandler(
        Controller controller,
        IControllerFactory controllerFactory,
        RequestContext requestContext)
    {
        this.Controller = controller;
        this.ControllerFactory = controllerFactory;
        this.RequestContext = requestContext;
    }

    public Controller Controller { get; private set; }
    public RequestContext RequestContext { get; private set; }
    public IControllerFactory ControllerFactory { get; private set; }
    public HttpContext Context { get; private set; }

    public IAsyncResult BeginProcessRequest(
        HttpContext context,
        AsyncCallback cb,
        object extraData)
    {
        this.Context = context;
        this.Controller.SetAsyncCallback(cb).SetAsyncState(extraData);

        try
        {
            (this.Controller as IController).Execute(this.RequestContext);
            return this.Controller.GetAsyncResult();
        }
        catch
        {
            this.ControllerFactory.ReleaseController(this.Controller);
            throw;
        }
    }

    public void EndProcessRequest(IAsyncResult result)
    {
        try
        {
            HttpContext.Current = this.Context;
            ActionResult actionResult = this.Controller.GetAsyncEndDelegate()(result);
            if (actionResult != null)
            {
                actionResult.ExecuteResult(this.Controller.ControllerContext);
            }
        }
        finally
        {
            this.ControllerFactory.ReleaseController(this.Controller);
        }
    }
}

現在のContextはBeginProcessRequestメソッドに保存されます.これは重要です.HttpContext.CurrentはCallContextに基づいており、一旦非同期コールバックHttpContextを通過すると、Currentはnullになり、リセットしなければなりません.次に、受信したAsyncCallbackとAsyncStateを保持し、フレームワークに既存のExecuteメソッドを使用してコントローラを実行します.Executeメソッドが返されると、Actionメソッド全体の呼び出しプロセスが終了します.これは、その呼び出し結果、すなわちIAsyncResultおよびEndDelegateオブジェクトが保持されていることを意味します.IAsyncResultオブジェクトを取り出して返します.EndProcessRequestメソッドについては,BeginProcessRequestメソッドに保存されているEndDelegateを取り出し,呼び出し,得られたActionResultをもう一度実行するだけでよい.
以上のコードは通常の場合の論理にのみ関与し、完全なコードにはActionメソッドがあるFilterによって終了または置換されるなどの特殊な場合の処理も含まれる.また、Controller FactoryがControllerオブジェクトを速やかに解放できるように、BeginProcessRequestにおいてもEndProcessRequestにおいても異常を適切に処理する必要がある.

ModelBinderサポート


メソッドのAsyncCallbackパラメータがnullであることがわかりますので、非同期Actionはまだ使用できません.これは、デフォルトのModel Binderでは、コンテキスト環境からAsyncCallbackオブジェクトを取得する方法が分からないためです.この点は非常に簡単で、AsyncCallbackModelBinderを構築するだけで、そのBindModel方法はAsyncMvcHandlerだけです.BeginProcessRequestメソッドに保存されているAsyncCallbackオブジェクトを取り出して返します.
public sealed class AsyncCallbackModelBinder : IModelBinder
{
    public object BindModel(
        ControllerContext controllerContext,
        ModelBindingContext bindingContext)
    {
        return controllerContext.Controller.GetAsyncCallback();
    }
}

アプリケーションの起動時にAsyncCallbackタイプとして登録するデフォルトのBinderを使用します.
protected void Application_Start()
{
    RegisterRoutes(RouteTable.Routes);
    ModelBinders.Binders[typeof(AsyncCallback)] = new AsyncCallbackModelBinder();
}

asyncStateパラメータについても同様の方法を使用できますが、objectタイプがあまりにも広いため、asyncStateパラメータを明確に指すことはできません.実際、asyncStateにbinderを設定しなくても、非同期ASPについては大きな問題はありません.NETリクエストでは、asyncStateは常にnullです.binderを指定する必要がある場合は、各ActionメソッドのasyncStateパラメータに次のAttributeをマークすることをお勧めします.AsyncStateModelBinderとともにプロジェクトに組み込まれています.
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
public sealed class AsyncStateAttribute : CustomModelBinderAttribute
{
    private static AsyncStateModelBinder s_modelBinder = new AsyncStateModelBinder();

    public override IModelBinder GetBinder()
    {
        return s_modelBinder;
    }
}

使用方法は次のとおりです.
[AsyncAction]
public ActionResult AsyncAction(AsyncCallback cb, [AsyncState]object state) { ... }

実際には、Controllerベースの拡張メソッドGetAsyncCallbackとGetAsyncStateは共通のメソッドであり、Actionメソッドがこの2つのパラメータを受け入れずに直接Controllerから取得できるようにすることもできます.もちろん、テスト性が低下し、提唱する価値はありません.

制限と欠点


このソリューションに欠陥がなければ、ASPに入れられたと信じています.NET MVC 1.0では、私がここで拡張する番ではありません.現在のソリューションでは、少なくとも以下の点が不足しています.
  • は厳格に守られていない.NETのAPMモードは、機能には影響しませんが、これはあくまでも残念です.
  • フレームワークの既存の機能を利用しているため、すべてのFilterはBeginXxxメソッドでのみ実行できます.
  • EndXxxメソッドも最終ActionResultの実行もFilterサポートされていないため、この過程で異常が投げ出すとASPに入ることができない.NET MVCが推奨する異常処理機能にあります.

  • ASPによる.NET MVCフレームワークのRoadmap,ASP.NET MVCフレームワーク1.0以降のバージョンでは非同期Actionがサポートされ、これらの欠陥はその時点で補うことができると信じています.しかし、これは大量の仕事が必要で、これはASPに任せるしかありません.NET MVCチームはゆっくりと実行しました.実際にはASP.でNET MVC RCソースのMvcFuturesプロジェクトでは、非同期アクション処理に関する内容が見つかります.IAsyncController、AsyncController、IAsyncActionInvoker、AsyncController ActionInvokerなど多くの拡張機能が追加されています.既存のクラスを「継承」していますが、私の以前の判断と似ています.例えば、AsyncControllerActionInvokerはほとんどActionInvokerの様々な機能を再実現しました.私はまだコードをよく読んでいないので、このような設計が優れているかどうかは判断できません.ASPのようにしてほしいだけです.NET MVC自体のようなシンプルさとエレガントさ.
    次に、現在のコードのEndXxxメソッドにもFilterサポートを加えるつもりですが、ASPをよく読む必要があります.NET MVCのソースコードでソリューションを探します.ASPになってほしいNET MVCは、非同期アクション以前の優れた代替案を正式にサポートしています.

    詳細


    完全なプロジェクトコードはMSDNコードGalleryに配置されており、ここではその「パフォーマンステスト」などの詳細にアクセスできます.この文章は拡張の設計原理に重点を置いて説明して、特殊な状況の処理とプログラムの丈夫性などの実現の細部の説明を省略して、コードをダウンロードして改善の提案を提出することを歓迎します.
     
    下一篇:はASPです.NET MVC拡張非同期アクション機能(上)