きよくらの備忘録

「三日坊主と呼ばせない!日記」改め。主にソフトウェア開発関連の話題。

テーブル値パラメータについて少し整理とメモ その2・Dapper編

前回 『テーブル値パラメータについて少し整理とメモ - きよくらの備忘録』の続きです。

前回は基本ということで、System.Data.SqlClient、つまり巣のADO.NETでやってみました。が、実際にはORMやMicroORMを利用することが大半でしょう。ということで、私が普段使っているDapperでテーブル値パラメータ (Table-Valued Parameters / TVPs) を使う方法について試してみました。

 

Dapperでテーブル値パラメータを使う

とりあえずDapperでやってみよう、ということで、素直に以下のように書き換えて実行しました。……が、結論から言うと、例外が発生して実行できませんでした。

// using Dapper; が必要
using (var con = new SqlConnection(connectionString))
{
  con.Open();
  var sql = "SELECT Id, Name, Age FROM Users WHERE Id IN (SELECT Value FROM @tvp);" ;
  var param = new { tvp = tvp };
  var rows = con.Query(sql, param); // ★例外発生!
  foreach ( var row in rows)
  {
    // 値を読みだす
  }
}

例外の内容は以下の通り。

"table 型パラメーター 'tvp' には、有効な型名が必要です。"

このエラーの内容だけ見ると、前回のSqlClientを使う場合にSqlParameterのTypeName プロパティに正しく型名を設定するのと、同様の何かが必要であるように読めます。

 

その答えはDapperのIssueにありました。

github.com

つまるところ、パラメータを設定する際に AsTableValuedParameter拡張メソッドを使用して型名を設定すれば良い模様。ということで、以下のように書いてみると、期待通りに動作しました。

using (var con = new SqlConnection(connectionString))
{
  con.Open();
  var sql = "SELECT Id, Name, Age FROM Users WHERE Id IN (SELECT Value FROM @tvp);" ;
  var param = new { tvp = tvp.AsTableValuedParameter( "dbo.IntValueTableType") }; //型名を指定する
  var rows = con.Query(sql, param);
  foreach ( var row in rows)
  {
    // 値を読みだす
  }
}

 

また前回に例として挙げた、ストアドプロシージャ / スカラ値関数 / テーブル値関数 についても、同じ対応で実行できました。

 

ストアドプロシージャだけはAsTableValuedParameterが省略できる

が、実はストアドプロシージャの場合、このAsTableValuedParameterによる型名の設定無しでもいけました*1

using (var con = new SqlConnection(connectionString))
{
  con.Open();

  var sql = "dbo.SummaryValueProc" ;
  var param = new { Values = tvp }; // AsTableValuedParameter 無しでもいける
  var rows = con.Query(sql, param, commandType: CommandType .StoredProcedure);
  foreach ( var row in rows)
  {
    // 値を読みだす
  }
}

この理由については調べれていませんが、受け取るストアド側でパラメータの型情報を持っているのためパラメータ名を使って問題なくマッピングできるから……というあたりでしょうか?

 

【追記】

よくよくMSDNのサンプルを見たら、ストアドの場合はSqlClientでも、TypeName プロパティを設定していませんでした。ということで、たぶん、上記の推測でだいたいあってるんじゃなかろうかと思います。

 

サンプルについて

今回のDapperのサンプルを前回と同じリポジトリに追加しました。

github.com

*1:実は先にストアドで使って普通に実行できていたので、ストアド以外のパターンで例外で出て悩んだという事情があったり

テーブル値パラメータについて少し整理とメモ

SQL Serverに対してアプリケーションから複数行のデータを渡す時に利用できる仕組みとして、テーブル値パラメータ (Table-Valued Parameters / TVPs) があります。 『アプリケーションからクエリを発行する際に配列を渡したい』と思った時に等にも利用できる、便利な仕組みです。

これはSQL Server 2008から実装されている仕組みなのですが、私はここ数年はOracleばかりでこの仕組みをあまり把握していませんでした。 ということで、少し調べてメモしておくことにします。

基本的に、情報の一次ソースはこのあたりです(※本エントリはあくまで私個人のメモですのです。正確な情報が欲しい方は一次ソースをあたってください)。

 

テーブル値パラメータとは

