WebSurfer's Home

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

サーバー側で応答コンテンツの取得

by WebSurfer 2018年7月14日 13:26

ASP.NET Web アプリの応答コンテンツの文字列を、サーバー側でのログ取得などの目的で取得する方法を書きます。(あまり需要はないかもしれませんが)

参考にしたのは stackoverflow のスレッド Logging raw HTTP request/response in ASP.NET MVC & IIS7 です。その記事を見れば情報としては十分かもしれませんが、リンク切れになると困るし、記事にはない HTTP モジュールを使っての設定方法も追加して書いておきます。

基本的には、応答コンテンツは応答ストリームに含まれますのでそれを何らかの手段で取得するということになります。

stackoverflow の記事では、応答ストリームをラッピングするフィルターを作成し、フィルターの中に応答ストリームとは別に MemoryStream を用意し、ASP.NET が応答ストリームに書き込む際 MemoryStream にも同じ内容を書き込むようにし(要するに MemoryStream にコピーを作成し)、MemoryStream から応答コンテンツを取得するという方法が紹介されています。

下のコード例を見てください。その中の OutputFilterStream クラスがラッピングフィルターです。stackoverflow の記事のコードをそのままコピーしたものです。

HTTP モジュール(下のコードでは ResponseContentLogHttpModule クラス)を用い、BeginRequest イベントのタイミングでラッピングフィルターを HttpResponse.Filter プロパティに設定します。(注:EndRequest のタイミングではダメです。ストリームにはその前に書き込まれるので)

HttpResponse.Filter プロパティの NSDN ライブラリの説明には "伝送する前に HTTP エンティティ本体を変更するために使用される、ラッピングフィルターオブジェクトを取得または設定します" とありますが、OutputFilterStream はコンテンツの変更は一切せずそのまま応答として返し、MemoryStream にコピーを取得することのみ行います。

BeginRequest のタイミングで設定したラッピングフィルターオブジェクトを EndRequest のタイミングで取得し MemoryStream にコピーされた応答コンテンツを取得します。

BeginRequest のタイミングから EndRequest のタイミングまでラッピングフィルターオブジェクトを保持するには HttpContext.Items プロパティを利用します。下のコード例を見てください。

クラスのフィールド等で保持するのは NG という報告がありましたので注意してください。HttpContext.Items を使えば、少なくとも HttpContext が存在する限りラッピングフィルターオブジェクトは保持されるが(GC の対象にはならないが)、クラスのフィールド変数ではその限りではないということのようです。

以下に HTTP モジュールとラッピングフィルターのコード例をアップしておきます。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.IO;

// HTTP モジュール
public class ResponseContentLogHttpModule : IHttpModule
{
  public ResponseContentLogHttpModule()
  {
  }

  public String ModuleName
  {
    get { return "ResponseContentLogHttpModule"; }
  }

  public void Init(HttpApplication application)
  {
    application.BeginRequest += this.BeginRequest;
    application.EndRequest += this.EndRequest;
  }

  private void BeginRequest(Object source, EventArgs e)
  {
    HttpApplication application = (HttpApplication)source;
    HttpContext context = application.Context;
    string filePath = context.Request.FilePath;
    string fileExtension =
        VirtualPathUtility.GetExtension(filePath);

    // とりあえず .aspx のみ対象(.js, .css 等は対象外)
    if (fileExtension.Equals(".aspx"))
    {
      HttpResponse response = context.Response;
      var filter = new OutputFilterStream(response.Filter);
      response.Filter = filter;

      // OutputFilterStream オブジェクトを EndRequest で
      // 利用できるよう HttoContext.Items に参照を保持。
      // (注:クラスのフィールド等で保持するのは NG)
      context.Items["FilterStream"] = filter;
    }
  }

  private void EndRequest(Object source, EventArgs e)
  {
    HttpApplication application = (HttpApplication)source;
    HttpContext context = application.Context;
    string filePath = context.Request.FilePath;
    string fileExtension =
        VirtualPathUtility.GetExtension(filePath);

    // とりあえず .aspx のみ対象(.js, .css 等は対象外)
    if (fileExtension.Equals(".aspx"))
    {
      // BegineRequest で HttpContext.Items に保持した
      // OutputFilterStream オブジェクトへの参照を取得
      var filter =
        (OutputFilterStream)context.Items["FilterStream"];

      // 応答コンテンツの文字列を取得
      string responseContent = filter.ReadStream();
    }
  }

