2015年5月23日

ASP.NET 出力レスポンスをキャプチャする方法

ブラウザに出力される HTML や API の出力などのレスポンスを、サーバー側で取得したい場合がたまにある。
ASP.NET (not MVC) でこれを行う方法はいくつかある。

Response.Filter を使う方法

Response.Filter にフィルタクラス (Stream 継承) を設定すると、レスポンスを出力前に加工することができる。
加工目的でなくても、Stream.Write に出力内容が入ってくるので、その内容を MemoryStream 等に入れればレスポンスをキャプチャできる。

[キャプチャ用フィルタクラス実装例]

public class CaptureStream : Stream
{
    public CaptureStream(Stream targetStream)
    {
        _targetStream = targetStream;
        Captured = new MemoryStream();
    }
    private readonly Stream _targetStream;
    public MemoryStream Captured { get; private set; }

    public override void Write(byte[] buffer, int offset, int count)
    {
        // 対象のストリームとメモリに書込み
        _targetStream.Write(buffer, offset, count);
        Captured.Write(buffer, offset, count);
    }

    public override void Flush()
    {
        _targetStream.Flush();

        // ここで Captured に対する処理を行う (ログ出力など)
        // OnFlush イベントを用意するとベター
    }

    public override void Close()
    {
        _targetStream.Close();
        Captured.Close();
        base.Close();

        // Captured に対する処理は、ここでも可能
        // MemoryStream は Close してもバッファは残るので ToArray などが可能
    }

    // 残りのオーバーライドは適宜実装
}

//---- 使用例 ----//
// Page クラス内の OnLoad などで
Response.Filter = new CaptureStream(Response.Filter);

デメリットは、キャプチャしたデータにアクセスできる Page イベント(オーバーライド可能メソッドも含む)が無いこと。
つまり、Page ライフサイクル内にキャプチャしたデータを処理するタイミングはなく、上記コードのようにフィルタクラスの Flush ないし Close 時に処理を挟むことしかできない。
そして、Page ライフサイクルから外れるため、セッションに保存できない。

余談だが、Response.Filter は ASP.NET MVC にも存在しているので、同じことができる。

Page.Render を使う方法

Page.Render をオーバーライドすると、出力されるレスポンスデータにアクセスできる。
→ そのままキャプチャ可能。

[Page.Render のキャプチャ実装例]

protected override void Render(HtmlTextWriter writer)
{
    string captured = null;

    using( var sw = new StringWriter() )
    using( var htw = new HtmlTextWriter(sw) ){
        // メモリ内にレスポンス出力
        base.Render(htw);
        captured = sw.ToString();

        // 文字列化したレスポンスをブラウザに出力
        writer.Write(captured);
    }

    // captured に対する処理を行う
}

この方法は、Page ライフサイクル内の処理なので、セッションに保存できる。

デメリットは、オーバーライドなので、フィルタクラスのように機能分離できないこと。

参考URL

2015年5月16日

Assert.AreEqual に NaN が入るとテストが成功するバグ (VS2010~2012)

Visual Studio 2010 (たぶん 2012 も) の MSTest の delta を指定する Assert.AreEqual には下記のようなバグがある。

// delta を指定する場合
Assert.AreEqual(1.0, double.NaN, 1.0); // 成功

// delta を指定しない場合
Assert.AreEqual(1.0, double.NaN); // 失敗

// 失敗するのが正しい動作なので、delta を指定する方はバグっている

バグの原因はソースを見れば明らかで、NaN を考慮していないため。
[Assert.AreEqual のソース (ILSpy)]

public static void AreEqual(double expected, double actual, double delta, string message, params object[] parameters)
{
    if (Math.Abs(expected - actual) > delta)
    {
        string message2 = FrameworkMessages.AreEqualDeltaFailMsg((message == null) ? string.Empty : Assert.ReplaceNulls(message), expected.ToString(CultureInfo.CurrentCulture.NumberFormat), actual.ToString(CultureInfo.CurrentCulture.NumberFormat), delta.ToString(CultureInfo.CurrentCulture.NumberFormat));
        Assert.HandleFail("Assert.AreEqual", message2, parameters);
    }
}

// 比較条件に NaN が含まれると false になるので if の中に入らない

Assert.AreEqual が含まれるアセンブリの配置場所は
%VS のディレクトリ%\Common7\IDE\PublicAssemblies\Microsoft.VisualStudio.QualityTools.UnitTestFramework.dll

Visual Studio 2013 では修正されているが・・・

このバグは VS2013 では修正されている。
[修正済み Assert.AreEqual のソース (ILSpy)]