テーブル値パラメータの概要を箇条書きにしてみました。

  • 複数行のデータ』をクライアントアプリケーションから SQL Server へ渡すための機構
  • 「ユーザー定義テーブル型」を定義し、これをパラメータとして利用する仕組み
  • SQL Server 2008から実装されている
  • 戻り値やOUTPUT引数としては利用できない
  • サイズに明示的な上限はなく、環境のメモリに依存する
  • 引数として渡されたT-SQL内(ストアドプロシージャや関数内)では通常のテーブルと同じように読みだすことができる
  • 以下の制限がある
    • テーブル値パラメータをCLR ユーザー定義の関数に渡すことはできない
    • テーブル値パラメータに対するインデックスはUNIQUE 制約または PRIMARY KEY 制約を付与するためのみ付与することでできる
      • 統計情報は保持しない
    • テーブル値パラメーターは Transact-SQL コードの中では読み取り専用
    • ALTER TABLEで定義を変更することはできない

あくまでも『アプリケーションというSQL Serverの外の世界から、まとまったデータをSQL Serverに対して渡す』ための機構、ということだと思います。

 

ADO.NETでのテーブル値パラメータの扱いについて

アプリケーションからSQL Serverに対してデータを渡すためには、何等かミドルウェアなりドライバなりが必要になります。.NETのアプリケーションの場合はADO.NETのSystem.Data.SqlClientが標準的なそれにあたります。

通常、System.Data.SqlClient においてSqlCommandにパラメータを渡すにはSqlParameterを作成してセットするわけですが、テーブル値パラメータでも同様です。この時SqlParameter のValueとして使用できる型は以下の模様。

  • DbDataReader(の派生オブジェクト)
  • DataTable
  • System.Collections.Generic.IEnumerable<SqlDataRecord>

他のデータソースからADO.NETで読み込んだ値そのまま渡す場合は、DbDataReaderでストリーミングとして渡すことが可能。例えばOracleデータベースから読みだした値をOracleDataReaderで受けてそのまま渡したり、Excelから読み込んだ値をOleDbDataReaderで受けて流し込んだりできる模様です。

それ以外の場合、たとえばアプリケーション内のその他のオブジェクトとして保持しているデータを渡したい場合は、DataTableかIEnumerable;SqlDataRecord>の形にしてから渡すことになります。

またSqlParameter のプロパティとして以下を設定しておく必要があるようです。

  • SqlDbType : SqlDbType.Structured
  • TypeName : "<ユーザー定義テーブル型の型名>"*1

このあたりの例とサンプルは、前述の『 テーブル値パラメーター(ADO.NET) 』に詳しく書かれています。

 

System.Data.SqlClientでのサンプルコード

自分でも触ってみないとよくわからないので、サンプルコードで試してみたいと思います。 とりあえず『単純な配列をパラメータとして渡したい』という想定で、以下を試してみました。

  • SELECT
  • ストアドプロシージャ
  • スカラ値関数
  • テーブル値関数

 

サンプルDBのオブジェクトの定義

サンプルDBには適当に以下のようなオブジェクトを定義します*2

-- テーブル
CREATE TABLE [dbo] .[Users] (
    [Id]   INT            IDENTITY ( 1 , 1 ) NOT NULL,
    [Name] NVARCHAR ( 50 ) NOT NULL,
    [Age]  INT           NOT NULL,
    PRIMARY KEY CLUSTERED ( [Id] ASC )
);

-- ユーザー定義テーブル型
CREATE TYPE [dbo] .[IntValueTableType] AS TABLE (
    [Value] INT NULL);

-- ストアドプロシージャ
CREATE PROCEDURE [dbo] .[SummaryValueProc]
    @Values IntValueTableType READONLY
AS
BEGIN
    SELECT SUM ( Value) AS Summary FROM @Values;
    RETURN 0;
END ;

-- テーブル値関数
CREATE FUNCTION [dbo] .[TransformValue]
(
  @Values IntValueTableType READONLY
)
RETURNS TABLE AS RETURN
(
  SELECT ( Value * Value ) AS Value FROM @Values
)

-- スカラ値関数
CREATE FUNCTION [dbo] .[SummaryValue]
(
  @Values IntValueTableType READONLY
)
RETURNS INT
AS
BEGIN
  DECLARE @sum int ;
  SELECT @sum = SUM( value ) from @Values ;
  RETURN @sum;
END

テーブルには適当に数行、レコードを追加しておきます(なんでもいいです)。

 

テーブル値パラメータ設定用のメソッドを定義

初めに、テーブル値パラメータをDataTableとして組み立てるメソッドをこんな感じで定義しておきます。