  // IHttpModule に定義されているので空でも以下が必要
  public void Dispose() { }
}

// ラッピングフィルタークラス
// stackoverflow の記事のコードをそのままコピー
public class OutputFilterStream : Stream
{
  private readonly Stream InnerStream;
  private readonly MemoryStream CopyStream;

  public OutputFilterStream(Stream inner)
  {
    this.InnerStream = inner;
    this.CopyStream = new MemoryStream();
  }

  public string ReadStream()
  {
    lock (this.InnerStream)
    {
      if (this.CopyStream.Length <= 0L ||
          !this.CopyStream.CanRead ||
          !this.CopyStream.CanSeek)
      {
        return String.Empty;
      }

      long pos = this.CopyStream.Position;
      this.CopyStream.Position = 0L;
      try
      {
        return new StreamReader(this.CopyStream).ReadToEnd();
      }
      finally
      {
        try
        {
          this.CopyStream.Position = pos;
        }
        catch { }
      }
    }
  }

  public override bool CanRead
  {
    get { return this.InnerStream.CanRead; }
  }

  public override bool CanSeek
  {
    get { return this.InnerStream.CanSeek; }
  }

  public override bool CanWrite
  {
    get { return this.InnerStream.CanWrite; }
  }

  public override void Flush()
  {
    this.InnerStream.Flush();
  }

  public override long Length
  {
    get { return this.InnerStream.Length; }
  }

  public override long Position
  {
    get { return this.InnerStream.Position; }
    set
    {
      this.CopyStream.Position =
      this.InnerStream.Position = value;
    }
  }

  public override int Read(byte[] buffer,
                          int offset, int count)
  {
    return this.InnerStream.Read(buffer, offset, count);
  }

  public override long Seek(long offset, SeekOrigin origin)
  {
    this.CopyStream.Seek(offset, origin);
    return this.InnerStream.Seek(offset, origin);
  }

  public override void SetLength(long value)
  {
    this.CopyStream.SetLength(value);
    this.InnerStream.SetLength(value);
  }

  public override void Write(byte[] buffer,
                             int offset, int count)
  {
    this.CopyStream.Write(buffer, offset, count);
    this.InnerStream.Write(buffer, offset, count);
  }
}

上記の HTTP モジュールが動くようにするには web.config での設定が必要ですので注意してください。以下に設定例を書いておきます。

<system.webServer>
  <modules>
    <add name="ResponseContentLogHttpModule" 
         type="ResponseContentLogHttpModule"/>
  </modules>
</system.webServer>

Tags: , ,

ASP.NET

Ajax Control Toolkit スクリプト バンドル

by WebSurfer 2018年6月10日 22:31

Ajax Control Toolkit は v7.x 以前では、使用する複数のスクリプトファイルをバンドルするために ToolkitScriptManager を使っていました。(ScriptManager ではなくて)

v15.1 以降では、DevExpress の記事 How to use Bundling and CDN にありますように、ScriptManager を使ってのバンドリングが可能となっています。(ちなみに、v15.1 以降 ToolkitScriptManager は提供されていません)

以下の画像の赤枠で囲った部分を見てください。これが Ajax Control Toolkit 用の複数のスクリプトファイルがバンドルされた結果です。

バンドルされたスクリプト

ASP.NET 4.5 以降で、Visual Studio Commnunity 2015 の[Web フォーム]のテンプレートを利用して作った ASP.NET Web Forms プロジェクトに、Ajax Control Toolkit を追加し、必要なスクリプトファイルを ScriptManager で統合できるようにしてみましたので、その方法や結果から分かったことを備忘録として書いておきます。

先の記事「ASP.NET 4.5 ScriptManage」に書きましたように、Microsoft Ajax、jQuery 等のスクリプトファイルが ScriptManager で統合できるようになり、[Web フォーム]のテンプレートを利用して作ったプロジェクトでは、それらが設定済みとなっています。

なので、そのプロジェクトに Ajax Control Toolkit 用のバンドル機能を追加すると、重複などの問題で修正が必要かも知れないと思っていましたが、自分が試した限りはそういうことはなかったです。

まず、NuGet で AjaxControlToolkit のパッケージをインストールします。それだけでも基本的なコントロールは動くようになりますが、ScriptManager を利用したバンドリングを可能にするためには、StaticResources パッケージも追加でインストールする必要があります。

NuGet パッケージの追加

