2011年10月15日土曜日

TraceListener into MongoDB

たまには週間たけはらブログ。
ASP.NETでTraceListener使ってますか?今まで結構仕込んでおいたんだけど、ファイルやイベントログだと扱いにくいなー、なんて思ってませんか。思ってました。融通効かないなー、と。大規模サイトなんかでSQLServerに入れちゃうと、大変なことにナチャウヨ。
そこでMongoDB。みんな大好きMongoDB。ドキュメントの日本語化も着実に進んでるので、英語なんてー、と気にすることもあんまりないでしょう。そーでもないですか?いろいろ可愛いやつですよ!ログデータの保持なんて、もう、得意中の得意です。保持する構造さえちゃんとしておけば、RDBじゃ処理しにくいものもお気楽に扱えます。用途と使い方次第デスけどね。
Home - Docs-Japanese - 10gen Confluence
MongoDBって何よ?っていうのは、いろいろ検索してね。
MongoDB と NoSQL を試す
MongoDB と NoSQL を試す (第 2 部)
MongoDB と NoSQL を試す (第 3 部)
開発環境で超簡単な使い方はコンソールでmongodを起動しておいて、mongoで中身を確認。
ml
だんだんmongodをサービス起動しておきたくなりますが、最初は単なるプロセス起動。もちろん、内容確認をコンソールのmongoで行うのも、ハッカーみたいな雰囲気あっていいかもしれないけど、そんなの面倒なので実際はGUIのツールを使いましょう。
MongoVUE | Gui tools for MongoDB
ml2
他にもイロイロあるので、気に入ったのを選んで試してみましょう。
Admin UIs – MongoDB
このMongoDBにTraceListenerからmessageを保存するようにしちゃいましょー!
開発するならHTTPでのREST操作(MongoDBに最初からあります)よりも、Driverを使った開発のほうが楽チンぽんです。前までは、いろいろ使い勝手の問題もあったりとかしたけど、いまとなっては標準公開されてるもので十分です。
CSharp Language Center – MongoDB
CSharp Driver Tutorial - MongoDB
英語かよ!どんまい。
マニュアル - Docs-Japanese - 10gen Confluence
なんにせよNuGetで取得できるのが便利なところです。
Official MongoDB C# driver - 1.2 : NuGet gallery
TraceListenerって自分で用意したことなかったんだけど、TraceListenerクラス派生でいいってことなので、お手軽ですね。必須なoverrideも2個だけ。
public override void Write(string message)
{
  // ...
}

public override void WriteLine(string message)
{
  // ...
}
やれそうですね。
えいやっ!
using System.Configuration;
using System.Diagnostics;
using MongoDB.Bson;
using MongoDB.Driver;

namespace MongoListener
{
  public class MongoDbTraceListener : TraceListener
  {
    private readonly MongoServer _server;
    private readonly MongoDatabase _database;
    private readonly string _collectionName;

    public MongoDbTraceListener() : this("TraceData") { }
    public MongoDbTraceListener(string initializeData)
    {
      _collectionName = initializeData;
      var serverName = ConfigurationManager.AppSettings["MongoDb:Server"];
      var databaseName = ConfigurationManager.AppSettings["MongoDb:Database"];

      if (string.IsNullOrEmpty(serverName))
          serverName = "mongodb://localhost";

      if (string.IsNullOrEmpty(databaseName))
          databaseName = "TraceListener";

      _server = MongoServer.Create(serverName);
      _database = _server.GetDatabase(databaseName);
    }

    private void Insert(BsonDocument document)
    {
      var collection = _database.GetCollection(_collectionName);
      collection.Insert(document);
    }

    private void InternalWrite(string message)
    {
      var document = new BsonDocument {{"Message", message}};
      Insert(document);
    }

    private void InternalWriteObject(object o)
    {
      var document = new BsonDocument();
      
      var type = o.GetType();
      foreach(var prop in type.GetProperties())
      {
          document.Add(prop.Name, prop.GetValue(o,new object[]{}).ToString());
      }
      Insert(document);
    }

    public override void Write(string message)
    {
      InternalWrite(message);
    }

    public override void WriteLine(string message)
    {
      InternalWrite(message);
    }

    public override void WriteLine(object o)
    {
      var type = o.GetType();
      if (type.Name.StartsWith("<>") && type.Name.Contains("AnonymousType"))
      {
          InternalWriteObject(o);
          return;
      }
      InternalWrite(o.ToString());
    }
  }
}
できたー。
後は、web.configに書いて使えるようにするだけですね!
  <appSettings>
    <add key="webpages:Version" value="1.0.0.0"/>
    <add key="ClientValidationEnabled" value="true"/>
    <add key="UnobtrusiveJavaScriptEnabled" value="true"/>
    
    <add key="MongoDb:Server" value="mongodb://localhost"/>
    <add key="MongoDb:Database" value="TraceListener"/>
  </appSettings>
<system.diagnostics>
  <trace autoflush="false" indentsize="4">
    <listeners>
      <add name="mongoListener" 
           type="MongoListner.MongoDbTraceListener, MongoListner"
           initializeData="MyTrace" />
      <remove name="Default" />
    </listeners>
  </trace>
</system.diagnostics>
  <system.web>
    <trace enabled="true"/>