private System.Data. DataTable createValuesTvp(int [] values)
{
  var dt = new System.Data.DataTable ();
  dt.Columns.Add( "Values" , typeof(int));
  foreach (var value in values)
  {
    var row = dt.NewRow();
    row[0] = value;
    dt.Rows.Add(row);
  }
  dt.EndLoadData();
  return dt;
}

 

SELECT文

まずは単純にSELECT文で使ってみます。SQL文中に『@』をつけて、普通のパラメタライズドクエリとして書きます。SQL文中でのテーブル値パラメータの部分は、テーブルと同じ扱いで書きます。

// 配列からTVPsとして渡すためのDataTableを作っておく
var values = new int[] { 1, 2 };
var tvp = createValuesTvp(values);

using (var con = new SqlConnection(connectionString))
{
  con.Open();

  var sql = "SELECT Id, Name, Age FROM Users WHERE Id IN (SELECT Value FROM @tvp);" ;
  var cmd = new SqlCommand (sql, con);
  var param = cmd.Parameters.AddWithValue("@tvp" , tvp);
  param.SqlDbType = SqlDbType.Structured;
  param.TypeName = "dbo.IntValueTableType";

  using ( var dr = cmd.ExecuteReader())
  {
    while (dr.Read())
    {
      // 読みだす
    }
  }
}

 

ストアドプロシージャ

次はストアドを実行してみました。なんとなくレコードを返すストアドにしていますが、もちろん必須ではありません。 パラメータとして設定する名前はストアド型定義している名前と一致させる必要があります(通常、ストアドを実行するのと同じです)。

var values = new int[] { 1, 2 };
var tvp = createValuesTvp(values);

using (var con = new SqlConnection(connectionString))
{
  con.Open();

  var sql = "dbo.SummaryValueProc" ;
  var cmd = new SqlCommand (sql, con);
  cmd.CommandType = CommandType.StoredProcedure;
  var param = cmd.Parameters.AddWithValue("@Values" , tvp); // ストアド側のパラメータとして定義した名前
  param.SqlDbType = SqlDbType.Structured;
  param.TypeName = "dbo.IntValueTableType"; //【追記】無くても動く

  using ( var dr = cmd.ExecuteReader())
  {
    while (dr.Read())
    {
      // 読みだす
    }
  }
}

【4/5 追記】

Dapper周りのところで調べていて気が付いたのですが、ストアドの場合はTypeNameを設定しなくても問題なく動作するようです(MSDNのサンプルでも記述していなかった)。たぶんですが、ストアドの引数の定義側で型情報を持っているから……とかじゃないかと思います(未確認)。

 

スカラ値関数

スカラ値関数のパラメータとして渡す場合も特に問題なく渡せました。普通にSELECT文で利用するのと同じです。注意点は、パラメータオブジェクトに設定する名前は、あくまでこちらのSELECT文のクエリで定義した名前に合わせる(=関数定義側のパラメータ定義の名前とは関係ない)という点くらいでしょうか。

var values = new int[] { 1, 2 };
var tvp = createValuesTvp(values);

using (var con = new SqlConnection(connectionString))
{
  con.Open();

  var sql = "SELECT dbo.SummaryValue(@tvp) AS Summary;" ;
  var cmd = new SqlCommand (sql, con);
  var param = cmd.Parameters.AddWithValue("@tvp" , tvp);
  param.SqlDbType = System.Data. SqlDbType .Structured;
  param.TypeName = "dbo.IntValueTableType";

  using ( var dr = cmd.ExecuteReader())
  {
    while (dr.Read())
    {
      // 読みだす
    }
  }
}

 

テーブル値関数

これも特に変わったとことはありませんね。普通に読めました。

var values = new int[] { 1, 2 };
var tvp = createValuesTvp(values);

using (var con = new SqlConnection(connectionString))
{
  con.Open();

  var sql = "SELECT Value FROM dbo.TransformValue(@tvp);" ;
  var cmd = new SqlCommand (sql, con);
  var param = cmd.Parameters.AddWithValue("@tvp" , tvp);
  param.SqlDbType = System.Data. SqlDbType .Structured;
  param.TypeName = "dbo.IntValueTableType";

  using ( var dr = cmd.ExecuteReader())
  {
    while (dr.Read())
    {
      // 読みだす
    }
  }
}

 

まとめ?

テーブル値パラメータについて、概要とSystem.Data.SqlClient で利用する基本を確認してみました。

今回のサンプルのソリューションは以下においています。

github.com

 

次回(?)はDapperを使ってアクセスするときにどうなるかを確認してみようと思います。

 

*1:追記:ストアドプロシージャの場合は必須ではない