バージョンは Ajax Control Toolkit 本体と合わせた方がよさそうです。ここでは、先にインストールした既存の Ajax Control Toolkit 本体が v17.1.1 なので、StaticResources のバージョンもそれに合わせて v17.1.1 にしました。

なお、Visual Studio で[空]のテンプレートを選択してプロジェクトを作った場合は StaticResources パッケージだけでは不足です。追加で Antlr, WebGrease などの NuGet パッケージもインストールする必要がありますが、StaticResources をインストールする際それらも自動的に追加されるはずです。

StaticResources のインストールが完了すると、以下の画像の赤枠で示したフォルダが生成され、その中に CSS ファイル、イメージ、スクリプトファイルが格納されます。

追加された CSS とスクリプト

Content/AjaxControlToolkit フォルダ下の Images フォルダには .gif, .jpg, .png などの画像ファイルが、Styles フォルダには minify してない .css ファイルと minify した .min.css ファイルの両方が格納されます。

Scripts/AjaxControlToolkit フォルダ下の Debug フォルダには minify してない .debug.js ファイルが、Release フォルダには minify した .js ファイルが格納されます。

さらに、web.config にはデフォルトで以下の設定が追加されます。

<ajaxControlToolkit 
    useStaticResources="true" 
    renderStyleLinks="false" />

DevExpress の記事 How to use Bundling and CDN の説明によるとバンドル機能をコントロールするもののようです。

あとは、DevExpress の記事 "Manual changes required" のセクションを参考に、ScriptManager に ScriptReference を追加してやればスクリプトは一番上の画像に示したようにバンドルされます。

具体的には以下の画像の赤枠で示したコードを追加してやります。

ScriptReference の追加

赤枠部分以外の ScriptManager のコードは、Visual Studio の[Web フォーム]のテンプレートを利用して作った ASP.NET Web Forms アプリのマスタページ Site.master に自動生成されたものです。

.css ファイルについても、DevExpress の記事に従って Styles.Render をページの適当な場所に追加してやれば、.css ファイルを参照する link 要素がレンダリングされます。

ただし、.css ファイルは web.config の compilation 要素で debug="false" とするか、Global.asax の Application_Start メソッドで BundleTable.EnableOptimizations を true に設定しないとバンドルされませんので注意してください。(スクリプトファイルと異なります。理由不明)

BundleTable.EnableOptimizations を true に設定する意味については Microsoft の文書 Bundling and Minification を見てください。抜粋すると "The following code (true に設定すること) enables bundling and minification and overrides any setting in the Web.config file." ということだそうです。

web.config の compilation 要素の debug 属性の true / false でどう異なるかですが、自分が検証した限りでは、

  • .css ファイル: true で minify &バンドルあり、false で minify なしでバンドルもなし。
  • スクリプトファイル: true / false の設定にかかわらず minify &バンドルあり。

・・・となりました。

スクリプトファイルの結果がちょっと解せないです。そのままでは、debug="true" にしてもスクリプトのデバッグができません。

.debug.js 版をダウンロードさせてデバッグ可能なようにするには、上の ScriptManager の設定のところで ScriptReference をコメントアウトする他なさそうです。

あと、minify&バンドルされたスクリプトファイルのサイズですが、何と 1,248,841 バイトもありました。Ajax Control Toolkit は使わないのにスクリプトをダウンロードするように設定するというのは避けた方がよさそうです。

ちなみに、.CSS ファイルの方は minify&バンドル後で 56,240 バイトでした。

最後にもう一つ、HtmlEditorEntender には AjaxControlToolkit.HtmlEditor.Sanitizer, HtmlAgilityPack が必要です。

それなしでは先の記事「Ajax Control Toolkit デモ」で述べたような意味不明のエラーが出てハマるかもしれませんのでご注意ください。

Tags: , ,

AJAX

Dictionary の JSON シリアライズ

by WebSurfer 2018年6月2日 18:56

Dictionary<TKey, TValue> クラスのオブジェクトを、ASP.NET MVC の Controller クラスの Json メソッドなどで使われている JavaScriptSerializer クラスを用いて JSON 文字列にシリアライズするとどうなるかという話を書きます。

Dictionary の JSON シリアライズ

