しばやん雑記

Azure とメイドさんが大好きなフリーランスのプログラマーのブログ

ASP.NET の Response.Cookies を弄ると空のクッキーが生成されて困った話

ASP.NET で Response.Cookies にとある名前のクッキーが既に入っているかをチェックするコードを書いたところ、空っぽのセッションクッキーが作られることに気が付きました。

例を挙げると以下のようなコードを書いた時です。

public partial class Default : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        if (Response.Cookies["hoge"] != null)
        {
            // 何かする
        }
    }
}

やりたいことは既にクッキーが発行されているかのチェックなのですが、どうもインデクサで触った瞬間にクッキーが作られてしまうようです。

実際に動かすと、空のクッキーが発行されているのが確認できます。

f:id:shiba-yan:20150114124727p:plain

RCW 経由で COM のインデクサを使った時にメモリリークしたのを思い出しました。

Request.Cookies を使う時と同じような感覚で使うと、ちょっとめんどくさいことになるようです。なので AllKeys をまずチェックしてから値を取り出すような拡張メソッドを書きました。

public static class HttpCookieExtension
{
    public static bool Contains(this HttpCookieCollection cookies, string name)
    {
        return cookies.AllKeys.Any(p => p == name);
    }

    public static HttpCookie Peek(this HttpCookieCollection cookies, string name)
    {
        return cookies.AllKeys.Any(p => p == name) ? cookies[name] : null;
    }
}

色々とメソッドが足りていないので、やっぱり HttpCookieCollection って古いコレクションなんだと実感出来ますね。きっと中身は NameValueCollection とかになってるんでしょう。

先ほどの例のようにクッキーが既に返されているかチェックする処理は、以下のように書けます。

public partial class Default : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        if (Response.Cookies.Contains("hoge"))
        {
            // 何かする
        }
    }
}

これで新しくクッキーが作成されることなく弄ることが出来ました。

ASP.NET MVC で任意のタイミングでモデルの検証を行う方法

MVC 5 でウィザード的に 1 ページごとに項目を入れていくページを作っていると、実際に DB に格納する前にセッションに入れておいたモデルの状態が正しいか調べたくなりました。

てっきり「あー、UpdateModel / TryUpdateMode の中身参考にしないといけないのか、めんどくさいなー」と思っていたところ、かなり前*1から Controller クラスに ValidateModel / TryValidateModel メソッドが用意されていることに気が付きました。

Controller.TryValidateModel メソッド (System.Web.Mvc)
Controller.ValidateModel メソッド (System.Web.Mvc)

MVC 5 の機能は一通りは触ってるだろうと思っていたら、こんな初歩的な部分を見落としていたことに恥ずかしい限り…。戒めとしてこの記事を書きます。

これらのメソッドは基本的にモデルバインダーが内部で行っている検証とほぼ同じ処理になるので、データアノテーションや IValidatableObject を使った検証まで行ってくれるようです。

public class SampleModel
{
    [Required]
    public string Title { get; set; }

    [Range(0, 1000)]
    public int Price { get; set; }
}

動作を確認するために、上のような簡単なモデルクラスを用意しました。

public ActionResult Confirm()
{
    // わざと検証エラーになるデータを入れる
    var model = new SampleModel
    {
        Price = -1
    };

    // データに不整合が発生していないかチェック
    TryValidateModel(model);

    return View(model);
}

簡単なビューを用意して実行してみたところ、モデルバインダーを使っていないにもかかわらずエラーが表示されることが確認できます。

f:id:shiba-yan:20141121004308p:plain

ちなみに以下のようにネストした場合にはルートのプロパティしか検証してくれないので、実際に使う場合にはちょっと工夫とか注意が必要になるかと思います。

public class SampleModel
{
    [Required]
    public string Title { get; set; }

    [Range(0, 1000)]
    public int Price { get; set; }

    public SampleChildModel ChildModel { get; set; }
}

