WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

ジェネリックハンドラー

by WebSurfer 2014年12月16日 14:42

Web サイトプロジェクトでジェネリックハンドラー(.ashx)を作成する際、コードビハインド形式では作成できないのですが、それは何故かについて書きます。(どうでもいい話かもしれませんが)

ジェネリックハンドラーの作成

上の画像は Visual Studio 2010 のソリューションエクスプローラーから「新しい項目の追加」ダイアログを開いてジェネリックハンドラーを追加しようとしているところです。[別のファイルにコードを書き込む(P)]のオプションがグレーアウトされて選択できないのがわかるでしょうか? そのまま作業を進めると、.ashx ファイルのみが生成されます。

一方、Web アプリケーションプロジェクトの場合は[別のファイルにコードを書き込む(P)]というようなオプションはそもそも表示されません(ジェネリックハンドラに限らず Web フォームなど他の項目も同様ですが)。自動的にコードビハインド形式になります(.ashx ファイルと .ashx.cs ファイルが作成されます)。

(注:Web アプリケーションプロジェクトと Web サイトプロジェクト の違いについては MSDN ライブラリの記事をリンクしたのでそこを見てください)

Web サイトプロジェクトでコードビハインド形式で作成できない理由が書いてある Microsoft の文書は見つけられなかったのですが、MSDN ライブラリ HTTP ハンドラの概要 のページの「ファイル名拡張子の作成」のセクションの以下の記述が関係ありそうです。

"ファイル名拡張子 .ashx に対応する HTTP ハンドラクラスを作成した場合、ハンドラは IIS と ASP.NET に自動的に登録されます。"

"ハンドラに対応するカスタムファイル名拡張子を作成する場合は、この拡張子を IIS と ASP.NET に明示的に登録する必要があります。"

引用文の前者がこの記事で言うジェネリックハンドラーのことで、HTTP ハンドラ本体(IHttpHandler を継承するクラス)とハンドラマッピング(@ WebHandler ディレクティブと Class 属性定義)を同一ファイル内に実装して .ashx という拡張子を付与し、IIS と ASP.NET に自動的に登録されるようにしたものです。

引用文の後者は、一般的な HTTP ハンドラのことで、HTTP ハンドラ本体を普通のクラスファイルに実装し、ソースコードのまま App_Code フォルダに置くか、コンパイルして dll を作って Bin フォルダに配置し、web.config にハンドラマッピングを定義するということになります。

つまり、ジェネリックハンドラーは、いちいち web.config にハンドラマッピングの定義を書かなくても、例えば <img src=handler.ashx ... /> のようにするだけで簡単に使えます。

コードビハインド形式にする(.ashx ファイルと .ashx.cs ファイルに分ける)のは取り扱いが面倒になるだなので、Web サイトプロジェクトでジェネリックハンドラーをコードビハインド形式で作成するオプションがないのだと思います。

Web アプリケーションプロジェクトではコード部分を分けざるを得ませので、選択の余地なく .ashx ファイルと .ashx.cs ファイルが分けて生成されます。ただし、Visual Studio からは .ashx.cs ファイルしか開けません(ファイルは存在しますが Visual Studio 上では開けません)。


以下は余談ですが、「コードビハインド」という呼び方について一言・・・

本来、HTML デザインブロックの背後にある ASP.NET 処理ブロック(.aspx.cs, .master.cs, .ascx.cs などに定義されるクラス)をコードビハンドと呼ぶようです。なので、.ashx ファイルの場合はコードビハンドという呼び方ではなく「HTTP ハンドラ本体」と言う方が適切かもしれません。

また、@ Page ディレクティブの CodeBehind 属性の説明を読むと "この属性は、以前のバージョンの ASP.NET との互換性を保持して、分離コード機能を実装するために用意されています。ASP.NET Version 2.0 では、代わりに CodeFile 属性を使用してソース ファイルの名前を指定すると同時に、Inherits 属性を使用してクラスの完全修飾名を指定します" とのことです。

@ WebHandler ディレクティブにも CodeBehind 属性はありますが、MSDN ライブラリによると "この属性は実行時には使用されません。この属性は、以前のバージョンの ASP.NET と互換性を保つために含まれています" とのことです。

なので「コードビハインド」という呼び方自体が古いのかもしれません。

Tags:

ASP.NET

ダウンロードは HTTP ハンドラで

by WebSurfer 2013年2月16日 16:50

画像、データ、ファイルなど、Page でないコンテンツをダウンロードするには、.aspx ページを利用するより、HTTP ハンドラ(.ashx)を利用したほうが良さそうだという話です。

