2011年5月6日金曜日

@RenderPageとHtml.RenderPartial

いや~、連休ももうすぐ終わってしまうと思うと、ちょっぴりアンニュイな気持ちになってしまいます。アンニュイってララバイくらい意味がよくわからない。

Razor使ってますか?使ってますよね。お腹すいたらRazorだし、テレビ見ながらRazorですよ!

疲れてるのかな...。

~/Views/Shared/_Partial.cshtml

@{
    var message = Model ?? (PageData.Any() ? PageData.First().Value : null);
}
ゴールデン @message

↑こんな部分ビューを用意しました。

~/Views/Home/Index.cshtml

@{Layout = null;}
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8" />
    <title>連休</title>
</head>
<body>
<p>@@RenderPage - 
  @RenderPage("~/Views/Shared/_Partial.cshtml", "ウィーク")</p>
<p>Html.RenderPartial - 
  @{Html.RenderPartial("~/Views/Shared/_Partial.cshtml", "ウィーク");}</p>
<p>Html.Partial - 
  @Html.Partial("~/Views/Shared/_Partial.cshtml", "ウィーク")</p>
</body>
</html>

↑そしてそれを利用するビューも用意しました。

ここで問題です!このページ(Index.cshtml)を表示するとどういう表示になるでしょーか?

答え

partial1

ビックリした?ねーねー、ビックリした?オレ、すげービックリした。

なんでHtml.RenderPartialだと”ゴールデン ウィーク”と表示されないんでしょうね。ソース追っかけたりしてスゴイ悩んだんですよ。そもそもRazorではストリームに直接出力うするHtml.RenderPartial使えないのか!?とか仕様を疑ったり(RenderPartialの実装がMVC3になってTextWriter渡す実装に変わってるから関係ないのにね)。

うーん、煮詰まった。ふと、順番入れ替えたらどうなるのか試してみたんす。

<p>Html.RenderPartial - 
  @{Html.RenderPartial("~/Views/Shared/_Partial.cshtml", "ウィーク");}</p>
<p>Html.Partial - 
  @Html.Partial("~/Views/Shared/_Partial.cshtml", "ウィーク")</p>
<p>@@RenderPage - 
  @RenderPage("~/Views/Shared/_Partial.cshtml", "ウィーク")</p>

@RenderPageを最後に移動。

partial2

おや~?ちゃんと出た。あれ~。この際Html.Partialは無視して試しに親戚のHtml.RenderActionはどうなるのか試してみたっす。

HomeControllerに以下を追加。

[ChildActionOnly]
public ActionResult Week()
{
    return Content("ウィーク");
}

Index.cshtmlを以下のように変更(@RenderPageを最初に持って来てHtml.RenderAction追加)。

<p>@@RenderPage - 
  @RenderPage("~/Views/Shared/_Partial.cshtml", "ウィーク")</p>
<p>Html.RenderPartial - 
  @{Html.RenderPartial("~/Views/Shared/_Partial.cshtml", "ウィーク");}</p>
<p>Html.RenderAction - 
  ゴールデン @{Html.RenderAction("Week");}</p>

partial3

Html.RenderActionは平気みたい。このまま@RenderPageを最後に移動。

<p>Html.RenderPartial - 
  @{Html.RenderPartial("~/Views/Shared/_Partial.cshtml", "ウィーク");}</p>
<p>Html.RenderAction - 
  ゴールデン @{Html.RenderAction("Week");}</p>
<p>@@RenderPage - 
  @RenderPage("~/Views/Shared/_Partial.cshtml", "ウィーク")</p>

 

partial4

ちゃんと出ますね。ということはですよ、同じWebPage中では@RenderPageした後はHtml.RenderPartialが正しく動かないということですよ。これってもしかして...。あ、いや、仕様かもしれないし。どういう事?教えてWebMatrixMan~!

※@RenderPage自体がRazor構文なのでWeb Formsでは関係ない問題です。

2011年5月4日水曜日

ぷっすぷすにしてやんよ、Glimpseで。

Glimpse - A client side look at whats going on in your server

なんと素敵なツールでしょうね!

glimpse1

↑ChromeでGlimpseをオンにしたうえに、デベロッパーツールもオンにした状態。かぶせまくり。

これ何?っていうのは上記Glimpseサイトのビデオか、MIX11でのHanselmanさんのセッションビデオ見れば一発でわかります。と言ってしまうとブログに書く必要が無くなってしまうので、少し説明すると、Trace.axdやelmah.axd、RouteDebuggerを全部ひっくるめて組み合わせてさらにサーバーサイドの情報をひと通り参照できるようにするツールデス。分かりにくいっすね。