あとはSystem.Diagnostics.Trace.Write/WriteLineです!すでにたくさん仕込んでいる場合にはコレでOK。
ASP.NET MVC3標準プロジェクトを作成して、MongoDbTraceListenerクラスを作成し、HomeControllerのIndexアクションにTraceを書いてみましょう。
ml4
わーお!素敵!!
MongoDBが違うマシンだったり(本番はそうしましょう)、Database名を変更したいときはappSettingsの値を変えてね。コレクション(テーブル相当)の名前を変えたかったらtrace/listnersのinitializeDataで指定してね。
ちなみに、これだけだとつまんないので、ここから少し拡張してパフォーマンス計測してみましょう。まずはIHttpModuleを実装して、リクエストの処理時間を計測するようにしてみます。
mvc-mini-profiler - A simple but effective mini-profiler for ASP.NET and WCF - Google Project Hosting
↑パフォーマンス計測ならこんな素敵なもの(NuGit.orgで実物見れますよ~)もありますが...。これから新規ならこっちのほうが...。いや、言うまい。
ml3
どりゃ!
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Web;

namespace MongoListner
{
  public class PerformanceTraceModule : IHttpModule
  {
    private string ItemKey = "_mongoDbTraceStart";
    private readonly string _serverName = "";

    public PerformanceTraceModule()
    {
      _serverName = Environment.MachineName;
    }

    public void Init(HttpApplication context)
    {
      context.BeginRequest += new EventHandler(context_BeginRequest);
      context.EndRequest += new EventHandler(context_EndRequest);
    }

    void context_BeginRequest(object sender, EventArgs e)
    {
      var httpContext = (sender as HttpApplication).Context;
      var startTime = httpContext.Items[ItemKey] = DateTime.Now;
    }

    void context_EndRequest(object sender, EventArgs e)
    {
      var httpContext = (sender as HttpApplication).Context;
      var startTime = (DateTime)httpContext.Items[ItemKey];

      Trace.WriteLine(new
      {
        Server = _serverName,
        RequestAt = startTime,
        Method = httpContext.Request.HttpMethod,
        Status = httpContext.Response.StatusCode,
        RawUrl = httpContext.Request.Url.ToString(),
        Milliseconds = (DateTime.Now - startTime).Milliseconds
      });
    }

    public void Dispose()
    {
    }
  }
}
これを利用するためにsystem.webServer/modulesに登録します。
  <system.webServer>
    <validation validateIntegratedModeConfiguration="false"/>
    <modules runAllManagedModulesForAllRequests="true">
      <add name="mongoListener" 
           type="MongoListner.PerformanceTraceModule, MongoListner" 
           preCondition="integratedMode"/>
    </modules>
  </system.webServer>
実行してみます。
ml5
BeginRequestからEndRequestの間を計測するものですが490msって...。遅すぎ!
ml6
と、思ったらF5リロードの2回目は5ms。そーだろそーだろ。ん?よく見たらStatusとMillisecondsがstringになってるー。TraceListenerのInternalWriteObjectでToStringしてたね。失敬。そこは修正しましょう。書きながら作る、作りながら書く!
private void InternalWriteObject(object o)
{
  var document = new BsonDocument();
  
  var type = o.GetType();
  foreach(var prop in type.GetProperties())
  {
    var value = BsonValue.Create(prop.GetValue(o,new object[]{}));
    document.Add(prop.Name, value);
  }
  Insert(document);
}
これでちゃんと型どおり。BsonValueにはいろいろあるのでドキュメント参照してみてください。
このHttpModuleがあればすべてのリクエストの処理時間が計測できますね!アクセスログのTimeTakenとダダカブリ。どんまい。
IIS 6.0 ログ ファイルの Time Taken フィールドは何を表し、何を意味していますか。 : IIS 6.0 についてよく寄せられる質問
ASP.NET MVCならActionFilter属性を使ってControllerでの処理時間とViewの処理時間をそれぞれ別で計測できてなお嬉しいはず。
そいやっ!
using System;
using System.Diagnostics;
using System.Web.Mvc;

namespace MongoListner
{
  public class PerformanceTraceAttribute : ActionFilterAttribute
  {
    private string ItemKey = "_mongoDbFilterTraceStart";
    private readonly string _serverName = "";

    public PerformanceTraceAttribute()
    {
      _serverName = Environment.MachineName;
    }

    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
      filterContext.HttpContext.Items[ItemKey] = DateTime.Now;

      base.OnActionExecuting(filterContext);
    }

    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {
      var startTime = (DateTime)filterContext.HttpContext.Items[ItemKey];

      Trace.WriteLine(new
      {
          Server = _serverName,
          ProcessAt = DateTime.Now,
          RawUrl = filterContext.HttpContext.Request.Url.ToString(),
          Method = filterContext.HttpContext.Request.HttpMethod,
          Controller = filterContext.RouteData.GetRequiredString("controller"),
          Action = filterContext.RouteData.GetRequiredString("action"),
          Milliseconds = (DateTime.Now - startTime).Milliseconds
      });

      base.OnActionExecuted(filterContext);
    }

    public override void OnResultExecuting(ResultExecutingContext filterContext)
    {
      filterContext.HttpContext.Items[ItemKey] = DateTime.Now;

      base.OnResultExecuting(filterContext);
    }