.aspx ページを利用してファイルをダウンロードするサンプルは、MSDN ライブラリなどでもよく目にしますが、いろいろ問題がありそうです。

MSDN ライブラリの HttpResponse クラス にある画像をダウンロードするサンプルコードを例に取って問題点を説明します。

このコードでは、まず bmp.Save(Response.OutputStream, ImageFormat.Jpeg) メソッドによって画像データが jpeg 形式で出力ストリームに保存され、その後 Response.Flush メソッドによって保存された画像データが chunked コーディングされてクライアントに送信されます。

そのあと直ちに、データの終了を示す 0 が送信されればいいのですが、サーバーは <!DOCTYPE ... で始まる html コードも生成し、それもクライアントに送信してから終了します。

従って、jpeg 形式のデータとしては余計な html コードが含まれてしまいます。IE9 で試した限りでは画像は表示されましたが、すべてのブラウザで問題ないかは保障の限りではありません。

以下のような解決策を考えましたが、.aspx ページを利用する限り決定打はなさそうです。

  1. Response.Flush の後、Response.Close する ⇒ 時々そのようなサンプルを目にしますが、これは解決策というよりは改悪です。Response.Close は "クライアントへのソケット接続を閉じます" ということなので、 chunked コーディングでデータの終了を示す 0 が送信されないまま終了してしまいます。IE9 でしか試してませんが、画像は表示されません。
  2. Response.Flush に替えて Response.End を使う ⇒ 応答ヘッダに Content-Length が指定されて(chunked コーディングされず)、データはまとめて送信されます。送信されるデータの内容も OK です。ただ��、最近知ったのですが、.NET 4 以降の MSDN ライブラリの説明で、End メソッドの使用は非推奨になっています。理由は、End メソッドでスローされる ThreadAbortException がパフォーマンスに悪影響を及ぼすからだそうです。代わりに HttpApplication.CompleteRequest メソッドを呼び出せとのことです。
  3. Response.Flush に替えて HttpApplication.CompleteRequest を呼び出す ⇒ やはり <!DOCTYPE ... で始まる html コードも送信されてしまいます。Response.Flush と異なる点は、chunked コーディングされず、Content-Length が指定されてデータが(html コードも含めて)まとめて送信されることです。
  4. html コードを全部削除し、Flush メソッドも End メソッドも使わない ⇒ これは見かけよさそうです。余計な html コードはくっついてきません。
  5. Response.Flush の後 Response.SuppressContent を true に設定する(2016/6/28 追記) ⇒ そうすると画像のみが送信され、<!DOCTYPE html... 以下は送信されません。ただし、Response.Flush の前で true に設定するとコンテンツは一切送信されない(ヘッダと chunked の終わりを示す 0 のみ送信される)ので注意してください。

.aspx をページを使う場合は上記 4 または 5 の方法がよさそうですが、普通と違うことをして思わぬところで副作用が出る可能性が否定しきれません。.aspx をページを使って余計な心配をするより、代わりに http ハンドラ(.ashx)を使った方が良さそうです。http ハンドラなら、Response.End メソッドで強制する等の処置は必要なく、HTTP パイプラインの最後まで普通に実行させれば済みます。

http ハンドラを使ったサンプルを書いておきます。コメントに注意事項を書いたので参考にしてください。

(2014/3/2 追記:IE の場合は UrlEncode を使ってファイル名をエンコードしていますが、半角空白は "+" に変換されるので、ブラウザ側ではそのまま "+" になってしまいます。それが気に入らない場合は、ダウンロードファイル名の文字化け に紹介したサンプルコードを参考に対処してください)

<%@ WebHandler Language="C#" Class="Handler" %>

using System;
using System.Web;