public static void AreEqual(double expected, double actual, double delta, string message, params object[] parameters)
{
    if (double.IsNaN(expected) || double.IsNaN(actual) || double.IsNaN(delta))
    {
        string message2 = FrameworkMessages.AreEqualDeltaFailMsg((message == null) ? string.Empty : Assert.ReplaceNulls(message), expected.ToString(CultureInfo.CurrentCulture.NumberFormat), actual.ToString(CultureInfo.CurrentCulture.NumberFormat), delta.ToString(CultureInfo.CurrentCulture.NumberFormat));
         Assert.HandleFail("Assert.AreEqual", message2, parameters);
    }
    if (Math.Abs(expected - actual) > delta)
    {
        string message3 = FrameworkMessages.AreEqualDeltaFailMsg((message == null) ? string.Empty : Assert.ReplaceNulls(message), expected.ToString(CultureInfo.CurrentCulture.NumberFormat), actual.ToString(CultureInfo.CurrentCulture.NumberFormat), delta.ToString(CultureInfo.CurrentCulture.NumberFormat));
        Assert.HandleFail("Assert.AreEqual", message3, parameters);
    }
}

// 引数のいずれかが NaN の場合、テスト失敗

が、VS2010 をインストールしている場合、修正されたアセンブリ「Microsoft.VisualStudio.QualityTools.UnitTestFramework.dll」(以降、UnitTestFramework.dll)が使われないケースがある。
[確認できた発生条件]
  • VS2010 と VS2013 インストール済 (インストール順序は関係ない)
  • .NET 4.0 以上のテストプロジェクト

原因

VS2010 は GAC に .NET 2.0 と 4.0 の UnitTestFramework.dll (バグ有り) をインストールするが、VS2013 は .NET 2.0 の UnitTestFramework.dll (修正済み) しかインストールしないため。
かつ、VS2010 の UnitTestFramework.dll (.NET 4.0) と VS2013 の UnitTestFramework.dll (.NET 2.0) のバージョン番号「10.0.0.0」が同じであるため。(公開キーは MS 製品なので同じ)
※他にバージョン番号「10.1.0.0」の UnitTestFramework.dll も存在するが、設定で参照されないようになっている

VS2010 と VS2013 をインストールした環境の GAC は下記の状態になる。
CLR 2.0 %windir%\assembly 修正済み
CLR 4 %windir%\Microsoft.NET\assembly バグ有り

そして、CLR 4 の実行アプリは CLR 4 の GAC を優先するため、バグ有りの方の UnitTestFramework.dll が参照されることになる。(参考:GAC 内のアセンブリ検索方法
公開キーが一緒なのは仕方ないとして、バージョン番号さえ変えとけば、こんな事態は回避できた・・・

解決手段

上記リンクでも説明されてるけど、CLR 4 の GAC の実ファイルを移動または削除すればOK。
%windir%\Microsoft.NET\assembly\GAC_MSIL\Microsoft.VisualStudio.QualityTools.UnitTestFramework\v4.0_10.0.0.0__b03f5f7f11d50a3a\Microsoft.VisualStudio.QualityTools.UnitTestFramework.dll

副次的な効果として、VS2010 のテストプロジェクトからも修正された UnitTestFramework.dll が使用されるようになる。

2015年5月10日

GAC 内のアセンブリ検索方法

.NET Framework アプリケーションの実行時、参照されている署名付きアセンブリ (DLL) は、まずグローバル アセンブリ キャッシュ (GAC) から検索される。
GAC 内に見つからなかった場合、アプリケーションフォルダや設定ファイルで指定した場所が検索される。

で、GAC 内のアセンブリ検索方法は、少し複雑である。

GAC の実フォルダ(アセンブリの配置場所)は、アセンブリの CLR バージョンによって 2 個所に分かれている。
※ 2015/05/01 現在
CLR 1.0 ~ 2.0
(.NET 3.5 以前)
%windir%\assembly
CLR 4
(.NET 4.0 以降)
%windir%\Microsoft.NET\assembly

アセンブリ検索方法も、実行アプリケーション (exe) の CLR バージョンによって異なる。
(A) CLR 1.0 ~ 2.0 ① を検索
(B) CLR 4 ② → ① の順で検索
※実行ファイルのビルド時に .NET 3.5 以前のアセンブリを参照しても ② から検索
→ アセンブリのランタイムバージョンは考慮されない

(B) ※ の補足。
同じ名前・バージョン番号・公開キーのアセンブリ (TEST.DLL) を .NET 3.5 と 4.5 でビルドして、それぞれGAC にインストールする。
→ ① と ② の両方にアセンブリが置かれる。
TEST.DLL (.NET 3.5) を参照する実行アプリを .NET 4.5 でビルドして実行する。
→ ② が先に検索されるため、実際に参照されるアセンブリは TEST.DLL (.NET 4.5)

このような事態を回避するため、GAC にインストールするアセンブリに複数の CLR バージョンがある場合、それぞれ異なるバージョン番号にしておくのが定石。

ちなみに実際に参照されるアセンブリのパスは、下記で確認できる。

// string クラスがあるアセンブリの配置場所
Console.WriteLine(Assembly.GetAssembly(typeof(string)).Location);

// C:\Windows\Microsoft.NET\Framework\v4.0.30319\mscorlib.dll
// ※ mscorlib.dll は GAC から読み込まれない例外

mscorlib.dll が GAC から読み込まれない理由は下記