ASP.NET MVCでの非同期処理 - AsyncControllerを使った方法


ASP.NET MVCで非同期処理をする場合、通常のajax()を使ったやり方以外にAsyncControllerクラスを利用するやり方があります。
AsyncControllerクラスを使ったやり方の方が、既存の処理を簡単に非同期化できます。

こんなプログラムを作ってみました

非同期処理のサンプルプログラムとして、下図のようなプログラムを作ってみました。これを元に説明していきます。

準備

通常ASP.NET MVCのコントローラー側のコードは、"Controller"クラスを継承して作成します。
ですが、非同期処理対応のコントローラーのコードは、"AsyncContoller"クラスを継承するようにします。

AsyncTestController.cs
    public class AsyncTestController : AsyncController //← "Controller"クラスではなく、"AsyncController"クラスを継承する。
    {
        // GET: AsyncTest
        public ActionResult AsyncTest(AsyncTestModel param)
        {
            return View(param);
        }

        ....

継承元のクラスを変更したら、非同期処理特有の命名規約に従ってコードを書きます。

命名規約と実行順

ASP.NET MVCはクラス名やメソッド名の命名規約に基づいて動作します(この辺の知識については@ITの記事などが参考になります)。
非同期処理についても、その処理シーケンス(=メソッドの呼ばれる順番)については、命名規約に基づいたものになります。まず、これを頭に入れておく必要があります。

以下に、命名規約と処理シーケンスを図示します。

上図を見るとわかるように、通常のASP.NET MVCと違って、メソッド名(=Action名)に"Async"や"Completed"のような接尾辞がついています。

(1) submit実行
通常のASP.NET MVCと同様、Action名とController名を指定します。これは非同期であっても同じです。

AsyncTest.cshtml
@using (Html.BeginForm("ExecProcess", "AsyncTest", FormMethod.Post))
{                    // ↑ Action名     ↑ Controller名

}

(2) Action名+Async()という名前のメソッドが実行される
Html.BeginForm(Action名, Controller名, メソッド種別)で指定したAction名に、接尾辞"Async"が付与されたメソッドが実行される。

AsyncTestController.cs
[HttpPost]
public void ExecProcessAsync(AsyncTestModel param)
{           // ↑ "Action名+Async"という名前のメソッドが実行される
    <重たい処理>
}

(3) (2)の処理の終了後、Action名+Completed()という名前のメソッドが実行される
上記のAction名+Async()というメソッドの中に書かれた処理が完了した後、Action名+Completed()という名前のメソッドが実行されます。

処理完了の契機となるタイミングについては後述します。

AsyncTestController.cs
public ActionResult ExecProcessCompleted(AsyncTestModel result)
{                    // ↑ "Action名+Completed"という名前のメソッドが実行される

     return RedirectToAction("AsyncTest", "AsyncTest", result);
}

この後、ビュー側に処理が戻り、非同期処理完了です。

この例からも分かるように、既存の処理を非同期化することも簡単にできます。

Action名+Completed()が実行されるタイミング

Action名+Async()の処理が終了したらAction名+Completed()が呼ばれると書きましたが、Action名+Async()の終了タイミングを管理しているのがAsyncManager.OutstandingOperationsです。

これは非同期処理が保留している処理の数を管理しているオブジェクトです。保留している処理が完了するたびに、このオブジェクト中のDecrement()メソッドを実行して-1します。
このカウンタが0になったら、処理終了と判断してCompleted()メソッドが呼ばれます。

今回の例では処理が1つだけなので、Action名+Async()メソッド実行時にIncrement()メソッドを実行して+1します。処理が完了したらDecrement()処理を実行し、-1してAction名+Competed()メソッドの処理が呼ばれるようにしています。

AsyncTestController.cs
[HttpPost]
public void ExecProcessAsync(AsyncTestModel param)
{
    AsyncManager.OutstandingOperations.Increment();  // AsyncManager.OutstandingOperationsを +1 する

    var testTask = Task.Run(() =>
    {
        Thread.Sleep(5000);

        if (AsyncManager.Parameters.ContainsKey("result"))
            AsyncManager.Parameters["result"] = param;
        else
            AsyncManager.Parameters.Add("result", param);

        AsyncManager.OutstandingOperations.Decrement(); // AsyncManager.OutstandingOperationsを -1 する。このカウンタが 0 になったら処理終了とみなされ、Action名+Competed()メソッドが実行される
    });
}

Action名+Async()メソッドからAction名+Completed()メソッドへの引数の渡し方

Action名+Async()の処理終了後、Action名+Completed()にデータを引数の形で渡したい場合、AsyncManager.Parametersを用います。

ここで注意が必要なのが、key名です。

Action名+Completed()の引数の変数名は、AsyncManager.Parametersに指定したkey名と同一の名前でないと正しく受け取れません。別の変数名だと、nullになってしまいます。

AsyncTestController.cs
[HttpPost]
public void ExecProcessAsync(AsyncTestModel param)
{
    AsyncManager.OutstandingOperations.Increment();

    var testTask = Task.Run(() =>
    {
        Thread.Sleep(5000);

        if (AsyncManager.Parameters.ContainsKey("result")) // AsyncManager.Parametersに、key名を付けてパラメータを入れる。
            AsyncManager.Parameters["result"] = param;
        else
            AsyncManager.Parameters.Add("result", param);

        AsyncManager.OutstandingOperations.Decrement();
    });
}

public ActionResult ExecProcessCompleted(AsyncTestModel key) // AsyncManager.Parametersに指定したkey名で引数を受とる。
{
    key.Finished = true;
    key.Message = "終わりました";

    return RedirectToAction("AsyncTest", "AsyncTest", key);
}

命名規約に色々制限がありますが、比較的楽に非同期化できます。