public class SampleChildModel
{
    [Required]
    public string Name { get; set; }
}

以下のようなアクションに書き換えて実行をしてみると、ChildModel.Name プロパティは必須にもかかわらずエラーとなりません。これはいまいちな挙動です。

public ActionResult Confirm()
{
    // わざと検証エラーになるデータを入れる
    var model = new SampleModel
    {
        Price = -1,
        ChildModel = new SampleChildModel()
    };

    // データに不整合が発生していないかチェック
    TryValidateModel(model);
    TryValidateModel(model.ChildModel);

    return View(model);
}

なので、とりあえずはネストしたモデルに対しても TryValidateModel メソッドを実行することで、意図したとおりの挙動にはなります。拡張メソッドを用意してもいいかもしれません。

f:id:shiba-yan:20141121004941p:plain

DB 登録前には最終的なモデルのチェックをしたかったのに、このメソッドの存在を知らなかった為に各方面に迷惑をかけた気がします。*2

*1:少なくとも MVC 2 の頃にはあったらしい…

*2:主に蠣殻町の某社さん

ASP.NET MVC の RequireHttps 属性は GET 以外の場合には例外を投げるので注意

タイトルの通りですが、ASP.NET MVC を使って SSL が必須のページを実装するのには RequireHttps 属性が便利ですね。しかし、この属性は GET 以外のリクエストが来ると無条件で例外を投げるようになってます。

例えば HEAD リクエストを投げてみると 500 が返ってきます。

f:id:shiba-yan:20141117123133p:plain

実際にコードを見てみると GET 決め打ちのコードとなってます。

// only redirect for GET requests, otherwise the browser might not propagate the verb and request
// body correctly.

if (!String.Equals(filterContext.HttpContext.Request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase))
{
    throw new InvalidOperationException(MvcResources.RequireHttpsAttribute_MustUseSsl);
}
ASP.NET MVC / Web API / Web Pages - Source Code

正直なところ、この挙動はいまいちな感じがします。少なくとも例外を投げるのは勘弁してもらいたい。

ヘッダ情報を要求してるのでリダイレクト先を教えるのは問題無いのではないかと思うんですが、新しくこの修正のためだけに属性を作るのも微妙なので、URL Rewrite を使って https へリダイレクトさせます。

<rule name="RequireHttps" enabled="true">
  <match url="(.*)" ignoreCase="false" />
  <conditions>
    <add input="{HTTPS}" pattern="off" />
  </conditions>
  <action type="Redirect" url="https://{HTTP_HOST}/{R:1}" appendQueryString="true" redirectType="Permanent" />
</rule>

サーバー変数 HTTPS に on/off が入ってくるので、それを見てリダイレクトするという簡単なルールです。

f:id:shiba-yan:20141117123457p:plain

今度は HEAD リクエストを投げた場合でもリダイレクトになっていることが分かります。こういった処理は URL Rewrite の段階で処理した方がパフォーマンス的にも有利かもしれませんね。

Azure Web サイトに追加されている ASP.NET の設定について

Azure Web サイトの App Settings にはデフォルトで以下のような設定が追加されています。

f:id:shiba-yan:20141101193712p:plain

aspnet:PortableCompilationOutput と aspnet:DisableFcnDaclRead という設定は名前から ASP.NET に関係する設定ということは分かりますが、これまでに見たことが無かったので調べてみました。

aspnet:PortableCompilationOutput

検索してみると以下の KB がヒットしました。

Hotfix rollup 2803755 is available for the .NET Framework 4.5 in Windows 8 and Windows Server 2012

.NET Framework 4.5 の ASP.NET ビューのコンパイラには、最新版か判断するためにコンパイルを行ったマシンの情報を使う挙動が原因で、コンパイル結果を複数のマシン間で共有できないという問題があるようです。