*2:サンプル以上の意味はないですが、まあ。

Chutzpah+QUnitでTypeScriptのユニットテストをTypeScriptで書いてみたメモ

ChutzpahとQUnitを使ってTypeScriptのユニットテストをやってみているので、そのあたりを備忘録として*1

 

環境とか前提条件とか

環境は Visual Studio 2013 Update 4を基準にしています。多分ですがUpdate 2あたりでも普通に動くとは思います。Expressでは Chutzpah Test Adapterが入らないと思いますが、今ならCommunityもありますので無償環境で試されたい方はCommunityを利用されると良いのではないでしょうか。

 

また、前提として、ASP.NET 4世代のプロジェクトでの話と思って頂ければと思います。

(というのは、現在プレビューが公開されているVS2015+ASP.NET 5世代ではプロジェクトの構造が変わり、特にjsやtsのコンパイル、テスト周り等の扱いは色々変わってくることが予想されます*2。ただし、VS2015であっても既存のASP.NET 4以前の形式のプロジェクトを扱う場合は、有効なんじゃないかと思ってます*3。)

 

で、Chutzpah+QUnitでTypeScriptのユニットテストですが、実はこのあたりの話は、我らが@chack411さんが過去にblogにまとめられています

こちらのエントリではVS2012について書かれておられますが、大枠は変わっていません。ということで『Chutzpahとは何ぞや』あたりについては、是非、そちらを参照していただければと思います。

 

『じゃあこのエントリいらんやん?』と思われる向きもあろうかと思います。が、実際にプロダクトのコードに対してユニットテストを書こうとすると、もうちょっとだけ設定が必要だったり考慮するべきことがあったりしました。今回はこのあたりについて特に書いてみようと思います。

 

必要なもは『Chutzpah』と『qunitの型定義ファイル』

実際にChutzpahをアダプターとしてQUnitを利用する場合、追加で必要になるのはChutzpah自身と、TypeScriptからQunitを使う際に必要になる型定義ファイルです。

 

Chutzpah Test Adapter for the Test Explorer

VS拡張として提供されている「Chutzpah Test Adapter for the Test Explorer」を検索してインストールします。VSのテストランナーから実行するだけであれば、これのみでOKです。 f:id:kiyokura:20141209233752p:plain

なお、「Chutzpah Test Adapter for the Test Explorer」にはQUnitやJasminなどが同梱されています。追加でインストールする必要はありません。

 

QUnitのTypeScriptの型定義ファイル

TypeScriptでユニットテストを書く場合、QUnitの型定義ファイルが必要になります。 NuGetでqunit.TypeScript.DefinitelyTypedを検索するなり、DefinitelyTypedのリポジトリなりからqunit.d.tsを入手しましょう。どこに配置するか、などは、また後ほど。

 

ソリューションの構成について

実際のプロダクトでテストを書く場合、VSの流儀?ではプロダクトとテストのソリューションを分けることが多いのではないかと思います。ということで、今回はプロジェクトを分けてみます。 サンプルとして、以下の構成でソリューションを作ってみました。

 

  • TsUtSample [ソリューション]
    • TsUtSample.Web [プロダクトのプロジェクト(Webアプリケーション)]
    • TsUtSample.Test [テストのプロジェクト(クラスライブラリ)]

 

また、TsUtSample.WebではjQueryとjQueryUIを利用したコードを書くこととします。そのため、jQueryとその型定義ファイルをNuGetから取り込んでいます。そしてアプリケーションのコードである、app.tsを追加しています。 f:id:kiyokura:20141209233753p:plain

まずはスタートはこんな感じ。

最後に、app.tsにテスト対象のコードとして、こんなコードを書いてみました。

/// <reference path="typings/jquery/jquery.d.ts" />
module App {
    export class Hoge {
        addNameSpan = (selector: string, name: string): void=> {
            $(selector).append('<span>' + name  +'</span>');
        }
    }
}

 

テストプロジェクトへのqunit.d.tsのインストール

テストプロジェクトに、先ほど説明したqunit.d.tsをインストールしておきます。とりあえず今回はNuGetから入れています。こんな感じで入ります。 f:id:kiyokura:20141209233754p:plain

 

テストの記述

実際にテストを書いてみます。 テストプロジェクトにappTest.tsという名前でファイルを追加して、QUnitを利用したテストを書いてみます(テストの内容については深く突っ込まないでください:p)。