とにかく、NuGetでInstall-Package Glimpseと入力してプロジェクトに入れて見ましょう。

その状態でデバッグを開始すると、おや?ナニも変化ないですね。なので、URLに/Glimpse/ConfigといれてGlimpse機能をOnにしまいしょう。Onにした後はURLをもとのページに自分で戻すことを忘れずに。

glimpse2

そうすると画面右下に変な目玉アイコン(Glimpseアイコンね)が表示されるのでそこをクリック!

glimpse3

そうすると出てきます。

glimpse4

すごいねー。ちなみにブラウザに表示されてるのはプラグインとかじゃなくて100%JavaScriptだけで作られたものです。

glimpse5 glimpse6

ソース表示すると</html>の後にデータがレンダリングされて、最後にglimpseClinet.jsがロードされてるのが確認できます。

あとは、見たまんまです。各タブにそれぞれ情報が表示されます。

  • Binding
    まだ開発中~。Binderの情報がでる予定(だと思います)
  • Config
    Web.Configの設定内容をセクション別に表示
    config
  • Environment
    サーバーの実行環境の情報
    Application Assemblies/System Assembliesが見れるのは嬉しいですね
    emvironment
  • Execution
    ActionInvokderの実行内容を表示(だと思う)
    Filterの実行も見れるよ!
    execution
  • Glimpse Warnings
    Glimpseの何か
  • MetaData
    どこぞのMetadata
  • Remote
    リクエスト履歴っぽい
  • Request
    HttpRequestBaseの中身からCookie,Form,QueryStringを表示
    request
  • Routes
    表示内容(アクション)に到達するルーティングの解決順とマッチしたルート
    route
  • Server
    ServerValiablesの内容
  • Session
    Sessionの内容
  • Trace
    System.Diagnostics.Traceに出力した内容
  • Views
    表示内容(View)の解決に至る過程
    view
  • XHRequest
    まだっぽい

全部スクリーンショットをとるのが面倒だったから動かして確認してみてね!

んじゃ、そもそもGlimpseの仕組みはどうなってるの?って気になるよね。NuGetからインストールすると使うのは楽チンだけどソースが確認できないから、改めてソースをダウンロードしましょう。

Glimpse/Glimpse - GitHub

真っ先に気になるのが、PreApplicationStartCode。当たり前のようにスタートアップコードが書かれてる。んで、その中身はMicrosoft.Web.Infrastructure.DynamicModuleHelper.DynamicModuleUtility.RegisterModuleを使ったカスタムIHttpModuleの動的登録。その中で各種プラグイン(タブで表示する情報を取得したりするもの)をMEFで登録。

Moduleでハンドリングするタイミングは以下の4つ。

context.BeginRequest += BeginRequest;
context.EndRequest += EndRequest;
context.PostRequestHandlerExecute += PostRequestHandlerExecute;
context.PreSendRequestHeaders += PreSendRequestHeaders;

ここから結構気になってるExecutionで表示してるのがホントにActionInvokerに介入した結果なのかチェックしてみよう。

ExecutionプラグインのSetupInitでいろいろ仕込んでますね。ControllerFactoryとDependencyResolverをGlimpseのものに置き換えてます。GlimpseControllerFactory.CreateControllerでIControllerExtentions.TrySetActionInvokerを呼んでCastleのInterceptorを利用して、プロキシを生成しそれをActionInvokerにセットしてました。これでプロキシが必ず呼び出されるから、実行をインターセプトして実行情報を取得できるということですね。うほ。

DependencyResolverを利用した場合にも大丈夫なようにGlimpseDependencyResolverが用意されてて、こっちでも同じくProxyを生成するように抜かりなし。

他のプラグインもIGlimpsePlugin実装になってて、SetupInitしてGetDataを呼ぶことで各種データをHttpContext.Itemsに入れることで、最終的にGlimpseRespondersがJsonにシリアライズ。まるっとApplication編集に保存してGlimpseResponders.StandardResponseがResponse.Writeしてる。

Glimpseの仕組み

MEFを使ったプラグイン構造でContext.ItemsからApplication変数にデータを保持(configのsaveRequestCountはこの履歴保持数ということみたい)し、それをレスポンス(リクエスト毎にGuidを発行してコンテキストがきちんと判定できるよう保持)。各種情報はプラグインが自立して取得するけど、実行状態のインターセプトはCastleがProxyを生成して介入。

イロイロなプラグインを作っていけば、データの取得と表示はベースの機能でまかなえる素敵設計であるのと、.NET4、MVC3の機能もめいいっぱい利用したお洒落実装。これは萌える。萌え萌え。ぷっすぷすにされちまったぜ!