この設定を追加することで、ポータブルな形でコンパイルが行われるみたいです。オートスケールやフェールオーバーで仮想マシン間をころころと移動する Azure Web サイトでは有効にしておくべき設定でした。*1

aspnet:DisableFcnDaclRead

さっきの設定は名前から割と想像できたんですが、こっちの設定は分かりにくいです。検索してみたところ、特に Hotfix などで追加されたものというわけではなさそうです。

debugging.io blog | Undocumented asp.net flag to improve performance of asp.net website when content is hosted on SMB share

Azure Web サイトはアーキテクチャ的にユーザーデータは SMB でマウントされた Blob に保存されています。ASP.NET アプリケーションは SMB で共有されている仮想ディレクトリ上で動かすと、パフォーマンスの低下や以下のようなエラーが発生することがあるようです。

"The network BIOS command limit has been reached" error message in Windows Server 2003, in Windows XP, and in Windows 2000 Server

原因は ASP.NET はファイルの変更監視時に DACL を読み込むみたいですが、この処理が SMB 上だと問題になるみたいです。なので DACL の読み込みをスキップする設定がこれです。

これも Web サイトのアーキテクチャ的に必須な設定ですね。SMB で wwwroot 以下をマウントしている場合には設定を追加しておきましょう。

*1:というか ASP.NET が動く PaaS なら必要な気がしないこともない

Azure と ASP.NET に関係する SSL 3.0 の脆弱性への注意点

SSL 3.0 の脆弱性に関して何回か取り上げてきましたので、最後に Azure と ASP.NET に関係がある部分での注意点についてまとめておきます。

Azure CDN

f:id:shiba-yan:20141017002018p:plain

Azure の CDN は SSL 3.0 が既に無効化されていました。これにより CSS や画像などのリソースが読み込めないという現象が発生する可能性が出てきます。

これは Azure 側が無効化したというよりも、EdgeCast が SSL 3.0 を無効化したという話でしょうけど。

Azure Storage

f:id:shiba-yan:20141017002402p:plain

CDN は SSL 3.0 が無効化されていましたが、Blob などのストレージに関しては有効なままでした。Blob に関して SSL 3.0 をオフにしたい場合には CDN を経由させるのが良さそうです。

Microsoft Ajax CDN

f:id:shiba-yan:20141017001724p:plain

Microsoft Ajax CDN も SSL 3.0 が無効になっていました。と言うか Azure CDN の時と同じ IP アドレスなので、Ajax CDN は EdgeCast を使って運用されていることがわかります。

Azure CDN や Microsoft Ajax CDN に限らず、jQuery や Bootstrap などの CDN 系は Google Hosted Libraries ぐらいしか 3.0 が有効になってない感じがしました。とても対応が早いです。

Azure Web サイト

今のところ、サーバー側で SSL 3.0 を強制的にオフにするという考えはないように思えます。

No.1 に教えてもらった会話ですが、ハンセルマン的にはブラウザでオフにすればいいよと言う感じ。

と言うか、マルチテナントである Azure Web サイトでは、簡単にサイトごとに SSL 3.0 のオンオフ機能とかを実装出来ない事情があります。Web サイトと言うか Windows の制約という感じですが。*1

f:id:shiba-yan:20141017113243p:plain

しかし David Ebbo 的には ARR と Site Extensions で SSL 3.0 での接続はさせつつも、コンテンツは表示させないという方法を検討しているようです。ちなみにインストールしてもまだ動作しません。

これだとクッキーをデコードされる危険性は残りますが、ユーザーに SSL 3.0 をオフにして TLS を使えと言う意思表示にはなるのかなと。

f:id:shiba-yan:20141017113850p:plain

ちなみに同じ PaaS である Heroku はさっさと SSL 3.0 をオフにしているようです。

追記

これまでは Site Extensions でコンテンツを表示させないという方法しかなかった Azure Web サイトですが、どうやら一斉に SSL 3.0 が無効化されることになったようです。