public class Handler : IHttpHandler 
{
    public void ProcessRequest (HttpContext context) 
    {
        HttpResponse response = context.Response;
        HttpRequest request = context.Request;
        
        string fileName = "日本語.txt";        
        
        // IE の場合、日本語ファイル名の文字化け対策が必要。
        // Firefox, Chrome, Safari, Opera の場合は不用。

        // 2014/3/3 修正
        // IE11 では Browser プロパティは "Mozilla" (.NET 2.0)
        // または "InternetExplorer" (.NET 4) になる。IE の場合
        // User Agent には必ず "Trident" と言う文字列が入ってい
        // るらしいので、そちらで判定した方がよさそう。
        if (request.Browser.Browser.ToUpper().IndexOf("IE") >= 0
            || request.UserAgent.Contains("Trident"))
        {
            fileName = context.Server.UrlEncode(fileName);
        }
        
        // キャッシュを許可するか否か、許可する場合は有効期限を
        // 指定しておくべき。
        // 以下のコードはキャッシュを許可しない場合の例。応答ヘ
        // ッダーは次のようになる。
        //    Cache-Control: no-cache
        //    Pragma: no-cache
        //    Expires: -1
        response.Cache.SetCacheability(HttpCacheability.NoCache);
        response.Cache.SetExpires(DateTime.Now.ToUniversalTime());
        response.Cache.SetMaxAge(new TimeSpan(0, 0, 0, 0));

        // ブラウザによってファイルの種類を判断する方法が異なる。
        // IE は、Content-Disposition: ヘッダが存在する場合は、
        // filename パラメータで設定されたファイル名の拡張子を
        // 優先的に使う。Content-Type: ヘッダの指定は無視される。
        // 逆に、Opera など、Content-Type: ヘッダの指定を優先的
        // に使うものもある。なので、Content-Disposition: ヘッダ
        // と Content-Type: ヘッダの両方を正しく指定しておいた方
        // がよさそう。        
        response.AppendHeader("Content-Disposition",
            "attachment;filename=" + fileName);
        response.ContentType = "text/plain";

        // 文字列 "Hello World" を応答 HTTP ストリームに書込み。
        // ファイルの場合は TransmitFile メソッドを使うのがお勧
        // め(WriteFile メソッドは大きなファイルは扱えないので)
        response.Write("Hello World");
        
        // Flush, End, Close メソッド等は使用しないこと。
    }
 
    public bool IsReusable 
    {
        get 
        {
            return false;
        }
    }

}

呼び出し方は、a 要素の href 属性に HTTP ハンドラの URL を設定するのがよさそうです。

Tags: ,

Upload Download

非同期 HTTP ハンドラ

by WebSurfer 2012年9月23日 13:18
2018/3/8 追記:
この記事と同じ機能を持つ非同期 HTTP ハンドラを .NET 4.5 から利用できるようになった async / await を利用して実装してみました。その記事は「非同期 HTTP ハンドラ (2)」にありますので興味があれば見てください。この記事の方法が時代遅れなのが分かると思います。

ASP.NET の非同期プログラミングモデルには、(1) 非同期ページ、(2) 非同期 HTTP ハンドラ、(3) 非同期 HTTP モジュールの 3 つがあります。このうち、(2) 非同期 HTTP ハンドラを使って Web サービスのメソッドを非同期に呼び出す場合について、いろいろ不明な点があったので、それらを調べて分かったことを備忘録として書いておきます。

MSDN マガジンの March 2007 の記事「ASP.NET の非同期プログラミングを使ったスケール変換可能なアプリケーション (Wicked Code: Scalable Apps with Asynchronous Programming in ASP.NET)」が、非同期プログラミングの目的、メリット、仕組みなどをサンプルコードを使って詳しく説明しており、大変参考になりました。(2017/5/20 追記:記事は .chm ファイル形式で保存されており、MSDN Magazine Issues and Downloads からダウンロードして読むことができます)

(2015/12/29 追記:非同期プログラミングのメリット、仕組みなど基本的な説明を書いた新しい記事 ASP.NET の非同期/待機の概要を紹介しておきます。.NET 4.5 以降で利用できる async / await を使うことが前提です)

ただし、その中の非同期 HTTP ハンドラのサンプルは複雑すぎて、その仕組みなどがよく分かりませんでした。特に分からなかったのは以下の点です。

  1. Web サービスのメソッドを非同期で呼び出すメソッドをどのように生成するか(例えば、Web サービスのメソッドが HelloWorld だったとすると、BeginHelloWorld と EndHelloWorld をどのように作成するか)。
  2. 非同期 HTTP ハンドラのコードで非同期呼び出しのためのメソッド(上の例で言うと、BeginHelloWorld メソッドと EndHelloWorld メソッド)をどのように呼び出すか。
  3. BeginProcessRequest メソッドの第 2 引数として渡されるコールバックメソッドのデリゲートは何か。
  4. EndProcessRequest メソッドが呼び出される仕組みとそのタイミングはどうなっているか。

という訳で、簡単なサンプルを作って上記の点について調べてみました。下の画像は、説明のためにここに紹介したサンプルを実行したときのものです。

非同期 HTTP ハンドラの使用

サンプルとして、以下の Web サービスを非同期 HTTP ハンドラを使って利用することを考えます。これに定義されているのは HelloWorld メソッドだけです。同期 HTTP ハンドラではこれを呼べば済みますが、非同期 HTTP ハンドラを使う場合は非同期で HelloWorld メソッドを呼び出し、結果を処理しなければなりません。