    public override void OnResultExecuted(ResultExecutedContext filterContext)
    {
      var startTime = (DateTime)filterContext.HttpContext.Items[ItemKey];
      var viewName = (string) null;
      if (filterContext.Result is ViewResult)
      {
        viewName = (filterContext.Result as ViewResult).ViewName;
      }
      Trace.WriteLine(new
      {
        Server = _serverName,
        ProcessAt = DateTime.Now,
        RawUrl = filterContext.HttpContext.Request.Url.ToString(),
        Result = viewName ?? filterContext.Result.GetType().Name,
        Controller = filterContext.RouteData.GetRequiredString("controller"),
        Action = filterContext.RouteData.GetRequiredString("action"),
        Milliseconds = (DateTime.Now - startTime).Milliseconds
      });

      base.OnResultExecuted(filterContext);
    }
  }
}
これを有効にするためにGlobal Filterに追加しておきましょう。
あとは実行するだけ!
ml7
ズームして見てね。
Controllerでの実行時間が223ms、ActionResultの実行時間が180ms、HttpModuleでの計測時間が497ms。差が94msありますね。そんなもんでしょう。ちなみにコレが初回実行時の計測で、2回目は↓。
ml8
Controllerでの実行時間が0ms、ActionResultの実行時間が0ms、HttpModuleでの計測時間が10ms。差が10ms。これまた、そんなもんでしょう。
ml9
こんな感じです!楽しいですね!
途中、MongoDBに入れるDocumentのカラムを変更したりしてるけど、MongoDB側へは何も手を加える必要がないんです。ドキュメント単位(テーブルなら行単位)にカラム構成が変更されてもお構いなしです。ちなみにサーバーさえいればDatabaseもCollectionも初回アクセス時に勝手に作られるので準備は不要。この手軽さと、レスポンス性能の高さがMongoDBの魅力です。
今回のプロジェクト一式は↓こちら。ローカルにMongoDBさえ入っていればそのまま動くはず。

※ファイルを小さくするために、packages/mongocsharpdriver.1.2を消してます。

2011年10月11日火曜日

AllowHtmlの深淵

月刊たけはらブログ。と、なってしまいましたね。

Understanding Request Validation in ASP.NET MVC 3

唐突ですが、↑随分前のこのエントリ、ずっと気になってたんです。ValidateInputAttributeってあるじゃないですか、Ver1の時から。これを指定したControllerやActionは不正なリクエスト文字列が含まれるとHttpRequestValidationExceptionをthrowするやつです。

これ、もともとASPXのPageディレクティブのValidateRequestとしても使われてますね。Pageディレクティブですよ。ココ重要。

MVC3になってからAllowHtmlAttributeって言うモデルプロパティに指定できるやつが追加されたじゃないですか。これ、とても不思議に思ってたんです。でも、まぁいっか、思ったように動くし、と、見て見ぬふりしてたんですけど、どーも、気持ち悪くてですね。

どういうことかというと、PageディレクティブだとHttpRequest.ValidateInput()に直接連携するというのはすんなり受け入れられるけど、それってリクエストコンテキスト単位のチェック(Form,QueryString,Cookieが送信されてきたリクエスト自体)で、個々の要素っていうか値単位のチェックなわけじゃない、っていう仕組みじゃないですか。なのでAllowHtmlってソレまでの仕組みとは全然違うものを実装してるということになりましょう。そうなると、どういう仕組で実装されてるのかキニナル。気になる。

で、先のブログのエントリなんですけど、正直かなり難しそうだなー、と思ってずっと敬遠してました。

さっそく原点、ASP.NET MVC1のControllerActionInvoker見てみます。

[SuppressMessage("Microsoft.Performance", "CA1804:RemoveUnusedLocals", MessageId = "rawUrl",
  Justification = "We only care about the property getter's side effects, not the returned value.")]
private static void ValidateRequest(HttpRequestBase request) {
  // DevDiv 214040: Enable Request Validation by default for all controller requests
  // 
  // Note that we grab the Request's RawUrl to force it to be validated. Calling ValidateInput()
  // doesn't actually validate anything. It just sets flags indicating that on the next usage of
  // certain inputs that they should be validated. We special case RawUrl because the URL has already
  // been consumed by routing and thus might contain dangerous data. By forcing the RawUrl to be
  // re-read we're making sure that it gets validated by ASP.NET.

  request.ValidateInput();
  string rawUrl = request.RawUrl;
}

HttpRequestBaseのValidateInputを呼んで、RawUrlにアクセスするだけのシンプルな実装ですね。コメントから苦労が垣間見れます。

ASP.NET MVC3の同じ部分を見てみます。

internal static void ValidateRequest(ControllerContext controllerContext) {
  if (controllerContext.IsChildAction) {
      return;
  }

  // DevDiv 214040: Enable Request Validation by default for all controller requests
  // 
  // Earlier versions of this method dereferenced Request.RawUrl to force validation of
  // that field. This was necessary for Routing before ASP.NET v4, which read the incoming
  // path from RawUrl. Request validation has been moved earlier in the pipeline by default and
  // routing no longer consumes this property, so we don't have to either.

  ValidationUtility.EnableDynamicValidation(HttpContext.Current);
  controllerContext.HttpContext.Request.ValidateInput();
}

微妙に違いますね。Microsoft.Web.Infrastructure.DynamicValidationHelperのValidationUtility.EnableDynamicValidationですね。誰や!?この旅、ここから長いです。長い割に身が無いです。できることはわかってるんだから。

先のブログエントリを、ちらっと見てるという前提で話を進めますが、MVC3だとHttpRequestのForm,QueryStringへのアクセスでもHttpRequestValidationException発生しますね。<httpRuntime requestValidationMode="2.0"/>はweb.configに入れてません。入れてないとASP.NET4ではデフォルトで全てのコレクションに対する検証がOnになるからです。

ASP.NET 4 Breaking Changes - ASP.NET Request Validation

ASP.NET 2/3.0/3.5と同じ挙動にしたいときだけ。<httpRuntime requestValidationMode="2.0"/>を指定しましょう。必要ないと思いますけど。んで、なんでここに手を入れてるのかというと、検証処理自体細かく制御できるように拡張ポイントを追加したからですね。requestValidationModeともう一つrequestValidationTypeというのも指定できるようになってますが、こっちで検証クラス(検証とずっと言ってるけど通常の入力検証じゃなくてリクエスト検証のことなので誤解しないでね!)を指定してカスタム出来るようになってます。