Disable SSL 3.0 in Azure Websites (updated!)

Azure Websites will disable SSL 3.0 for all sites by default to protect our customers from the vulnerability mentioned before. We are rolling out the changes across our data-centers and monitoring traffic in the process. The changes will be rolling out through the week of Monday October 27th, 2014. Once this is complete, customers will no longer need to take any action to disable SSL 3.0 in Azure Websites and should have protection by default.

How to Disable SSL 3.0 in Azure Websites, Roles, and Virtual Machines | Microsoft Azure Blog

今週中にはメンテナンスが行われて、SSL 3.0 がデフォルトで無効化されます。

古いガラケー向けのサービスを Azure Web サイトで提供している場合には、この変更で繋がらなくなる可能性があるのでちょっとだけ注意。

おまけ - Azure MVP の意見

個人的には SNI SSL の時は暗黙的に SSL 3.0 無効とか出来ればいいなと思いましたが、こういうのは落としどころが結構難しそうです。

*1:レジストリ変更 + OS の再起動が必要になるため

ASP.NET MVC の XSS 脆弱性 CVE-2014-4075 について調べた

昨日は SSL 3.0 の脆弱性以外に ASP.NET MVC の脆弱性に関しても情報が公開された日でした。

2014 年 10 月のセキュリティ情報 (月例) - MS14-056 ~ MS14-063 - 日本のセキュリティチーム - Site Home - TechNet Blogs
MS14-059: Description of the security update for ASP.NET MVC 5.1: October 14, 2014

影響を受ける ASP.NET MVC のバージョンは 2.0-5.1 とかなり広くなっています。既に NuGet でも 3.0-5.1 までは修正版が公開されているので、該当するバージョンを使っている場合には早めにアップデートしておきましょう。ちなみに最新の MVC 5.2 は影響を受けません。

NuGet Gallery | ASP.NET MVC 3 3.0.50813.1
NuGet Gallery | Microsoft ASP.NET MVC 4 4.0.40804
NuGet Gallery | Microsoft ASP.NET MVC 5.0.2
NuGet Gallery | Microsoft ASP.NET MVC 5.1.3

MVC 2.0 に関しては NuGet が存在していない時のバージョンなので、ダウンロードセンターから手動でのアップデートが必要になります。

Download Microsoft ASP.NET MVC セキュリティ更新プログラム MS14-059 (KB2990942) from Official Microsoft Download Center

と言っても、手動でインストールしなくても、殆どの場合は Windows Update で GAC の System.Web.Mvc.dll が自動的に更新されるので問題無いでしょう。

ちなみに NuGet でインストールしていたとしても、サーバーの GAC にアセンブリが入っていればそちらが優先されるので、アプリケーション側のアップデートをすることなく Windows Update だけで完結出来る環境もありそうです。*1

Azure Web サイトの場合

Azure Web サイトには MVC 3.0 のアセンブリだけが GAC にインストールされているので、Windows Update で自動的に反映されるのは 3.0 だけになるかと思います。

基本的に NuGet でインストールした MVC 4.0-5.1 を使っているアプリケーションは、NuGet から最新版をインストールしてからの再デプロイが必須になると考えています。

脆弱性の内容について

調べてみたところ、どうやらこのコミットで脆弱性が修正されたようです。

http://aspnetwebstack.codeplex.com/SourceControl/changeset/2b12791aee4ffc56c7928b623bb45ee425813021

内容としては特定の条件の下で HTML ヘルパーを使っていた時に、HTML エンコードが行われない問題を修正したようです。変更点をまとめると以下のような感じ。

  • ModelMetadata に HtmlEncode プロパティを追加
    • デフォルトは HtmlEncode = true
  • DisplayFormat 属性の HtmlEncode プロパティを見る
  • DisplayFor / EditorFor の ObjectTemplate でテンプレートの深さが 1 以上の時に HtmlEncode プロパティを参照してエンコードの有無を切り替える
  • DisplayText / DisplayTextFor では ModelMetadata の HtmlEncode プロパティを参照してエンコードの有無を切り替える