2011年5月3日火曜日

Dapper.NET

dapper-dot-net - Simple SQL object mapper for SQL Server - Google Project Hosting

これ。Massiveと同じくらい短いコードのORM。WebMatrix.Data.Databaseみたいなものですね。

オープンソースとしてたくさん存在する軽量ORMのなかでもDapperがスゴイところがパフォーマンス。どのくらい早いのかは上記サイトを確認してみると各種ORMとの速度比較が出てます。

A day in the life of a slow page at Stack Overflow

ASP.NET MVC & SQLServer & LINQ to SQLのスタックで有名な大規模サイトといえばStackoverflow.comですね。なんとそのStackoverflow.comでパフォーマンスに問題が出てきたので解決するために、このDapperを利用したというじゃないですか。

そもそもORMでN+1問題が出やすいので、LINQならjoin使ってたくさんのSQLを発行しないようにするとか、DBとのあいだのやりとりもちゃんと確認する(パフォーマンスに問題があると認識したならの話です。問題になってないなら気にしなくていいですよ)必要がありましょう。その辺はちゃんと実装したとしてもDapperが早いということです。Emitしちゃってるし。

使い方も簡単でIDbConnectionの拡張メソッドとして実装してるので、DBコネクションさえあれば簡単に導入できます。もちろんモデルクラスをPOCOで定義しておくことが最速を維持する秘訣。

AdventureWorksLT2008R2をDBに利用するためのサンプル。

public class Product
{
  public int ProductId { get; set; }
  public string Name { get; set; }
  public string ProductNumber { get; set; }
}

using (var connection = connectionFactory())
{
  var result = connection.Query<int>("select count(*) from Product").Single();
  Console.WriteLine(result + " products.");
}

// 1テーブルを1クラスにマッピング
using (var connection = connectionFactory())
{
  var result =
      connection.Query<Product>(
          "select * from Product where ListPrice between @low and @high",
          new { low = 10.0, high = 100.0 });
  Console.WriteLine("-- simple mapping:" + result.Count());
  foreach (var p in result)
  {
      Console.WriteLine(string.Format("ID:{0},Name:{1}", p.ProductId, p.Name));
  }
}

// ジェネリック指定しないで1テーブルをマッピング
using (var connection = connectionFactory())
{
  var result =
      connection.Query(
          "select * from Product where ListPrice between @low and @high",
          new { low = 10.0, high = 100.0 });
  Console.WriteLine("-- dynamic mapping:" + result.Count());
  foreach (var p in result)
  {
      Console.WriteLine(string.Format("ID:{0},Name:{1},Price:{2}", p.ProductID, p.Name, p.ListPrice));
  }
}

dapper2

ここで、実際にテーブルにはたくさん項目あるけど、モデルには省略しちゃってるんですけど、このままSQLにjoin含めた場合、ちゃんとモデルにはマッピングできませんでした。dynamicだとうまくいくんだけど、その辺の整合性はちゃんととっておかないとダメなわけですね。でもdynamicな戻り値なら早いわけではない。

dapper1

var stopwatch = new Stopwatch();
stopwatch.Start();
for (var i = 0; i < 100; i++)
{
  using (var connection = connectionFactory())
  {
    var result =
        connection.Query(
            @"select p.*, c.ProductCategoryId as CategoryId, c.Name as CategoryName
          from Product as p 
            inner join ProductCategory as c on 
              p.ProductCategoryID = c.ProductCategoryID 
          where p.ListPrice between @low and @high",
            new { low = 10.0, high = 100.0 });
    foreach (var p in result){}
  }
}
stopwatch.Stop();
Console.WriteLine("dapper time:" + stopwatch.ElapsedMilliseconds + " ms");

stopwatch.Reset();
stopwatch.Start();
for (var i = 0; i < 100; i++)
{
  using (var l2s = new AwDataContext(connectionFactory()))
  {
    var result = from p in l2s.Product
                 join c in l2s.ProductCategory on p.ProductCategoryID equals c.ProductCategoryID
                 select
                     new
                         {
                             ProductId = p.ProductID,
                             p.Name,
                             Category = c.Name
                         };
    foreach (var p in result) {}
  }
}
stopwatch.Stop();
Console.WriteLine("linq2sql time:" + stopwatch.ElapsedMilliseconds + " ms");

大きなサイトへ導入されてるというのは、興味深いじゃないですか。それだけの負荷で問題が起きないように丁寧な実装がされてるわけで(どっちもだけど)。

今後、より使いやすくなることを期待しつつ見守っていきたいプロジェクトです。