標準テンプレートのまま1つソリューションを作成します。HomeControllerのIndexで以下のようにQueryStringにアクセスするとします。

public ActionResult Index()
{
  var p1 = Request.QueryString["p1"];

  ViewBag.Message = "Welcome to ASP.NET MVC!";

  return View();
}

なんの変哲もないコードですね。これを実行してブラウザのアドレス欄に”?p1=<a>a</a>”なんていうのを足してアクセスしなおすと、例のエラーでます。そりゃそうです。The XSSです。

rv1

rv2 rv3

今度はそこに↓こんなずるいクラスを追加してrequestValidationTypeに指定。

using System.Web;
using System.Web.Util;

namespace ReqValidate.MVC3
{
  public class PassRequestValidator : RequestValidator
  {
    protected override bool IsValidRequestString(
        HttpContext context,
        string value,
        RequestValidationSource requestValidationSource,
        string collectionKey,
        out int validationFailureIndex)
    {
      validationFailureIndex = -1;
      return true;
    }
  }
}

ひどい。

<httpRuntime
  requestValidationType="ReqValidate.MVC3.PassRequestValidator"/>

このまま先のURLにもう一度アクセス。

rv4

同じようにダメそうなQueryStringを指定してるのに、今度はエラーになりませんでした。より厳しい条件を指定したいときなんかはカスタムすればいいですね。同じ仕組でRawUrlのチェックで許可したい文字を増やす事や、拒否したい文字を減らすこともできるでしょう。残念ながら標準でできるようになってるので、そんなことする意味は無いですけどね。

ASP.NET 4 and Visual Studio 2010 Web Development Overview - Expanding the Range of Allowable URLs

ただ、この辺突き詰めていけば、AllowHtmlにたどり着くのかというと、そうでもない、っていうね。なんでしょーね。この話、必要なかったですね。

話を戻すと、requestValidationModeが2.0の時と、4.0(デフォルト)の時での挙動の違いとして、チェックが有効になるタイミングの違いがあるようです。2.0だとBegineRequestではまだ有効になってなくて、4.0では有効になってます。

Global.asaxでブレークを仕掛けるとわかります。なるほど。Request.ValidateInput()でチェックフラグがオンになるので、以降のForm/QueryStringへのアクセスでチェックがかかるという仕組みです。

requestValidationMode=”2.0”の時。

rv5

Microsoft.Web.Infrastructure.DynamicValidationHelper.ValidationUtility.IsValidationEnabled(HttpContext.Current)で確認しましょう。

falseです。

なので、このタイミングで上記スクリーンショットのQueryStringを参照すると、XSSな値が入っていても、このタイミングではエラーとなりません。

でも、Request.ValidateInput()でマークフラグをセットすると、同じものが今度は例外となります。

rv6

続いて、同じコードでValidateInput()を呼ばずに、requestValidationMode=”4.0”の時。

rv7

IsValidationEnabledもtrueで、例外も起きます。

面白いのは、同じコードを続けて実行すると二回目以降は例外が発生しないところです。これは興味深い挙動ですね。

rv8

不思議な仕様ですねー。だって、この状態でも、Request.Unvlidated().QueryStringでオリジナルを取得することはできるんだから。過去の遺産だったりするのかなー。

rv9

やっと、本題。AllowHtmlを成立させるために必要なのはモデルのプロパティ名毎に検証を実施しないとダメですが、ここまでの流れで分かる通り、マッピング対象のコレクションにアクセスした段階で検証が発動する仕組みなので、このまますんなり行くとは思えません。がしかし、Reques.QueryStringやRequest.Formのアクセスは要素に関係なくエラーになるにもかかわらず、Actionへの引数となるマッピングの場合はマッピングが発生しない要素のにたいする検証は実施されてません。

rv10 rv11

どうやって実現してるんでしょうね。Form/QueryStringへのアクセスはすべて引っ掛けられてるなら途中介入するなんてできそうな気がしないです。

エントリを読み進めるとどうやら、コレクションが内部で保持しているArrayListとHashtableを置き換えてるようです。何と置き換えてるかはエントリの通り、LazyValidationArrayListとLazyValidationHashtableです。Microsoft.Web.Infrastracture.DynamicValidationHelper配下。だからといって、コレでも要素アクセスに限り検証し、プロパティマッピングでは検証しないというルールに繋がらないです。ふむむー。

改めてMVC3のソースを確認すると、そこにはIUnvalidatedRequestValuesと見慣れないインターフェースを使ったValueProviderたちがいました。あれれ?これの実装クラスがUnvalidatedRequestValuesクラスのインスタンスを内包したクラス。UnvalidatedRequestValuesといえば、Request.Unvalidated()で取得できる検証スキップコレクション。

さらに各種ValueProviderはNameValueCollectionValueProviderを派生したもので、IUnvalidatedValueProviderを実装してます。

public interface IUnvalidatedValueProvider : IValueProvider {
  ValueProviderResult GetValue(string key, bool skipValidation);
}

ははーん。見えてきましたね。MVC3からのValueProviderたちはコレを実装した形になっているので、マッピング対象の値をForm/QueryString等のコレクションから取得する際に、こっちのGetValueを呼び出すことで判断してるんですね。この中を少し見てみるとValueProviderResultPlaceholderを各ValueProviderからの戻り値とする際に、検証コレクションと未検証コレクションを切り替えて返す。