ぶっちゃけ良く分からない。

とりあえず ObjectTemplate に関しては ModelMetadata の SimpleDisplayText プロパティの扱いに注意。DisplayText / DisplayTextFor に関しては、元々 HTML エンコードしないのが仕様だと思ってたんですが、これからは基本的に明示的に HTML エンコードを無効にしない限りは全てエンコードされるようになりました。

HTML を直接出力したい場合には Html.Raw を明示的に呼び出すか、DisplayFormat 属性の HtmlEncode を明示的に false にしましょうと言う話でした。*2

まとめ

ASP.NET MVC に関して CVE で脆弱性が上がるのは初めてだと思います。

基本的に ASP.NET は XSS に対して、リクエスト時点での検証、デフォルトで全て HTML エンコードされる Razor などで防御を固めていますが、今回は仕様がいまいちなヘルパーが原因だった感じです。

後は @chack411 がきっとブログなどで補足してくれるはずです。

*1:少なくとも自分の環境では 5.1.2 をインストールしていても GAC の 5.1.3 が読み込まれた

*2:DisplayText / DisplayTextFor を使っている人がいるのかは疑問

Windows Server Technical Preview の IIS というか http.sys が HTTP/2 に対応していたらしい

Windows Server Technical Preview には正直あまり興味が無かったんですが、今日に IIS でも HTTP/2 が使えたという話が Twitter で流れてきたので早速 Azure の仮想マシンに入れて試しました。

SPDY も HTTP/2 も SSL 上に作られているので、基本的に https が必須です。つまり IIS で https のバインディングを追加しないと有効にならないので気が付きにくそうです。

f:id:shiba-yan:20141006213345p:plain

こんな感じで https のバインディングを追加してあげれば、勝手に HTTP/2 が使われるようになりました。

ちなみに HTTP/2 の Draft 13 に対応している Chrome だと有効にならなかったので、IIS はラストコールになった Draft 14 を実装しているようです。

HTTP/2がHTTPbisワーキンググループのラストコールに - Publickey

何となくメジャーなブラウザと HTTP/2 で通信できるのか確認してみました。

Internet Explorer

Windows 10 の IE から HTTP/2 に対応したことが IEBlog にて公表されてます。

HTTP/2

Internet Explorer on the Windows 10 Technical Preview includes support for new the HTTP/2 networking protocol which is a standardized effort with broad industry support. HTTP/2 builds on our experience delivering SPDY/3 support in IE11 and enables improved performance on the Web using techniques including multiplexing, header compression and Server Push.

Internet Explorer and the Windows 10 Technical Preview - IEBlog - Site Home - MSDN Blogs

F12 開発ツールでネットワークを確認すると HTTP/2 で通信していることが分かります。

f:id:shiba-yan:20141006213344p:plain

IE11 では SPDY/3 に対応しつつもサーバー側が非対応だったので、次期 Windows Server のリリースに期待が高まります。

Chrome

最初にベータリリースの 38 を使って SPDY/4 を有効にした状態で試しましたが、残念ながら HTTP/2 は有効になってくれませんでした。既に書いたように Draft 14 を IIS が実装していたので、そこで違いが発生していたようです。

なので Chrome の Canary 版を入れてみたところ、問題なく HTTP/2 で通信が行えました。

f:id:shiba-yan:20141006213342p:plain

Firefox

Nightly ビルドをインストールして確認したところ、特に設定も必要なく HTTP/2 で通信が行えました。

f:id:shiba-yan:20141006213343p:plain

http.sys 絡み

IIS の HTTP 周りは http.sys というドライバが処理を受け持っているはずなので、同じ http.sys を使う HttpListener でも HTTP/2 が有効になるのではと思ったので確認してみました。