例えば、以下のような Controller のアクションメソッドで Dictionary<string, Car> オブジェクトを作って Json メソッドで JSON 文字列にシリアライズしてみます。(注: TKey は JSON の「名前:値」ペアの「名前」になるので string 型にする必要があるようです。実際 int 型ではエラーになりました)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace Mvc5App.Controllers
{
  public class Car
  {
    public string Make { get; set; }
    public string Model { get; set; }
    public int Year { get; set; }
    public float Price { get; set; }
  }

  public class HomeController : Controller
  {        
    public ActionResult JsonDictionary()
    {
      Dictionary<string, Car> dic = 
                          new Dictionary<string, Car>()
      {
        { "Car1", new Car() { Make="Audi",
            Model="A4", Year=1995, Price=2995f } },
        { "Car2", new Car() { Make="Ford",
            Model="Focus", Year=2002, Price=3250f } },
        { "Car3", new Car()  {Make="BMW",
            Model ="503i",Year=1997,Price=1995f } }
      };            

      return Json(dic);
    }
  }
}

上のアクションメソッドをブラウザから POST 要求すると、以下の JSON 文字列が応答として返ってきます。(注: POST 要求するのは、Json メソッドはデフォルトでは GET 要求を拒否するからです)

{
"Car1":{"Make":"Audi","Model":"A4","Year":1995,"Price":2995},
"Car2":{"Make":"Ford","Model":"Focus","Year":2002,"Price":3250},
"Car3":{"Make":"BMW","Model":"503i","Year":1997,"Price":1995}
}

つまり、JSON の「名前:値」ペアとして、Dictionary<TKey, TValue> の TKey が「名前」に、 TValue(Car の JavaScript オブジェクトの JSON 文字列) が「値」に設定され、そのペアが Dictionary の要素の数(上の例では 3 つ)並べられた JSON 文字列になります。

知ってました? 実は自分は知らなかったです。(汗) Dictionary<TKey, TValue> なんて JSON にシリアライズできないと思ってました。(恥)

デシリアライズも、もちろんできるようです。例えば、jQuery alax を利用して上記アクションメソッドを呼び出すと、自動的に JSON 文字列が JavaScript オブジェクトにデシリアライズされ、コールバックの引数として渡されます。

以下の例を見てください、function(data) ... の引数 data にデシリアライズされた JavaScript オブジェクトが渡されます。それを jsonresult という id を持つ div 要素の中に書き込んだのが上の画像です。

function jsonDictionary() {
    $.ajax({
        type: 'POST',
        url: '/home/jsonDictionary'
    })
    .done(function (data) {
        $("#jsonresult").empty();
        $.each(data, function (index, car) {
            $('#jsonresult').append(
                '<p><strong>' + index + '</strong></p>' +
                '<p>' + car.Make + ' ' + car.Model +
                ', Year: ' + car.Year +
                ', Price: $' + car.Price + '</p>');
        });
    })
    .fail(function (jqXHR, textStatus, errorThrown) {
        $("#jsonresult").text('textStatus: ' + textStatus +
          ', errorThrown: ' + errorThrown);
    })
}

上のコードの例では、jQuery の jQuery.each() メソッドを使っていますが、JavaScript の for ... in ループを使って同じ処置を行う例も書いておきます。

for (var key in data) {
    $('#jsonresult').append(
        '<p><strong>' + key + '</strong></p>' +
        '<p>' + data[key].Make + ' ' + data[key].Model +
        ', Year: ' + data[key].Year +
        ', Price: $' + data[key].Price + '</p>');
}

元の .NET Framework の Dictionary<string, Car> オブジェクトにも JavaScriptSerializer や Json.NET (Newtonsoft.Json) を使ってデシリアライズできます。

string json = "上の JSON 文字列";

// JavaScriptSerializer
JavaScriptSerializer serializer = new JavaScriptSerializer();
Dictionary<string, Car> dic =
    serializer.Deserialize<Dictionary<string, Car>>(json);

// Json.NET (Newtonsoft.Json)
Dictionary<string, Car> dic =
  JsonConvert.DeserializeObject<Dictionary<string, Car>>(json);

ただし、DataContractJsonSerializer では、自分が試した限りですが、ダメでした。何かやり方はあるのかもしれませんが。

Tags: ,

JavaScript

About this blog

2010年5月にこのブログを立ち上げました。その後 ブログ2 を追加し、ここは ASP.NET 関係のトピックス、ブログ2はそれ以外のトピックスに分けました。

Calendar

<<  2018年7月  >>
24252627282930
1234567
891011121314
15161718192021
22232425262728
2930311234

View posts in large calendar