検証されるべきコレクションはインフラストラクチャの値をそのまま利用し、検証をスキップしたい時のコレクションはUnvalidatedRequestValuesを利用する。それをModelMetadataのRequestValidationEnabledから判定(AllowHtmlはここをセットするための属性クラス)。すっきりした!

途中出てきたLazyValidationArrayList/LazyValidationHashtableがインフラストラクチャが実施する検証を遅延してくれてるんだろーか。その辺はリフレクションとかしまくっててわかりにくし。FormやQueryStringにアクセスするとエラーが起きるのはそのままだから、なにが遅延かわかりにくいです。どーしてなーん?

こんな感じの実装になってるからAllowHtmlが成立するというのがわかったので良しとします。こうすることでWebPagesとも検証コードが同一のものにできるわけですね(WebPageHttpHandler.ProcessRequestInternalでEnableDynamicValidation)。この辺はSystem.Web.UI.Page派生じゃないものに対しても正しく検証を行うために必要なところですよね。Pageディレクティブないし。

ちょっとスッキリ。

2011年9月23日金曜日

DB Migration on EF4 alpha 3

Code First Migrations: Alpha 3 Released - ADO.NET team blog - Site Home - MSDN Blogs

EF.Migrationのアルファ3でた。前回はSQL CompactとSQL Expressへの依存が入ってたみたいで、ちゃんと動かせなかったけど、今回これは削除されたって。これでやっと試せる。

せっかくなので

↑この2つを試してみよう。

そのまえに!すでにEF4.1のパッケージをインストールしている場合は、削除するか最新版に更新しましょう。今回は削除したけど。これが残ってるとマイグレーションできないので気をつけてね。

まずはNoMagic(AutomaticMigrationsEnabled = false;)。書かれてる通りの操作をするとあれよあれよと、DBが更新されていく。

  1. データベースが作られてBlogsテーブルが出来る。
  2. Blogsにカラムとユニークインデックスの追加、Postsテーブルが追加。
  3. Postsにカラム追加しつつ、カスタムのUpdate文を実行。

※黄色い箇所は手書きしましょう。

既存のデータベースにたいしての、途中からのマイグレーションはできないってさ。

どうやってトラッキングしてるのかというとCodeFirstではEdmMetadataユーザーテーブルだったのが、__MigrationHistoryシステムテーブルに引っ越して管理されてます。

Add-Migrationで変更分のコードを自動生成。差分は手で入力。DbMigrationの派生クラスが作成されるので、いかようにもいじれますね。ただ、どんなメソッドがあるんだろね。ドキュメント希望。どっかにあるけ?

Update-DatabaseでデータベースにSQL発行。-Verbose付けとくと発行されるSQLが実行時に確認できます。ただ、手元の環境では-Scriptがエラーで動きませんでした。SQLをファイルにしてくれるオプションのはずなんだけどなー...。

続いてMagic(AutomaticMigrationsEnabled = true;)。何がマジックかというと、Add-MigrationしないでいきなりUpdate-Database。すると、ちゃんとMigrationHistoryにAutomaticMigrationとしてレコードが作られて、テーブルも作成されている。凄いね。

カスタムSQLの実行や、カラム初期値を指定する場合なんかだと、コードに指定したメタから生成される情報からだと足りないから、手書きで足してたんだけど、Automaticだとどうするかというと、これがまた単純にAdd-Migrationを呼んで、空のマイグレーションファイルを作成(Up/Downが空)しておいて、そこに書きこんでいくだけ。適用はUpdate-Databaseで、ドーン!

面白いね。しかもコレ、途中経過のDbMigrationがいないのに、ちゃんとDownしていく。なんで??-Verboseつけて実行すると、ちゃんと差分カラムの削除だけ実行してる...。まさにマジック!気持ち悪ーい。

でも、多分AutoMigrationはオススメしないかなー。何をやったのかの履歴が目で見てわからないもん。バージョン管理デキナイヨ!コードでするからやらないよ!?なのかな。

途中までTumblrで書いてたけど中途半端に長くなったので、こっちに移動しました。なのでスクリーンショットもなくてすいません。ただ!やればわかる!書いてることなぞるだけだし。MVC4のMobileチュートリアルよりよっぽどミスしないよ。

Ef41A3

せっかくなのでお試しあれー。

あ、そうそう、結局EFのCodeFirstなのでデータベースを接続文字列ベースにしたいときは、App.ConfigにconnectionStringsを追加してその名前をDbContextの派生クラス(今回ならBlogContext)のctor : base(“名前”)とすればいいです。

2011年8月28日日曜日

IIS7でnode.js

Installing and Running node.js applications within IIS on Windows - Are you mad? - Scott Hanselman

node.jsをIISで動かす!ファイル更新で自動リサイクルは楽チンでいいですね!受け口がIISだからIISのその他の便利モジュールがそのまま使えるのもいい。

install.batでやってることも紹介してくれてるので、同じ手順でIIS Expressでも大丈夫!なはず。

iisnode.dllっていうネイティブモジュールが橋渡し。よさそうな雰囲気。プロセス数の設定とか、いろいろ調整できるのも素晴らしす。

と、いうわけで、試しに動かしてみました。

インストール手順。

  1. node.jsのバイナリダウンロードしてc:\nodeにコピー
  2. iisnodeのバイナリをc:\inetpub/iisnodeにコピー
    ※ソースから自分でビルドしても可
  3. iisnode内のinstall.batを実行

以上。簡単ですね!node.exeのパスを変えたい場合は、install.batの確認場所の変更と、web.configでnodeProcessCommandLine指定でどーぞー。

node2

node