f:id:shiba-yan:20141006222338p:plain

予想通り https の場合には HTTP/2 が有効になったので、IIS というよりも http.sys が HTTP/2 に対応したというのが正しいです。

f:id:shiba-yan:20141006213341p:plain

当然ながら ASP.NET アプリケーションでも、特別な対応無しに HTTP/2 の恩恵に与れます。

ASP.NET 4.5.2 で追加されたバックグラウンド処理 API を使って待つ必要のない非同期処理を丸投げする

.NET Framework 4.5.2 向けのアプリケーションで Google API クライアント周りがコンパイルエラーになって調べていたときに、そういえば ASP.NET に API が追加されたことを思い出したので調べてみました。

ちなみに .NET Framework 4.5.2 で追加された ASP.NET 向けの機能は以下のようになっています。

New APIs for ASP.NET apps. The new HttpResponse.AddOnSendingHeaders and HttpResponseBase.AddOnSendingHeaders methods let you inspect and modify response headers and status code as the response is being flushed to the client app. Consider using these methods instead of the PreSendRequestHeaders and PreSendRequestContent events; they are more efficient and reliable.


The HostingEnvironment.QueueBackgroundWorkItem method lets you schedule small background work items. ASP.NET tracks these items and prevents IIS from abruptly terminating the worker process until all background work items have completed. This method can't be called outside an ASP.NET managed app domain.


The new HttpResponse.HeadersWritten and HttpResponseBase.HeadersWritten properties return Boolean values that indicate whether the response headers have been written. You can use these properties to make sure that calls to APIs such as HttpResponse.StatusCode (which throw exceptions if the headers have been written) will succeed.

What's New in the .NET Framework 4.5, 4.5.1, and 4.5.2

AddOnSendingHeaders とか HeadersWritten をアプリケーションから直接使う場面はそこまで多くない気がしますが、HostingEnvironment に追加された QueueBackgroundWorkItem は利用価値が高いと思います。

既に .NET Web Development and Tools Blog にて紹介されています。すっかり存在を忘れていました。

QueueBackgroundWorkItem to reliably schedule and run background processes in ASP.NET - .NET Web Development and Tools Blog - Site Home - MSDN Blogs

紹介されている通り、メール送信というような結果を await で待つ必要が無い処理の場合には、QueueBackgroundWorkItem を使うことでレスポンスを素早く返しつつ、時間のかかる処理を完了させることが出来るようになります。

とりあえず簡単なサンプルコードを載せておきます。

public ActionResult Index()
{
    // これは良くない -> http://neue.cc/2013/07/02_412.html
    //ProcessAsync(new CancellationToken());

    HostingEnvironment.QueueBackgroundWorkItem(token => ProcessAsync(token));

    return View();
}

private async Task ProcessAsync(CancellationToken cancellationToken)
{
    Debug.WriteLine("executing");

    // 時間のかかる処理のつもり
    await Task.Delay(10000, cancellationToken);

    Debug.WriteLine("executed");
}

QueueBackgroundWorkItem を使って実行されるタスクには実行コンテキストが渡されないので、ConfigureAwait(false) を呼び出した状態と同じになるようです。これだけだと使うメリットが無さそうですが、QueueBackgroundWorkItem で登録したタスクは CancellationToken が渡されるので、タスク内でキャンセル時の処理を組み込んでおくことが出来ます。

そして ASP.NET の AppDomain がシャットダウンされる時に QueueBackgroundWorkItem で追加されたタスクが残っている場合、デフォルトで 90 秒間はシャットダウンを待つようになっています。

挙動を実際に確認しておきます。まずは QueueBackgroundWorkItem を使わずに Task を投げっぱなしで実行し、IIS Express をシャットダウンした場合のログです。

f:id:shiba-yan:20140831125216p:plain