test("指定した要素に名前を表示するspanを追加する", function () {

    // #qunit-fixtureにテスト用のDOMツリーを作成
    $("#qunit-fixture").append("<div id=\"target\"></div>");

    var app = new App.Hoge();
    app.addNameSpan("#target", "kiyokura");

    // 雑だけどとりあえずこれでテストOKとする
    var actual = $("#target").children("span").text();
    var expected = "kiyokura";

    equal(actual, expected, "innerTextが'kiyokura'なspanが追加されている");
});

ただし、このままではTypeScriptのコンパイラがエラーが出ます。下記のように赤線が出ます。jQueryQUnit、アプリケーションコードのそれぞれの識別子が解決できないためです。 f:id:kiyokura:20141209233755p:plain

 

referenceの追加

ということで、必要なものを参照するように設定します。このあたりは前述のchackさんのblogにもある通りです。プロジェクトをまたがっている場合でも、ドラッグアンドドロップすれば相対パスで記載してくれます。これでTypeScriptコンパイラのエラーは解消しました。 f:id:kiyokura:20141209233756p:plain

 

テストを実行してみる(※ここではエラーになります)

ここでテストを実行してみます。メニューやショートカット(Ctrl+R , A)などからテストを実行してみましょう。表示されていなければ「テスト エクスプローラー」を表示させておくと良いでしょう。

見出しでネタばれてしていますが、ここではテストは失敗します。詳細を見てみると、どうやら実行時に『$(jQuery)』が解決していないような雰囲気です。 f:id:kiyokura:20141209233757p:plain

 

実行時に必要なライブラリを解決する

ということで、実行時に必要なライブラリの参照を解決してやる必要があります。

ここで、TypeScriptならではの注意点があります。JavaScriptであれば、型定義ファイルと同様に/// <reference path="" />で参照を指定すればよいのですが、TypeScriptの場合、/// <reference path="" />に記述できるのはtsファイルのみです(VSのエディタで見ると、赤線でエラーとして表示されるのがわかります。)。

 

『chutzpah_reference』による参照解決

これに対して、chutzpah側で解決策が用意されています。/// <chutzpah_reference path="" />という代替記法を使って、JavaScriptファイルへの参照を記載します*4

完成版?のテストファイルのソースはこんな感じになります。今回はjQueryだけですが、他に必要なライブラリがあればすべて記載する必要があります。chutzpah_reference

/// <reference path="typings/qunit/qunit.d.ts" />
/// <reference path="../../tsutsample/scripts/app.ts" />
/// <reference path="../../tsutsample/scripts/typings/jquery/jquery.d.ts" />
/// <chutzpah_reference path="../../tsutsample/scripts/jquery-2.1.1.min.js" />

test("指定した要素に名前を表示するspanを追加する", function () {

    // #qunit-fixtureにテスト用のDOMツリーを作成
    $("#qunit-fixture").append("<div id=\"target\"></div>");

    var app = new App.Hoge();
    app.addNameSpan("#target", "kiyokura");

    // 雑だけどとりあえずこれでテストOKとする
    var actual = $("#target").children("span").text();
    var expected = "kiyokura";

    equal(actual, expected, "innerTextが'kiyokura'なspanが追加されている");
});

これで、再度実行すると、見事、テストが通りました。 f:id:kiyokura:20141209233758p:plain

 

参照をまとめる

とりあえずこれでテストは通るようになりましたが、テストのスクリプトのファイルごとに毎回これを書くのは面倒になるかもしれません。この場合は、一つのファイルにまとめ、これを参照することで解決できます。ファイル名は何でも良いのですが、Visual StudioJavaScriptの参照ファイルの名前に合わせて、『_reference.ts』とでもしておきます。

/// <reference path="typings/qunit/qunit.d.ts" />
/// <reference path="../../tsutsample/scripts/app.ts" />
/// <reference path="../../tsutsample/scripts/typings/jquery/jquery.d.ts" />
/// <chutzpah_reference path="../../tsutsample/scripts/jquery-2.1.1.min.js" />

そして、テストスクリプトからはこの_reference.tsを参照するようにします。

/// <reference path="_reference.ts" />

test("指定した要素に名前を表示するspanを追加する", function () {

    // #qunit-fixtureにテスト用のDOMツリーを作成
    $("#qunit-fixture").append("<div id=\"target\"></div>");

    var app = new App.Hoge();
    app.addNameSpan("#target", "kiyokura");

    // 雑だけどとりあえずこれでテストOKとする
    var actual = $("#target").children("span").text();
    var expected = "kiyokura";

    equal(actual, expected, "innerTextが'kiyokura'なspanが追加されている");
});

 

 