なんかいいね。6000req/sec。たいしたもんだ。ただ、iisnode.dllがちょいちょい落ちる(アプリケーションプールごと道連れ)。まだまだ発展途上。でもネイティブモジュールのスゴさを垣間見れるし、手軽にnode.jsをWindows環境で走らせることが出来ていいです!

ちなみにハンセルマンさんのエントリでも書いてる通り、AppPoolのアカウントをLocalSystemにしとかないと動きません。あと、wcatのsettings.ubrはちゃんとsettings{}で囲んでおきましょう。wcatについてはよく知りませんが(すません)、wcclientが動いてくれなくて、ちゃんと走りませんでした(手動起動で動かした)。

こんなに簡単にできるなら、何か面白そうなことを考えて試してみたり、node.jsでできることいろいろ追いかけて見たくなりますね!

2011年8月19日金曜日

HighPerformanceSessionStateProvider

ASP.NET Univarsal Providers のセッションプロバイダを使ってみる (2) « ブチザッキ

カメさんのこのエントリーに対してこんなコメントをしてみたわけですが、具体的なコードを提示しないで案だけ出すって、そんな失礼なことがあっちゃいけねー。江戸っ子なら宵越しの銭はもってちゃいけねー。江戸っ子じゃないんですけどね。

まずは現状確認。カメさんのコードをいただきつつ。ASP.NET MVC3のサイトをデフォルトで作成。ASP.NET Universal ProvidersをNuGetでインストール。web.configでSessionStateの設定をCookieless="UseUri"とTimeout="1"に変更(検証が簡単だからね)。

using System;
using System.Net;

namespace ConsoleApplication1
{
  class Program
  {
    static void Main(string[] args)
    {
      System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();

      GetPage();

      int count = int.Parse(args[0]);

      for (int i = 0; i < count; i++)
      {
        sw.Reset();
        sw.Start();
        var success = GetPage();
        sw.Stop();
        Console.ForegroundColor = success ? ConsoleColor.White : ConsoleColor.Red;
        Console.WriteLine("{0}\t{1}\t{2}\t{3}", 
            DateTime.UtcNow.ToString("yyyy/MM/dd hh:mm:ss.fff"), 
            sw.Elapsed, i, success ? "OK" : "NG");
      }
    }

    static private bool GetPage()
    {
      var result = true;
      try
      {
        var request = WebRequest.Create("http://localhost:52855/");
        {
          using (var response = request.GetResponse())
          {

          }
        }
      }
      catch
      {
        result = false;
      }

      return result;
    }
  }
}

ずるくないもん!オマージュだもん!

コレとMVCサイトで実行した結果(クリックでズーム)。

up1

セッションを生成するコンソールは3個でCPUはMAX。白行はエラーなく進んだところで、赤行はASP.NET側でエラーになったところ。

up2

1分でセッションは切れるので、1分後からドカドカ古いセッションの削除が始まります。

概ね680セッションあたりで飽和。エラーと正常実行を繰り返す感じです。切ないですね。セッションの削除が同時実行されててんやわんやな例外。

これを解消するために、InitializeRequestをoverrideしたSessionStateProviderクラスを定義します。

やることはSessionsテーブルにExpires列のインデックスがなければ作成と、セッション初期化時の有効期限切れセッションの削除をEFのDeleteObjectじゃなくSQLのDelete文を実行するように。

ちょっと長いですけど。

using System;
using System.Configuration;
using System.Data.Common;
using System.Reflection;
using System.Web;
using System.Web.Providers;
using System.Web.Providers.Entities;

namespace UniversalProviders
{
  public class HighPerformanceSessionStateProvider : DefaultSessionStateProvider
  {
    private string _connectionStringName;
    private bool _initialized = false;

    public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config)
    {
      _connectionStringName = config["connectionStringName"];

      base.Initialize(name, config);
    }

    private void ExecuteSql(ConnectionStringSettings connectionStringSettings, Action<DbProviderFactory, DbCommand> functor)
    {
      var providerName = connectionStringSettings.ProviderName;
      var factory = DbProviderFactories.GetFactory(providerName);
      using (var connection = factory.CreateConnection())
      {
        connection.ConnectionString = connectionStringSettings.ConnectionString;
        connection.Open();

        var command = connection.CreateCommand();

        functor(factory, command);
        command.ExecuteNonQuery();
        connection.Close();
      }
    }

    private void CreateSessionIndex(ConnectionStringSettings connectionStringSettings)
    {
      ExecuteSql(connectionStringSettings, (factory, command) =>
      {
        command.CommandText = @"IF not EXISTS (SELECT * FROM sys.indexes WHERE object_id = OBJECT_ID(N'[dbo].[Sessions]') AND name = N'IX_Sessions')
begin
CREATE NONCLUSTERED INDEX [IX_Sessions] ON [dbo].[Sessions] 
(
[Expires] ASC
) ON [PRIMARY]
end
";
      });
    }

    private void RemoveExpiredSessions(ConnectionStringSettings connectionStringSettings)
    {
      ExecuteSql(connectionStringSettings, (factory, command) =>
      {
        command.CommandText = "delete Sessions where Expires < @0";
        var parameter = factory.CreateParameter();
        parameter.ParameterName = "@0";
        parameter.Value = DateTime.UtcNow;
        command.Parameters.Add(parameter);
      });
    }

    private MethodInfo GetCreateSessionEntities()
    {
      var modelHelper =
        Assembly.GetAssembly(typeof(Session))
                .GetType("System.Web.Providers.Entities.ModelHelper");
      return modelHelper.GetMethod("CreateSessionEntities", 
        BindingFlags.NonPublic | BindingFlags.Static);
    }