非同期呼び出しのために、BeginHelloWorld メソッドと EndHelloWorld メソッドが必要ですが、それらはどのように作ればよいのでしょうか?

(1) Web サービス (141-HelloWorldWebService.asmx)

<%@ WebService Language="C#" 
    Class="_141_HelloWorldWebService" %>

using System;
using System.Web;
using System.Web.Services;
using System.Web.Services.Protocols;
using System.Threading;

[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
public class _141_HelloWorldWebService  : WebService {

  [WebMethod]
  public string HelloWorld(int callDuration) 
  {
    Thread.Sleep(callDuration);
    return String.Format(
      "Hello World from WebService. Call Time: {0}",
      callDuration);
  }    
}

答えはサービスプロキシを定義することです。と言っても、自力でコードを書く必要はなく、Visual Studio を使ってサービス参照を追加するか、または、SDK にある Wsdl.exe ツールを利用してサービスプロキシクラスを自動生成すれば OK です。ここでは、後者の Wsdl.exe ツールを利用した例を書きます。

Wsdl.exe ツールの詳しい使用方法は、MSDN ライブラリの XML Web サービス プロキシの作成 にあります。これを参考に、上記の Web サービスの URL とプロキシのコードの出力先のファイル名をパラメータとして、コマンドラインから Wsdl.exe を実行します。以下の画像のような感じです。

wsdl.exe を使ってプロキシクラスを生成

この結果、上記 (1) の Web サービスからプロキシクラス HelloWorldWebService.cs が自動生成されます。コードは以下のとおりです(一部省略、改行等をしています)。

(2) Web サービスプロキシ (HelloWorldWebService.cs)

using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Web.Services;
using System.Web.Services.Protocols;
using System.Xml.Serialization;

[System.CodeDom.Compiler.GeneratedCodeAttribute(
  "wsdl", "2.0.50727.3038")]
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.ComponentModel.DesignerCategoryAttribute("code")]
[System.Web.Services.WebServiceBindingAttribute(
  Name="_141_HelloWorldWebServiceSoap", 
  Namespace="http://tempuri.org/")]
public partial class _141_HelloWorldWebService : 
  System.Web.Services.Protocols.SoapHttpClientProtocol 
{
  private System.Threading.SendOrPostCallback 
    HelloWorldOperationCompleted;
    
  public _141_HelloWorldWebService() 
  {
    this.Url = 
      "http://msdntestnew/141-HelloWorldWebService.asmx";
  }
    
  public event HelloWorldCompletedEventHandler 
    HelloWorldCompleted;

  [System.Web.Services.Protocols.SoapDocumentMethodAttribute(
    "http://tempuri.org/HelloWorld", 
    RequestNamespace="http://tempuri.org/", 
    ResponseNamespace="http://tempuri.org/", 
    Use=System.Web.Services.Description.SoapBindingUse.Literal,
    ParameterStyle=
      System.Web.Services.Protocols.SoapParameterStyle.Wrapped)]
  public string HelloWorld(int callDuration) 
  {
    object[] results = this.Invoke(
        "HelloWorld", new object[] { callDuration });
    return ((string)(results[0]));
  }
    
  public System.IAsyncResult BeginHelloWorld(
    int callDuration, 
    System.AsyncCallback callback, 
    object asyncState) 
  {
    return this.BeginInvoke(
      "HelloWorld", 
      new object[] { callDuration }, 
      callback, 
      asyncState);
  }
    
  public string EndHelloWorld(System.IAsyncResult asyncResult) 
  {
    object[] results = this.EndInvoke(asyncResult);
    return ((string)(results[0]));
  }

  // ・・・中略・・・

}

上に示したプロキシクラスに、SoapHttpClientProtocol.BeginInvoke メソッドSoapHttpClientProtocol.EndInvoke メソッド をラップした BeginHelloWorld メソッドと EndHelloWorld メソッドが生成されているのが分かるでしょうか?

このファイル HelloWorldWebService.cs を、Web アプリケーションのルート直下の App_Code フォルダに置けばプロキシは使用可能になり、それに定義されている BeginHelloWorld メソッドと EndHelloWorld メソッドを利用して、以下のように非同期 HTTP ハンドラを作成できます。

(3) 非同期 HTTP ハンドラ (141-HelloWorldAsyncHandler2.ashx)

<%@ WebHandler Language="C#" 
    Class="_141_HelloWorldAsyncHandler2" %>

using System;
using System.Web;
using System.Threading;

public class _141_HelloWorldAsyncHandler2 : IHttpAsyncHandler
{
  private HttpContext _context;
  private _141_HelloWorldWebService _serviceProxy;
    