await 後のログが出力されていないので、IIS Express のシャットダウンによって実行中のスレッドも強制的にシャットダウンされたようですね。

今度は QueueBackgroundWorkItem を使って実行した場合のログです。

f:id:shiba-yan:20140831125346p:plain

CancellationToken を Task.Delay に渡しているので、Task.Delay の処理が途中でキャンセルされて TaskCancelledException が投げられています。

最後は QueueBackgroundWorkItem を使い、Task.Delay に CancellationToken を渡さない場合のログです。

f:id:shiba-yan:20140831125910p:plain

CancellationToken を渡さなかったので Task.Delay はキャンセルされることなく実行されましたが、強制的に AppDomain がシャットダウンされることなく、完了後に IIS Express が終了していることがわかります。

ASP.NET 4.5.2 の環境では普通に ThreadPool を使って実行するよりも、QueueBackgroundWorkItem を使った方が安全かつ確実に処理を行えそうです。

Azure Web サイトの .NET Framework が 4.5.2 にアップデートされていた話 - しばやん雑記

.NET Web Development and Tools Blog の記事では Azure Web サイトはまだ対応していないと書いてありますが、既に 7 月の時点で .NET 4.5.2 へのアップデートが行われたので、管理ポータルの .NET Framework のバージョンを V4.5 に変更すれば使えるようになっています。

ASP.NET MVC 5.2.2 / Web API 2.2.2 / Web Pages 3.2.2 がリリースされました

先月の終わりに ASP.NET MVC / Web API / Web Pages がアップデートされていました。

NuGet Gallery | Microsoft ASP.NET MVC 5.2.2
NuGet Gallery | Microsoft ASP.NET Web API 2.2 5.2.2
NuGet Gallery | Microsoft ASP.NET Web Pages 3.2.2

リリースノートは出ていませんが、MVC 5.2.2 に関しては Web Pages との依存関係のためにバージョンを上げただけのようです。なので新機能の追加やバグ修正は行われていません。

Microsoft.AspNet.MVC 5.2.2

This release doesn’t have any new features or bug fixes in MVC. We made a change in Web Pages for a significant performance improvement and have subsequently updated all other dependent packages we own to depend on this new version of Web Pages.

What’s New in ASP.NET MVC 5.2 | The ASP.NET Site

Web Pages 3.2.2 に関しては ASP.NET Web Pages 3.2.1 のベータ版がリリースされました - しばやん雑記 でも紹介した Razor のレンダリング時に LOH を圧迫する問題の修正が含まれているので、基本的にアップデートを行っておくべきでしょうね。

Web API 5.2.2 も新機能の追加やバグ修正は含まれていないですが、Json.NET への依存関係がバージョン 6.0.4 以上に変更されたので、Json.NET の新機能を使えるようになったみたいです。

Microsoft.AspNet.WebAPI 5.2.2

In this release we have made a dependency change for Json.Net 6.0.4. For more information on what is new in this release of Json.NET, see Json.NET 6.0 Release 4 - JSON Merge, Dependency Injection. This release doesn’t have any other new features or bug fixes in Web API. We have subsequently updated all other dependent packages we own to depend on this new version of Web API.

What's New in ASP.NET Web API 2.2 | The ASP.NET Site

そして今回のリリースと関係ないですが、Web API 向けに OData 5.3 RC がリリースされています。

Announcing the Release Candidates for Web API OData 5.3 - .NET Web Development and Tools Blog - Site Home - MSDN Blogs

次の MVC / Web API は 5.3 に、Web Pages は 3.3 となる予定のようです。

ASP.NET MVC 5 で追加された属性ベースのルーティングを使ってモデルバインダ絡みのエラーを回避する

ASP.NET MVC を利用した開発を行っている人は、下のエラー画面を 1 度は見たことがあるかと思います。

f:id:shiba-yan:20140826173411p:plain