(Chutzpahには、Chutzpah.jsonという設定ファイルでこのあたりの参照を管理・設定することができるようなのですが、ちょっと試した感じうまいこと行きませんでした。時間ができたらまた色々と試してみようと思います。)

 

 

まとめ

ということで、Chutzpah+QUnitでTypeScriptのユニットテストをやってみました。まだまだ手探りでやってる所も多く、突っ込みどころ等ありましたら是非是非よろしくお願いいたします。

サンプルのソリューションはgithubに上げています。

*1:プロジェクトが始まったばかりなのでまだ試行錯誤フェーズ

*2:Karma と grunt or gulpで…とかになるのかなーとおぼろげに想像中。

*3:保証はできませんが

*4:https://github.com/mmanela/chutzpah/wiki/Chutzpah-File-References#alternate-reference-syntax

ASP.NET MVC と Azure Web Sites で VS Online のカスタムService Hookを作成してサクッとリモートデバッグする

本エントリはASP.NET Advent Calendar 2014 24日目対応エントリです。4日目に続き今年2回目のエントリですが、今回はもう少しASP.NET的な成分が多くなる…予定。

VS Online のカスタムService Hook

以前のエントリで少し触れましてが、Visual Studio Online は外部のサービスと連携可能です。

 

例えばHipChatをはじめJenkinsやAppVeyor、Hubot等々、色々なものとの連携がビルトインされてますが、仕様に沿ったWeb APIの口を用意するとこでカスタムのWeb Hookを行うことも可能になっています。

 

Web Hookを作成するにあたっては、もちろんですが特に言語などは選びません。規定の形式のJSONでPOSTされるデータを受信できるWeb APIさえ用意すればOKです。

言語はなんでもOKなんですが、Visual StudioASP.NET MVC、Azure Web Sitesを組み合わせると、ほぼ手間をかけずに試せます。ということで、一通り軽く紹介してみたいと思います。とりあえず前提条件としては以下を想定します。

 

Webアプリケーションの作成

1.ASP.NET MVC 5のプロジェクトを作成

MVC 5を適当に作成します。ミニマムのサンプルということで、テンプレートはEmptyでMVCだけ参照しておきます。 Web APIでも問題ないともうので、そっちがいい人はそっちで作って、以降は適当に読み替えてください。

f:id:kiyokura:20141224220936p:plain

 

2. コントローラーの追加

テンプレートが展開されたら、コントローラを一つ追加します。今回はGit関連の操作に対するアクションをHookするAPIで利用することを想定して、GitControllerとしました。

f:id:kiyokura:20141224220937p:plain

 

3. 引数をマップするクラスを定義する

アクションメソッドを作成する前に、APIに渡されるデータを格納するためのクラスを定義します。 データはVSOからJSONでPOSTされます。適切な型のクラスを定義してアクションメソッドの引数にしておくけばASP.NET MVCのモデルバインダがよしなにマッピングしてくれます。が、そのためにはもちろんですが適切にクラスを定義する必要があります。

POSTされるデータはAPI仕様の記載から読み取って定義する必要があり、一見すると面倒です。が、VSの機能を使うと瞬殺で完了します。 イベント引数クラスは、とりあえず前の手順で作成したGitController.cs内に作成することにします(まあサンプルですし)。

 

3.1 仕様からサンプルデータをクリップボードにコピーする

ここではCodePushのイベントでPOSTされるデータを対象にしてみます。下のAPI仕様の『Code pushed (git.push)』のSample event欄に記載されているサンプルのJSONを選択し、クリップボードにコピーします。

Service Hooks events

f:id:kiyokura:20141224220938p:plain

 

3.2 クラスとして貼り付ける

コピーしたら、クラスを記述したい場所にカーソルをポイントした後、メニューの[編集]-[形式を選択して貼り付け]から、[JSONをクラスとして貼り付ける]を選択します。

f:id:kiyokura:20141224220939p:plain

するとこんな感じで、JSONからクラスのスケルトンを作ってくれます*1。超便利ですね。

f:id:kiyokura:20141224220940p:plain

 

4. アクションメソッドを作成

あとはアクションメソッドを定義して完了です。 引数の型は先ほど貼り付けて作成したクラスの最上位のオブジェクト(Rootobject)、戻りの型はHttpResponseMessageとします。一応、属性でHttoPostをつけておきました。 (なお、HttpResponseMessage は System.Net.Http にあるので、参照の追加を行っておきます。)