    public override void InitializeRequest(HttpContext context)
    {
      var connectionStringSettings = ConfigurationManager.ConnectionStrings[_connectionStringName];
      if (!_initialized)
      {
        var initializer = GetCreateSessionEntities();
        initializer.Invoke(null, new object[] { connectionStringSettings });
        CreateSessionIndex(connectionStringSettings);
        _initialized = true;
      }

      RemoveExpiredSessions(connectionStringSettings);
    }
  }
}

クラス名はもちろんHighPerformanceSessionStateProviderデス!

up3

<sessionState mode="Custom" customProvider="HighPerformanceSessionProvider" cookieless="UseUri" timeout="1">
  <providers>
    <add name="DefaultSessionProvider"
         type="System.Web.Providers.DefaultSessionStateProvider, System.Web.Providers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
         connectionStringName="DefaultConnection" applicationName="/" />
    <add name="HighPerformanceSessionProvider"
         type="UniversalProviders.HighPerformanceSessionStateProvider, UniversalProviders"
         connectionStringName="DefaultConnection" applicationName="/" />
  </providers>
</sessionState>

Web.configも上記のように変更しちゃいましょう!customProviderの指定がDefaultSessionProviderでオリジナルに戻ります。

セッションが飽和するのが、940件くらいのところです。もちろん例外なんて起きませんよ。有効期限切れのレコード削除なんて何回実行したって、無いものはない!潔し!ちなみに先程の数値もそうですけど、コレが同時実行の限界値なわけではないです。マイノートPCの同時実行の限界くらいです。Webの受付のね。DbProviderFactoriesを利用することでEFと同じだけのポータビリティ(言い過ぎ)。

名前ほど早くないけど、エラーが起きないから実用的なんじゃん?読み込み性能は気にしてないでーす。あくまで新規セッション作成時のパフォーマンス向上委員会です。言い訳がましい...。

ご利用は計画的に!

Azureでは動かしてないので、そちらの検証はたぶんカメさんがその3でやってくれるんじゃないでしょーか。

2011年8月14日日曜日

Controllerを名前から生成するしHTMLを書き換えたりもしてみる

Developer @ ADJUST : ASP.NET MVC のコントローラクラスを、コントローラ名から取得したいのだけれど...結局、ぜんぶ列挙!?

前回のエントリーからはや1ヶ月以上経過したこのブログ。エアコンが壊れてて、バリバリ節電に貢献できてる気はするけど、辛い毎日でブログどころではないですよ!

で、坂本さん、上記の件ですが以下のような方法はいかがでしょうか。単純に型が欲しいだけならインスタンスは不要なので無駄な感じはしなくもないですが。必要なのがインスタンスであれば一番いい方法だと思います。

var factory = ControllerBuilder.Current.GetControllerFactory();
var controller = factory.CreateController(Request.RequestContext, "Home");
var type = controller.GetType();
factory.ReleaseController(controller);

コレと言って、自信で発明したものはなく、やってることはMvcHandlerでやってることと同じです。MvcHandlerってあれです、MvcRouteHandlerが返してくるIHttpHandlerのことです。

内部でタイプキャッシュとかしてくれてるので、パフォーマンスには自信があります!あると思います!

ここから別件。

もう暑さで脳みそが働かない...。けど、これだけじゃ坂本さんも納得しないと思うので、全然関係ない面白い機能紹介。といっても、URLRewrite 2.0の機能です。ハイパフォーマンスWebサイトの構築には必須のOutputCacheなんですが、クローラーなんかはUser AgentがCookieに対応してないと言ってくるじゃないですか。browserファイルで対応するのが王道ですが、それだと追従していくのが大変面倒くさい。

Creating Outbound Rules for URL Rewrite Module : URL Rewrite Module 2 : URL Rewrite Module : The Official Microsoft IIS Site

想定している状況を簡単にいうと、SessionのcookielessはuseDeviceProfile。PCサイトとケータイサイトを同一アプリケーションで実行。PCサイトは積極的にOutputCacheを利用し、ケータイサイトでは消極的(Viewでは使わない)に利用。この場合、PCサイトにクローラが来るとそのままではCookielessとしてセッションが生成されるので、OutputCacheにSessionIDを保持したHTMLが出力されてしまって都合悪い!

ということなんですが、伝わるでしょうか...。

OutputCacheをVaryByUserAgentにするのも手ですね。いろいろ方法はあると思うんですけど、こないだ試したのがPCサイトの場合、出力されるHTMLにSession IDが含まれることが正常系ではありえないので、PCサイトの場合、Response.FilterでURLにSession IDが含まれてたら消してしまうという方法。Response.Filterでやるよりももっと簡単なのがURL Rewrite 2.0の出力書き換えを利用するっていう方法。どうですか、面白そうじゃないですか?

ちなみにOutbound Rule用に独自Provider書いたりもできるけど、そこまでやることはあんまりないかなー、と思いますがどうでしょう。

<rewrite>
  <outboundRules>
      <rule name="Sessionless" preCondition="html" enabled="true">
          <match filterByTags="A, Area, Base, Form, Frame, Head, IFrame, Img, Input, Link, Script, CustomTags" customTags="All" pattern="(.*)/\(S\([0-9a-z]+\)\)(.*)" />
          <action type="Rewrite" value="{R:1}{R:2}" />
          <conditions>
              <add input="{REQUEST_URI}" matchType="Pattern" pattern="\)/mobile" ignoreCase="true" negate="true" />
          </conditions>
      </rule>
      <customTags>
          <tags name="All" />
      </customTags>
      <preConditions>
          <preCondition name="html">
              <add input="{RESPONSE_CONTENT_TYPE}" pattern="text/html" />
              <add input="{REQUEST_URI}" pattern="/mobile" negate="true" />
          </preCondition>
      </preConditions>
  </outboundRules>