  // これが呼び出されることはない。万一呼び出されたら例外
  // をスローして自爆する
  public void ProcessRequest (HttpContext context) 
  {
    throw new InvalidOperationException();
  }
 
  public bool IsReusable 
  {
    get { return false; }
  }

  // まず最初にこのメソッドが呼ばれる。
  // cb には ASP.NET が内部で生成したコールバックメソッド
  // void OnAsyncHandlerCompletion(System.IAsyncResult)
  // のデリゲートが渡される。これが EndProcessRequest メ
  // ソッドを呼び出す。extraData には null が渡される。
  public IAsyncResult BeginProcessRequest(
    HttpContext context,
    AsyncCallback cb,
    Object extraData)
  {
    this._context = context;

    context.Response.Write(
      "<p>BeginProcessRequest:<br />" +
      " IsThreadPoolThread is " +
      Thread.CurrentThread.IsThreadPoolThread +
      "<br />" +
      " ManagedThreadId is " +
      Thread.CurrentThread.ManagedThreadId.ToString() +
      "</p>");

    _serviceProxy = new _141_HelloWorldWebService();

    // 終了すると引数 cb に設定したコールバックデリゲート
    // が起動される。その中で EndProcessRequest メソッドが
    // 呼び出される。
    return _serviceProxy.BeginHelloWorld(3000, cb, null);
  }

  // 引数 ar には BeginProcessRequest メソッドの戻り値であ
  // る IAsyncResult オブジェクトが渡される。
  public void EndProcessRequest(IAsyncResult ar)
  {
    _context.Response.Write(
      "<p>EndProcessRequest:<br />" +
      " IsThreadPoolThread is " +
      Thread.CurrentThread.IsThreadPoolThread +
      "<br />" +
      " ManagedThreadId is " +
      Thread.CurrentThread.ManagedThreadId.ToString() +
      "</p>");

    string result = _serviceProxy.EndHelloWorld(ar);

    _context.Response.Write("<p>" + result + "</p>");
  }
}

非同期 HTTP ハンドラでは、BeginProcessRequest メソッドが最初に実行されます。引数 context には現在の HttpContext オブジェクトへの参照が、cb には ASP.NET が内部で生成したコールバックメソッドのデリゲートが、extraData には null が渡されます。

BeginProcessRequest メソッドでは、プロキシクラスを初期化して BeginHelloWorld メソッドを呼び出し、HelloWorld メソッドの非同期実行を開始します。非同期実行の開始後すぐに制御が戻って、ここまでの処理に使用していたスレッドはスレッドプールに戻されます。

BeginHelloWorld メソッドの第 1 引数は HelloWorld メソッドに渡されます(ここでは、待機時間 3000 即ち 3 秒を渡しています)。第 2 引数 cb には BeginProcessRequest メソッドの引数 cb に渡されたコールバックメソッドのデリゲートがそのままコピーされます。第 3 引数は BeginProcessRequest メソッドの戻り値、即ち EndProcessRequest メソッドの引数 ar に渡される IAsyncResult オブジェクトから、AsyncState プロパティを使って取得できるデータですが、ここでは使用しないので null としています。

ここで覚えておくべきことは、BeginHelloWorld メソッドの第 2 引数として渡されたコールバックメソッドのデリゲートが実行されると EndProcessRequest メソッドが実行されることです(逆に言えば、コールバックメソッドが実行されないとフリーズしてしまう)。

非同期処理の完了後、コールバックメソッドのデリゲートが実行され、EndProcessRequest メソッドが呼び出されます。その際、スレッドプールから空いているスレッドを取得して EndProcessRequest メソッドを実行します。この時、引数 ar には BeginProcessRequest メソッドの戻り値である IAsyncResult オブジェクトが渡されます。 EndProcessRequest メソッドの中で、EndHelloWorld メソッドを実行し、Web サービスからの応答をその戻り値から取得します。EndProcessRequest メソッドの終了後、制御がコールバックメソッドに戻って全体のタスクが終了します。

上記の非同期 HTTP ハンドラを iframe の src 属性に設定した aspx ページの例は以下の通りです。一番上の画像は、以下の aspx ページの実行結果です。

(4) aspx ページ (141-HelloWorldAsyncHandler.aspx)

<%@ Page Language="C#" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<script runat="server">

</script>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
  <title></title>
</head>
<body>
  <form id="form1" runat="server">
    <iframe src="141-HelloWorldAsyncHandler2.ashx" 
      id="iframe2"
      width="400px" 
      height="200px">
    </iframe>
  </form>
</body>
</html>

Tags:

ASP.NET

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。

Calendar

<<  2024年4月  >>
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

View posts in large calendar