先の記事「非同期 HTTP ハンドラ」で、Web サービスに非同期でアクセスする HTTP ジェネリックハンドラのサンプルを書きました。
その HTTP ハンドラは IHttpAsyncHandler インターフェイスを継承して BeginProcessRequest, EndProcessRequest メソッドを実装するという旧来の方法を使っており、記事に書いてありますように非常に複雑なコードになります。
ここまで複雑なことをしなければならないのなら、HTTP 503 エラー (Server Too Busy) が頻発して困っているというような事情がなければ、従来通り同期版のままでもいいかなって気もします。(笑)
ですが、.NET 4.5 から簡単に非同期呼び出しが実装できるようになったそうです。どのぐらい簡単にできるのか、同じ機能を async / await を利用して実装してみました。
確かにはるかに簡単でした。前のように、何がどうなっているのかを調べて、悩んで、時間をかけて実装するという手間は大幅に減っています。
上の画像は、この記事に書いた方法で作成した HTTP ハンドラを、ブラウザから直接呼び出した結果です。具体的にどのように実装したかは以下に書きます。
HelloWorldWcfService.cs
先の記事の Web サービスは WCF サービスに変更しました。HelloWorld メソッドの実装は先の記事の Web サービスのものと全く同じです。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
using System.Threading;
using System.ServiceModel.Activation;
[AspNetCompatibilityRequirements(RequirementsMode =
AspNetCompatibilityRequirementsMode.Allowed)]
public class HelloWorldWcfService : IHelloWorldWcfService
{
public string HelloWorld(int callDuration)
{
Thread.Sleep(callDuration);
return String.Format(
"Hello World from WcfService. Call Time: {0}",
callDuration);
}
}
WCF サービスに変更したのは、Web サービスが Legacy Technology だからということもありますが、一番の理由はサービス参照の追加を行うと自動生成されるサービスプロキシに含まれる非同期版メソッドが利用できるからです。
(非同期版メソッドについて、詳しくは先に記事「WCF サービスの非同期呼び出し」を見てください)
HelloWorldAsyncHnadler.ashx
Visual Studio で、既存の ASP.NET Web Forms アプリにジェネリックハンドラーを追加します。.ashx ファイルには何も手を加える必要はありません。
<%@ WebHandler Language="C#"
CodeBehind="HelloWorldAsyncHandler.ashx.cs"
Class="WebFormsApp.HelloWorldAsyncHandler" %>
Web アプリケーションプロジェクトの場合、.ashx ファイルとそのコードビハインドの .ashx.cs ファイルは別になります。Web サイトプロジェクトの場合、すべて .ashx ファイルに含まれるという違いがあります。
HelloWorldAsyncHandler.ahsx.cs
自動生成されたコードは同期版ハンドラのベースとなるものです。これを HttpTaskAsyncHandler クラスを継承したクラスに書き換えます。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Threading;
using System.Threading.Tasks;
using WebFormsApp.HelloWorldServiceReference;
namespace WebFormsApp
{
public class HelloWorldAsyncHandler : HttpTaskAsyncHandler
{
HelloWorldWcfServiceClient client;
// これが呼び出されることはない。万一呼び出されたら例外
// をスローして自爆する
public override void ProcessRequest(HttpContext context)
{
throw new InvalidOperationException();
}
public override async Task ProcessRequestAsync(
HttpContext context)
{
context.Response.Write(
"<p>Before await:<br />" +
" IsThreadPoolThread is " +
Thread.CurrentThread.IsThreadPoolThread +
"<br />" +
" ManagedThreadId is " +
Thread.CurrentThread.ManagedThreadId +
"</p>");
client = new HelloWorldWcfServiceClient();
string result = await client.HelloWorldAsync(3000);
context.Response.Write(
"<p>After await:<br />" +
" IsThreadPoolThread is " +
Thread.CurrentThread.IsThreadPoolThread +
"<br />" +
" ManagedThreadId is " +
Thread.CurrentThread.ManagedThreadId +
"</p>");
context.Response.Write("<p>" + result + "</p>");
}
public override bool IsReusable
{
get
{
return false;
}
}
}
}
上のコードの WebFormsApp.HelloWorldServiceReference はサービスプロキシの名前空間です。HelloWorldWcfServiceClient コンストラクタ、HelloWorldAsync メソッドの定義は自動生成される Reference.cs ファイルにあります。
結局は先の記事「非同期 HTTP ハンドラ」と同様なことを行っていて、先の記事のコードの BeginProcessRequest メソッドが await キーワードの前に、EndProcessRequest メソッドが await の後に実行されているようです。
BeginProcessRequest, EndProcessRequest メソッドをプログラマがコードを書いて実装しなくて済むよう、コンパイラが肩代わりしているという感じです。