ソースコードとしてはこんな感じですです。下記では受けた後に特に何もしていませんが、実際にはcontentの中身をDBに収めるなり他に通知するなりを行うと思ってください。

public class GitController : Controller
{
  [HttpPost]
  public HttpResponseMessage CodePushed(Rootobject content)
  {
    // 実際はcontentの中身をつかってやりたいことをやる!

    return new HttpResponseMessage(HttpStatusCode.OK);
  }
}

とりあえずGitでPushを受け付けた時の通知を受信できるアクションメソッドの大枠ができました。 ここから実際に何かに保存したりどこかに通知したりといった処理を実装することになります…が本当にVSOからの通知を受信できるか試してみたいですよね。

 

動作確認

ということで、軽く動作確認をしてみたいと思います。VSOからHTTPが届くところならどこでもいいですが、手軽なのでここではAzure Web Sitesを使ってみます*2。 Azure WebSitesにデプロイしてリモートデバッグを設定し、データの受信ができているかどうかを確認してみます。

 

1. Azure Web Sitesにデプロイ

適当にAzure WebSitesにデプロイしましょう。とりあえずVisual Studioの『発行』からWebサイトを新規作成してそのままデプロイしてみます。

[発行]から「Microsoft Azure Websites」を選んで…

f:id:kiyokura:20141224220942p:plain

WebSitesを新規に作成して…

f:id:kiyokura:20141224220943p:plain

デプロイ…の前に、リモートデバッグでステップ実行できるように、[設定]で構成を[Debug]に設定してから発行します。

f:id:kiyokura:20141224220944p:plain

f:id:kiyokura:20141224220945p:plain

これで完了です(ブラウザが立ち上がってエラーが表示されてるかもしれませんが、とりあえず気にせず)。

 

2. リモートデバッグの設定

デプロイができたら、リモートデバッグの設定をしておきます。「サーバーエクスプローラー」を開き、[Azure]-[Webサイト]から、デプロイしたサイトのノードを右クリック、[デバッガーの接続]を選択。

f:id:kiyokura:20141224220946p:plain

 

特にエラー等表示されず、Visual Studioデバッグ実行モードになっていればOKです。とりえあず、CodePushedアクションメソッド内にブレークポイントを張っておきます。

f:id:kiyokura:20141224220947p:plain

 

3. VSOでService Hookを設定してテスト実行

次にService Hookの設定をします。VS OnlineでService Hookを設定したいプロジェクトコレクションを開き、設定の[Service Hooks]タブで[+]マークで追加でダイアログを表示。Serviceから『Web Hooks』を選びます。

f:id:kiyokura:20141224220948p:plain

 

Triggerに『Code Pushed』を選択して…

f:id:kiyokura:20141224220949p:plain

 

Actionの『URL』に、デプロイしたサービスのURLを記載します。たとえばこんな感じ。

f:id:kiyokura:20141224220950p:plain

 

ここで[Test]を実行すると……しばらくするとVisual Studioでステップインされてきます。

f:id:kiyokura:20141224220951p:plain

 

引数をクイックウォッチで確認すると…ちゃんと入ってますね!

f:id:kiyokura:20141224220952p:plain

 

まとめ

簡単にではありますが、VSOのWeb Hooksと連携するオリジナルのWebアプリケーションのひな形を作成して動作確認してみました。 サンプルコードもまとめておいておきます。

 

VSO で Gitを使う場合はこんな感じでPullRequest駆動開発も可能です。

またこんな感じでこのあたりの機能は日々追加されていて、VSO+Gitも割といい感じになってきていると思います。

興味ある方は色々触ってみるとよいのではないでしょうか。

*1:C#的には小文字始まりのプロパティが気になりますが、とりあえず今回は放っておきます。

*2:VSO使ってるなら、おそらくAzureにアカウントもあるはずですし

DapperのQuery<dynamic>()の戻りをASP.NET MVCのJsonResultでいい感じで返す

本エントリはASP.NET Advent Calendar 2014 4日目対応エントリです。

小ネタというか完全誰得ネタですが、割と悩んだのでメモがてら。

 

やりたいこと

まとまりきらないタイトルであれですが、やりたいことはそのまんまです。

例えば以下のような結果のレコードを返すクエリをDBに発行したとします。

name age
Taro 20
Jiro 25

 

このクエリ結果をこんな感じのJSON投げ返してやる、というのが目標です。

[
  { "name": "Taro", "age": 20 },
  { "name": "Jiro", "age": 25 }
]

 