MVC では URL ルーティングで定義されたパラメータやクエリ文字列を、モデルバインダが適切な型へ変換してアクションの引数へバインドする仕組みになっています。この時にアクションの引数の型が int なのに null が渡されたり、アルファベットなどが指定された場合は変換に失敗しエラーとなります。

具体的に以下のアクションを例に見ていきます。

public class ArticleController : Controller
{
    public ActionResult Details(int id)
    {
        return View();
    }
}

この場合 Details アクションは int 型の id という名前の引数を取るので、アクセスする URL としては /article/details/123 という形式である必要があります。

しかし /article/details/ や /article/details/abc といったような URL でアクセスされた場合には int への変換が不可能なので、モデルバインダから例外が投げられ HTTP エラーになるという仕組みです。URL ルーティングの定義を変更し、id というパラメータが数字のみ受け付けるように正規表現で制約をかけることも出来ますが、アクションが多い場合には非常に手間がかかります。

routes.MapRoute(
    "Article",
    "article/details/{id}",
    new { controller = "Article", action = "Index" },
    new { id = @"^[0-9]+$"}
);

このようなルーティング定義をアクションごとに手動で定義していくのはとても大変ですし、間違いも多くなってしまいそうですね。しかしアクション側としては必要な型やフォーマットが決まっているので、不正な URL の場合には 404 として扱ってほしいです。

なので、もっと楽に盤石な URL ルーティングを定義するためにも、MVC 5 から追加された属性ベースのルーティングを使って定義していきたいと思います。既に MSDN ブログでとても詳細な使い方が紹介されているので、まずはこの記事を一通り読んでおいてください。

Attribute Routing in ASP.NET MVC 5 - .NET Web Development and Tools Blog - Site Home - MSDN Blogs

割と今更感のある MVC 5 ネタですが、仕事でこのエラーがちょいちょい発生してとてもイラついたのでブログを書くことにしました。

属性ベースのルーティングは標準の URL ルーティングよりも引数へ簡単に制約をつけることが出来ます。以下の例で使っているように {articleId:int} と書くと int しか受け付けなくなります。

[RoutePrefix("article")]
public class ArticleController : Controller
{
    [Route("{articleId:int}")]
    public ActionResult Details(int articleId)
    {
        return View();
    }

    [Route]
    public ActionResult Add()
    {
        return View();
    }

    [Route("{articleId:int}/edit")]
    public ActionResult Edit(int articleId)
    {
        return View();
    }

    [HttpPost]
    [Route("{articleId:int}/edit")]
    public ActionResult Edit(int articleId, FormCollection collection)
    {
        return View();
    }
}

注意点としては、出来るだけ id といった同じ名前のパラメータを使いまわさないことと、GET と POST で同じアクション名のメソッドを定義している場合には、両方ともに Route 属性を付ける必要がある点です。属性ベースのルーティングでは、内部的に ActionDescriptor を保持しているので、同じ URL テンプレートだからと言って使い回しが効くわけではありません。

このルーティング定義で以下のような URL が有効になります。そして、それ以外の URL でアクセスされた場合には 404 になります。

  • /article/123
  • /article/add
  • /article/123/edit

試しに int が必要なパラメータをアルファベットに変更してアクセスしてみます。

f:id:shiba-yan:20140826224459p:plain

アプリケーションエラーとならずに、意図したとおり 404 として正常に処理が完了しました。

先ほどの例では、全てのアクションに Route 属性を付けていましたが、特別な URL テンプレートの指定が必要ない場合にはコントローラレベルで属性を付けることで省略が可能です。

[Route("{action}"), RoutePrefix("admin")]
public class AdminController : Controller
{
    public ActionResult Index()
    {
        return View();
    }

    public ActionResult Settings()
    {
        return View();
    }
}

属性ベースのルーティングは自動的に URL ルーティングを組み立てているだけなので、Route 属性の URL テンプレートを空にしてしまうとアクション全てへアクセスできなくなるので注意。