</rewrite>
<urlCompression doDynamicCompression="false" />

だいたいこんな感じの設定です。これをWeb.configに書いておきましょう。system.webServer配下です。ケータイサイトが/mobile配下という前提です。

実験。MVCサイトをデフォルトのまま生成し、Web.configに上記設定を追加しないで動作を確認。

この状態で出力されるのは↓。

cookieless1

そりゃそうですね。中身は↓。

cookieless2

なんてことないですよね。

今度はsessionStateのcookielessをUseUriにしてみましょう。見た目は同じなのでソースだけ。

cookieless3

画像をクリックして大きくしてみるとわかりますが、Session IDを含んだURLですよね。

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>Home Page</title>
    <link href="/(S(f4grns0hiatkwquk55qulbmd))/Content/Site.css" rel="stylesheet" type="text/css" />
    <script src="/(S(f4grns0hiatkwquk55qulbmd))/Scripts/jquery-1.5.1.min.js" type="text/javascript"></script>
    <script src="/(S(f4grns0hiatkwquk55qulbmd))/Scripts/modernizr-1.7.min.js" type="text/javascript"></script>
</head>
<body>
    <div class="page">
        <header>
            <div id="title">
                <h1>My MVC Application</h1>
            </div>
            <div id="logindisplay">
                    [ <a href="/(S(f4grns0hiatkwquk55qulbmd))/Account/LogOn">Log On</a> ]
            </div>
...以下省略

今度は先のURLRewrite設定をOnにした場合。

cookieless4

あら素敵。ウソじゃないよ!URLにはちゃんとSession ID入ってるでしょ?

これでクローラが来てもOutputCacheにへんてこなものが仕込まれることないですね!あと、動的圧縮をオフにしてるのは既知です。設定いいんですけど今回はこれで。

役に立つやら立たないやらな情報でした!

2011年6月30日木曜日

全角数値を半角数値に変換するModelBinder

拝啓、まゆきっつぁん

いつも、The Shodoにて筆の練習をさせていただいてます。ただ、いつまで経っても筆ぺんでは上手にかけるようになれません。本物の習字道具を使わないと練習の成果が出ないのでしょうか?The Fudepenで練習すると効果が期待できるかもしれないですね。是非、考慮いただければと思います。

先日のmvcConf @:Japan懇親会での一件について。ふと、思い出したので書いてみました。あの時はValueProviderがどうのこうのという話になったような気がしないでもないですが、ValueProviderではレイヤ低すぎて型は意識されてないのダメですね。

こんな感じでいかがでしょうか?整数に限定してますが、応用すると他にもいろいろできると思います。

public class IntegralModelBinder : DefaultModelBinder
{
  private readonly List<Type> _integralTypes = new List<Type>
  {
    typeof (sbyte),
    typeof (byte),
    typeof (char),
    typeof (short),
    typeof (ushort),
    typeof (int),
    typeof (uint),
    typeof (long),
    typeof (ulong)
  };
  private const string WideIntegrals = "1234567890一二三四五六七八九零壱弐参肆伍陸柒捌玖零";
  private const string NarrowIntegrals = "123456789012345678901234567890";

  protected override void SetProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, object value)
  {
    if (!_integralTypes.Contains(propertyDescriptor.PropertyType) || value != null)
    {
      base.SetProperty(controllerContext, bindingContext, propertyDescriptor, value);
      return;
    }

    var providerResult = bindingContext.ValueProvider.GetValue(propertyDescriptor.Name);
    value = providerResult.AttemptedValue;

    var narrow = string.Join("",(value + "").Select(c => WideIntegrals.Contains(c) ? NarrowIntegrals[WideIntegrals.IndexOf(c)] : c));
    var converter = TypeDescriptor.GetConverter(propertyDescriptor.PropertyType);
    try
    {
      value = converter.ConvertFrom(narrow);
      bindingContext.ModelState.Remove(propertyDescriptor.Name);
    }
    catch{}

    base.SetProperty(controllerContext, bindingContext, propertyDescriptor, value);
  }
}

試しに以下のようなモデルを定義してみました。

    public class Person
    {
        public string Name { get; set; }
        public int Age { get; set; }
        public DateTime? Birthday { get; set; }
        public byte Rank { get; set; }
    }

AgeとRankが整数型なので処理対象となります。Global.asaxでDefaultBinder(ModelBinders.Binders.DefaultBinder = new IntegralModelBinder();)を差し替えて実行した結果は↓こんな感じになります。

mb1

@model ZenBinder.Models.Person
@{
    ViewBag.Title = "ホーム ページ";
}

<h2>@ViewBag.Message</h2>

@using (Html.BeginForm())
{
    @Html.EditorForModel()

    <button type="submit">送信</button>
}

まずはフォームを出すでしょう。簡単にEditorForModelを使います。

mb2

普通に半角だけで試して送信してみると、ちゃんと動きます。同じものを全角にしてみても結果は同じになります。

mb3

変換出来ない場合はDefaultModelBinderの挙動になります。十とか百とか千も変換するためのマッピングを用意すれば、もう少しオシャレさが増すかもしれないです。

mb4

いかがでしょう。ケータイでの入力に是非応用してみてください。

dotnetConf2015 Japan

https://github.com/takepara/MvcVpl ↑こちらにいろいろ置いときました。 参加してくださった方々の温かい対応に感謝感謝です。