DapperのQuery()の戻りをそのまま返してみる

では、実際にやってみます。 まずはDapperを使ってデータを取得して、そのままIList<dynamic>で返すメソッドを作ります。

private IList<dynamic> GetDynamicRecords()
{
  var conStr = ConfigurationManager.ConnectionStrings["Default"].ConnectionString;
  using (var con = new System.Data.SqlClient.SqlConnection(conStr))
  {
    con.Open();
    var sql = @"select 'Taro' as name, 20 as age union select 'Jiro' as name, 25 as age;";
    var records = con.Query<dynamic>(sql);
    return records.ToList();
  }
}

この戻りをそのままJsonResultで返すASP.NET MVCのコントローラーのアクションメソッドを次のように書きます。

public JsonResult Default()
{
  var json = new JsonResult()
  {
    Data = GetDynamicRecords(),
    JsonRequestBehavior = JsonRequestBehavior.AllowGet
  };
  return json;
}

すると、結果として取れるJSONはこんな感じになります。

[
  [
    { "Key": "name", "Value": "Taro" },
    { "Key": "age",  "Value": 20 }
  ],
  [
    { "Key": "name", "Value": "Jiro" },
    { "Key": "age",  "Value": 25 }
  ]
]

あれ?ちょっと残念な感じ……

 

対処方法:Json.NETを使う

こうなる理由は…まあとりあえずおいておいて*1、どうにか最初の例示した形で欲しいわけです。

いろいろ試行錯誤したところ、ASP.NET MVCのJsonResultクラスが標準で使っているJavaScriptSerializerではなく、ASP.NET Web APIが標準で使っているJSON.NETのシリアライザを使うといい感じ(というか僕が欲しい形)のJSONが得られることがわかりました*2

ということは、JSON.NETを使ったJsonResultの互換クラスを使って、さくっと差し替えればいいわけです。

ということで、以下のように、JsonResultを継承したJsonNetResultなるクラスを作り、ExecuteResultメソッドをoverrideしてJson.NETでシリアライズするようにしてやりましょう。

とりあえず本家ASP.NET MVCのJsonResultクラスの実装を参考に、シリアライズ部分だけJson.NETに差し替えた感じです。

public class JsonNetResult : JsonResult
{
  public override void ExecuteResult(ControllerContext context)
  {
    if (context == null)
    {
      throw new ArgumentNullException("context");
    }
    if (JsonRequestBehavior == JsonRequestBehavior.DenyGet &&
        String.Equals(context.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase))
    {
      throw new InvalidOperationException("JSON GET is not allowed");
    }

    HttpResponseBase response = context.HttpContext.Response;

    if (!String.IsNullOrEmpty(ContentType))
    {
      response.ContentType = ContentType;
    }
    else
    {
      response.ContentType = "application/json";
    }
    if (ContentEncoding != null)
    {
      response.ContentEncoding = ContentEncoding;
    }
    if (Data != null)
    {
      response.Write(JsonConvert.SerializeObject(this.Data));
    }
  }
}

これを利用するアクションメソッドはこんな感じ。JsonResultをJsonNetResultにそのまま差し替えるだけですね。

public JsonResult JsonNet()
{
  var json = new JsonNetResult()
    {
      Data = GetDynamicRecords(),
      JsonRequestBehavior = JsonRequestBehavior.AllowGet
    };
  return json;
}

 

実行するといい感じで結果が取れました。めでたしめでたし。

[
  { "name": "Taro", "age": 20 },
  { "name": "Jiro", "age": 25 }
]

 

まとめ?

はたしてこの情報を欲する人がどれだけ居るのか分りません。……が、他にもExpandoObjectを使って自作したオブジェクトをシリアライズしようとしても同じことになるので、その場合にもこの手法は有効だと思います。

今回のサンプル実装をGithubにあげておきました:

また、せっかくJson.NETに差し替えるのであれば、Json.NETのもつ機能を有効に生かす実装も可能です。そのあたりについては、

Replacing MVC JavascriptSerializer with JSON.NET JsonSerializer

や、北陸のエース@xin9leさんによる

ASP.NET MVCでCamel CaseなJSONを出力する

などが参考になるのではないかと思います。

*1:dynamic型といっても内部的にはDapperのprivateな型のDapperRow型でこの中が…とか色々

*2:MVCじゃなくてWeb API使ってればよかったんじゃあ…』というのも一瞬頭をよぎりました……が、諸事情でWebAPIではなくてMVCを使っているのにも理由があったわけで