ラベル ASP.NET の投稿を表示しています。 すべての投稿を表示
ラベル ASP.NET の投稿を表示しています。 すべての投稿を表示

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年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年4月24日日曜日

IIS ExpressでSSLを有効にしてデバッグ

Working with SSL at Development Time is easier with IISExpress - Scott Hanselman

Hanselmanさんのエントリに書かれてる通りデス!

IIS ExpressでSSLを有効にしたデバッグだと無効な証明書だと警告はでるけど、HTTPSでのテストはコレでできますよね。

だけど、名前はlocalhostに限定です。これはまぁ、そういうバインドしかapplicationHost.configに登録してないからなのと、アクセス許可をしてなからなんですが(詳しくはmvcConf 2 - Vaidy Gopalakrishnan: IIS Express | mvcConf | Channel 9)、その辺をちゃんと登録して気持ちよく使いましょうという内容ですね。途中URLRewriteでAccountControllerへのアクセスをHTTPSに書き換えるというのが挟まってるけど、コードで対応するならここは飛ばしてもOKです。

netshへのcerthashの登録、信頼されたルート証明機関の証明書として証明書を登録、最終的にIisExpressAdminCmdでACLの許可登録で気持よく、マシン名でもSSL有効な状態でアクセスできます。

というのを、自分の環境でも確認してみましょう。

1.ASP.NET MVCサイトを作成

もちろんVS2010で行うのが簡単ぽんなんですが、VS2008でも同じようにできるので(applicationHost.configとnetshなので)、そっちしかない場合はそっちで。プロジェクトの設定で、外部プログラムを起動するを利用しましょう(IIS ExpressのVS2008/VS2010利用)。

今回は簡単なVS2010で新しくなったMVC3 Toolsで作ります。

ssl1

いい感じにHTML5ですね!

2.プロジェクトのプロパティでSSL有効化

ssl2

trueにするだけの簡単な作業です。これでSSL URLが入ります。この例だと https://localhost:44301/ ですね。

この状態でまずは実行してみましょう。

ssl3

文句は言われるけど、普通に問題なくアクセスできますね。楽チンデス。IIS Express最高!

3.localhostじゃなく好きな名前でアクセス

マシン名でもいいんですが、そこはほら、恥ずかしい名前付けてる場合もあるじゃないですか。そんな状態で表示するのもどうかと思うわけですよ。なので、今回は完全カスタムな名前にしてしまいます。

takepara-local

という名前にします。これが恥ずかしい名前じゃないかと言われると、まぁ微妙ですが。あ、ちなみに自分のマシン名はそんなに恥ずかしい名前じゃないですよ。

IisExpressAdminCmdでアクセスできるようにしましょう。

※”.”でドメインな感じのマシン名だとsetupFriendlyHostnameUrlに失敗します。

ssl6

これでブラウザからアクセスしてみましょう!

ssl7-2

残念。エラーです。何か忘れてますね。そうですね、applocationHost.configにバインド登録してなかったですね。

<site name="SslTest" id="15">
    <application path="/" applicationPool="Clr4IntegratedAppPool">
        <virtualDirectory path="/" physicalPath="..." />
    </application>
    <bindings>
        <binding protocol="http" bindingInformation="*:51578:localhost" />
        <binding protocol="https" bindingInformation="*:44301:localhost" />
        <binding protocol="http" bindingInformation="*:80:takepara-local" />
        <binding protocol="https" bindingInformation="*:443:takepara-local" />
    </bindings>
</site>

ssl7

グレートですね。ちなみにhostsにtakepara-local登録して、netshでurlacl登録しても一緒です。

このまま、調子にのってHTTPSにしてみる。

ssl8

残念。ダメでした。

4.証明書を作成

takepara-localの証明書を作成してませんでしたね。hanselmanさんのエントリに書かれてるとおりです。makecertしましょう。CNだけ変えて後はそのままで。

ssl9

makecert -r -pe -n "CN=TAKEPARA-LOCAL" -b 01/01/2000 -e 01/01/2036 -eku 1.3.6.1.5.5.7.3.1 -ss my -sr localMachine -sky exchange -sp "Microsoft RSA SChannel Cryptographic Provider" -sy 12

VSコマンドプロンプトから実行しましょう。パス通ってて確実。

ssl10

証明書をnetshで登録。

netsh http add sslcert ipport=0.0.0.0:443 appid={214124cd-d05b-4309-9af9-9caa44b2b74a} certhash=864759f75a04d030e243ecf1cfe19ce336653374

ssl11ssl12

警告でるけど、気にせず表示。

5.証明書を信頼

このままでもいいんですが、やっぱり気持よく。

ssl13

信頼しちゃいましょう。自分のマシンで、自分で作った証明書。信頼しちゃえばいいじゃないか!

ssl14

Chrome厳しす。まぁ、いいか。警告でなくなったしね!

6.Accountコントローラを厳しく

URLRewriteで同じようになってもいいんですが、あえてコードで対応しましょう。と、言っても難しいことはなく、AccountControllerにRequireHttps属性を付けるだけ。

ssl15

これで、AccountControllerへのアクセスがHTTPSになってなければ、リダイレクトされます。リダイレクトされるということはHTTPでのPOSTは無視される(リダイレクトはGETデス)ので、気をつけましょう。

ssl16

この動きが気持ち悪いときはViewでURLを埋め込むときにHTTPSにしてあげる必要があるので、修正箇所が随分増えちゃいますが、通常の開発であればViewでもちゃんと書いて、ControllerへのRequireHttpsも付ける2段構えになるのが王道でしょう。抜け道なし。ViewのURLなんて嘘つくからね。

例えば、_LogOnPartial.cshtmlを以下のようにする。

@if(Request.IsAuthenticated) {
    <text>Welcome <strong>@User.Identity.Name</strong>!
    [ @Html.ActionLink("Log Off", "LogOff", "Account", "https", "takepara-local", "", null, null) ]</text>
}
else {
    @:[ @Html.ActionLink("Log On", "LogOn", "Account", "https", "takepara-local", "", null, null) ]
}

これで、必ずHTTPSのURLが生成されます。

画像てんこ盛りですが、手順通りやればうまくいくので、HTTPSでのテストをやりたい場合は是非どうぞ。

なんか、うまくできない!という場合はタスクトレイのIIS Expressを終了させてやり直してみるといいかも。applicationHost.config書き換えしたときなんかは特に。

2011年3月12日土曜日

WebActivatorでお手軽Bootstrapper

いろんなセッションビデオやサンプルコードによく登場してるWebActivator。属性指定のしかたから、.NET4で登場したPreApplicationStartMethod(AssemblyInfo.csに指定するやつ)に関する何かだろうと勝手に解釈して、ちゃんと見てなかったんだけど、なんか急に見てみたくなった。

Light up your NuGets with startup code and WebActivator - Angle Bracket Percent - Site Home - MSDN Blogs

解釈はおおむね間違ってないんだけど、PreAppricationStartMethodだと1個しか指定できない(よね?)のと、フックポイントが起動直後しかない(そりゃそうだ)のを拡張してしまおうというものでした。なるほど。

Pre/Post/Shutdowの3箇所に仕込めるんですね。

  • PreApplicationStartMethod
  • PostApplicationStartMethod
  • ApplicationShutdownMethod

この3種類。うまいこと考えてますね。PreとPostはAOPっぽい感じです。タイミングはPreはそのままPreなんだけど、PostはMicrosoft.Web.Infrastructure.DynamicModuleHelper.DynamicModuleUtility.RegisterModule を使って動的にロードする初期化HttpModule(これ自体ちゃんとWebActivatorに含まれてるので気にすることは無いですね)のInitイベントで実行。ShutodownはそのHttpModuleのDispose時に実行。

David Ebbo: Register your HTTP modules at runtime without config

面白いですね。賢いですね~。アセンブリ属性にしてるのは使い勝手を考慮してのことでしょう。属性宣言とクラス実装が近くにあるはずだから、クラス属性にしてもいいような気もするけど、使い勝手は大事でしょう!

[assembly: WebActivator.PreApplicationStartMethod(typeof(クラス), "実行メソッド名")]
[assembly: WebActivator.PostApplicationStartMethod(typeof(クラス), "実行メソッド名")]
[assembly: WebActivator.ApplicationShutdownMethod(typeof(クラス), "実行メソッド名")]

こんな感じでアセンブリ内に宣言します。いくつ宣言してもいいです。ActivationManager(これが標準のPreApplicationStartMethodで指定されてるよ)が実行時にbinフォルダのアセンブリから全部抽出してくれます。

davidebbo / WebActivator / overview – Bitbucket

オレオレBootstrapperを卒業するときがきましたね!

注意点として、HttpModuleを利用するPostとShutdownはAppDomainがロードされなおすたびに実行されるので、何度も実行されるのでその辺気をつけましょう。

とにかくNuGetで Install-Package webactivator とタイプしてインストールしてみて以下のコードで動かしてみました。

[assembly: WebActivator.PreApplicationStartMethod(typeof(Bootstrapper1), "Pre")]
[assembly: WebActivator.PreApplicationStartMethod(typeof(Bootstrapper2), "Pre")]
[assembly: WebActivator.PreApplicationStartMethod(typeof(Bootstrapper3), "Pre")]

[assembly: WebActivator.PostApplicationStartMethod(typeof(Bootstrapper1), "Post")]
[assembly: WebActivator.PostApplicationStartMethod(typeof(Bootstrapper2), "Post")]
[assembly: WebActivator.PostApplicationStartMethod(typeof(Bootstrapper3), "Post")]

[assembly: WebActivator.ApplicationShutdownMethod(typeof(Bootstrapper1), "Shutdown")]
[assembly: WebActivator.ApplicationShutdownMethod(typeof(Bootstrapper2), "Shutdown")]
[assembly: WebActivator.ApplicationShutdownMethod(typeof(Bootstrapper3), "Shutdown")]

namespace Mvc3
{
    public class Bootstrapper
    {
        public static void Pre(string name)
        {
            Console.WriteLine("Pre : "+name + " " + DateTime.Now);
        }

        public static void Post(string name)
        {
            Console.WriteLine("Post : " + name + " " + DateTime.Now);
        }

        public static void Shutdown(string name)
        {
            Console.WriteLine("Shutdown : " + name + " " + DateTime.Now);
        }
    }

    public class Bootstrapper1
    {
        private static string Name = "1番";
        public static void Pre()
        {
            Bootstrapper.Pre(Name);
        }

        public static void Post()
        {
            Bootstrapper.Post(Name);
        }

        public static void Shutdown()
        {
            Bootstrapper.Shutdown(Name);
        }
    }

    public class Bootstrapper2
    {
        private static string Name = "2番";
        public static void Pre()
        {
            Bootstrapper.Pre(Name);
        }

        public static void Post()
        {
            Bootstrapper.Post(Name);
        }

        public static void Shutdown()
        {
            Bootstrapper.Shutdown(Name);
        }
    }

    public class Bootstrapper3
    {
        private static string Name = "3番";
        public static void Pre()
        {
            Bootstrapper.Pre(Name);
        }

        public static void Post()
        {
            Bootstrapper.Post(Name);
        }

        public static void Shutdown()
        {
            Bootstrapper.Shutdown(Name);
        }
    }

}

VS2010SP1だとIIS Expressで実行ができるけど、あえて外部コマンドでIIS Expressを指定して実行してみます。

wa

コンソールのスクリーンショットを取りたかったからデス!これって統合されたらどこで見ればいいんだろか。出力ウィンドウだとちょっと違うじゃん?ん~。まぁ、いいか。おいおい分かるでしょう。

残念ながらShutdownは表示するまで待つのも設定変えるのも面倒(IIS ExpressだとidleTimeoueでシャットダウンしない?)なので未確認!雰囲気が伝わればいいかな、なんて。

2010年10月17日日曜日

カペタ

カペタが面白いとクマさんが言ってたので読んでみたら(全然グルメ漫画とかじゃなく予想外だったけど)、これがかなり面白くてびっくりした。

サルベージすらしてなくてすいません...。

そろそろ真面目にData Services&PowerPivot使ってデータ集計できるようにしておくかと思い、どりゃっとセットアップしてたんですが、100万レコード超えたりとかしてるでかいテーブルに対して、全然レスポンスが帰ってこないくて困った。そんな方、他にもいないですか?あ、ADO.NET Data Servicesのほうです。.NET3.5のほうです。WCFじゃないほうです。

動作的にあからさまにタイムアウトにもかかわらず、レスポンスストリームに何も出力されないっていう気持ち悪い状況なんですよね。コマンドラインでcurl使ってリクエストしてもレスポンスがないぜ!って怒られる。んで、どのレイヤーでタイムアウトしてるのかな~って思うわけじゃないですか。

なんだかんだ言ってもWCFなんでbindingら辺が怪しいのかな~と思っていろいろセットしてみたんだけど、よくわからん。

ADO.NET Data Services, Entity Framework und SQL/HTTP Timeouts | Marco Scheel aka GeekDotNet

↑ここにいろいろ書いてるのを見て試したんですけど、どれもイマイチ。CreateDataSourceのCommandTimeout設定なんてちょっと素敵な感じですよね。いろんなレイヤー気にするのもな~、と思って思い出したんですよね。

ダウンロードの詳細 : Windows 7 および Windows Server 2008 R2 用 .NET Framework 3.5 SP1 の ADO.NET データ サービス更新プログラム

これ。そーか、と。サーバーサイドページングを有効にしておけば、まるっとすべて解決なんじゃんね。データ件数をそもそも絞ってしまえばどのレイヤででもタイムアウトしないじゃんよ!ってことですよ。PowerPivotはさ、賢いから自動でページングして全データ抽出してくれるんですよ。

Server Paging in Data Services - WCF Data Services Team Blog - Site Home - MSDN Blogs

InitializeServiceにIDataServiceConfigurationでそのままconfigを受け取ってると見えないけど、DataServiceConfirugarionクラスとして受け取るか、asで型指定してしまえば見えるようになるじゃないですか。

var v2 = config as DataServiceConfiguration;
if (v2 != null)
{
 v2.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V2;
 v2.SetEntitySetPageSize("*", 100);
}

SetEntittSetPageSizeですべてのテーブルを100件ページング指定してしまえばいいね!

2010年9月23日木曜日

LosFormatter

Validation of viewstate MAC failed error - Stack Overflow

なんか面白そう~、な感じだったので試してみたけど、思った結果にはなりませんでした。

  public class HomeController : Controller
  {
    public ActionResult Index()
    {
      var person = new Person {Name = "ルフィー", Age = 18};
      
      // machinekey
      var macKeyModifier = "";
      var machineKey = WebConfigurationManager.GetSection("system.web/machineKey") as MachineKeySection;
      if(machineKey!=null)
      {
// DecryptionKeyでも同じでした... macKeyModifier = machineKey.ValidationKey; } //plain var los = new LosFormatter(); using (var writer = new StringWriter()) { los.Serialize(writer,person); ViewData["stateLos"] = writer.ToString(); } var los2 = new LosFormatter(true, macKeyModifier); using (var writer = new StringWriter()) { los2.Serialize(writer, person); ViewData["stateLosMac"] = writer.ToString(); } return View(person); } }

ASPX

    <%: ViewData["stateLos"]%>
    <%: ViewData["stateLosMac"]%>
    <%: Html.Serialize("statePlain", Model, SerializationMode.Plaintext)%>
    <%: Html.Serialize("stateSigned", Model, SerializationMode.Signed)%>
    <%: Html.Serialize("stateEncrypted", Model, SerializationMode.Encrypted)%>
    <%: Html.Serialize("stateEncryptedAndSigned", Model, SerializationMode.EncryptedAndSigned)%>

HTML

    /wEywQEAAQAAAP////8BAAAAAAAAAAwCAAAAQFN0YXRlVGVzdCwgVmVyc2lvbj0xLjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPW51bGwFAQAAABdTdGF0ZVRlc3QuTW9kZWxzLlBlcnNvbgIAAAAVPE5hbWU+a19fQmFja2luZ0ZpZWxkFDxBZ2U+a19fQmFja2luZ0ZpZWxkAQAIAgAAAAYDAAAADOODq+ODleOCo+ODvBIAAAAL
    /wEywQEAAQAAAP////8BAAAAAAAAAAwCAAAAQFN0YXRlVGVzdCwgVmVyc2lvbj0xLjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPW51bGwFAQAAABdTdGF0ZVRlc3QuTW9kZWxzLlBlcnNvbgIAAAAVPE5hbWU+a19fQmFja2luZ0ZpZWxkFDxBZ2U+a19fQmFja2luZ0ZpZWxkAQAIAgAAAAYDAAAADOODq+ODleOCo+ODvBIAAAAL
    <input name="statePlain" type="hidden" value="/wEywQEAAQAAAP////8BAAAAAAAAAAwCAAAAQFN0YXRlVGVzdCwgVmVyc2lvbj0xLjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPW51bGwFAQAAABdTdGF0ZVRlc3QuTW9kZWxzLlBlcnNvbgIAAAAVPE5hbWU+a19fQmFja2luZ0ZpZWxkFDxBZ2U+a19fQmFja2luZ0ZpZWxkAQAIAgAAAAYDAAAADOODq+ODleOCo+ODvBIAAAAL" />

    <input name="stateSigned" type="hidden" value="/wEywQEAAQAAAP////8BAAAAAAAAAAwCAAAAQFN0YXRlVGVzdCwgVmVyc2lvbj0xLjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPW51bGwFAQAAABdTdGF0ZVRlc3QuTW9kZWxzLlBlcnNvbgIAAAAVPE5hbWU+a19fQmFja2luZ0ZpZWxkFDxBZ2U+a19fQmFja2luZ0ZpZWxkAQAIAgAAAAYDAAAADOODq+ODleOCo+ODvBIAAAALIrCvmCvq7EIjLblEZK6YIw0F/48=" />
    <input name="stateEncrypted" type="hidden" value="tMKYGlOhnBXLHzreFpfQURQbcha/ZHrcdQbcNs7Vxud0Nq4yQ41FUq1DDqDnORVqxV+VSfpOFHqQFA/ylboLGtpQRbH46a3YJsRqzKf5dlq7tOlp7Ys3zMCWE8ozJ4m2FUG9/bq7KVCo9UsNxOjLWkokHQtr/SORahKAPnbeuFsHOn0B3fl0d1DPtl9MPANWp5qwqqdi+c7JuplYyFT6tEqm3Og+M7fJYnmc3Vkj1oiHclyMCvh6X9Ti9uyPuVvwGpBa2pj6gbETDQa6pQJ4rQ==" />
    <input name="stateEncryptedAndSigned" type="hidden" value="tMKYGlOhnBXLHzreFpfQURQbcha/ZHrcdQbcNs7Vxud0Nq4yQ41FUq1DDqDnORVqxV+VSfpOFHqQFA/ylboLGtpQRbH46a3YJsRqzKf5dlq7tOlp7Ys3zMCWE8ozJ4m2FUG9/bq7KVCo9UsNxOjLWkokHQtr/SORahKAPnbeuFsHOn0B3fl0d1DPtl9MPANWp5qwqqdi+c7JuplYyFT6tEqm3Og+M7fJYnmc3Vkj1oiHclyMCvh6X9Ti9uyPuVvwGpBa2pj6gbETDQa6pQJ4rQ==" />

machineKey

    <machineKey
      validationKey="CABF0BEECF16B90F1BF9B5196A0E12EBBB950D6088FEDC9D2A674D75BE461713A1B9EA89C7B774CA249A45605B64994C54B9F59DA06AC673DE55A4661A7AE6DC"
      decryptionKey="79B6DB1CACB7B81A5EF317F43D5B05BE6872F3C285544222E32F36820785A4E4"
      validation="SHA1" 
      decryption="AES"/>

LosFormatter自体が内部でObjectStateFormatterを呼び出してた(Reflector確認)ので、イケるかな~と思ったんだけどな~。LosFormatterの使い方間違ってますかね?

2010年9月5日日曜日

クッキーとビスケットは何が違うんですか?

IMG_0056

なにこのモンスター...。

IMG_2568

これがラテアートになると↑こうなっちゃうんだから不思議です。

ここ最近ずっと悩まされていた問題にかたがつきました。石野さん、例の問題がやっと落ち着きました。

「ユー、ブログにちゃんと書いときなYO」と大きな後輩に言われ、いじめが怖いのでちゃんと書いておくことにします。

そもそも問題となった現象は「au端末でのPOSTリクエスト時に500エラー(Internal Server Error)が発生する」というものです。これはあくまで表面的な現象で本質は別のところにあるんですが、この状況から問題を特定し解決するに至る長い長い戦い。血で血を洗う争い。

結論だけ先に書くと「ASP.NETでセッションをLB配下で利用する場合、machineKeyだけじゃなくIIS7でのSite IDも同一の値にしておかないと、SQLServerセッションストアでセッションデータを正しく取り出せない」です。

PRB: セッション状態が損失 Web ファームで SqlServer または StateServer セッション モードを使用します。

これまでこの問題に出会ったことがなかったので原因を特定するのにとても苦労しました。

Web サイトのアプリケーション パスを同期化する

こっちではインスタンスIDと言ってますね。

ちなみに、構築環境はテスト環境(ステージング)と本番環境(ライブ)の2つありステージングでは全く問題が発生しないという前提条件があります。サーバー機は同一(Windows Server 2008 R2でIIS7.5)なのにくわえアセンブリもバイナリレベルで同一(.NET 3.5SP1)です。SSLのホスト名と発行元が違う、あと暗号化ビット長がテスト環境では1024ビット、本番では2048ビットという違いもあります。LBでIPスティッキー設定をしてるので同一IPからのリクエストはすべて同一ホストにルーティングされるようにもなってます。そもそもセッションはDBにいれてるんだからスティッキー必要ないんだけど、そのことに気がつかず、必要でしょ!みたいなのりで環境設定を行ったのも問題を見つけづらくする原因でした。

パフォーマンス: ASP.NET アプリケーションのスケーリング戦略
ロードバランスクラスタの実装

最初に疑ったのは証明書のビット長。auで2048ビットで問題があるという情報を良く見かけますよね。プログラムコードはテスト環境で問題なく動いてるものだから、問題が内在してるとしても、それは別の問題で今回の現象が発生する原因ではないと思ってるので、あくまで環境の違いに焦点をあわせてひたすら調査です。で、会社に無理をお願いして2048ビット長の証明書を買ってもらうも、あいかわらずテスト環境では問題が再現できない。ということで、auでの2048ビット問題がどうのこうのではないということでしょう。

発行元に問題があるのかいうのが最後まで拭いきれ無かったんですが、同一発行元できちんと機能する部分が他サイトであったので、そこも問題視しづらい。けど、ひっかかる、みたいな。

テストコードを書いて再現可能な端末でいろいろ試した結果、どうもセッションの内容が直前に設定した値じゃなく、ずいぶん前の値が表示されてる気がする(時間をセッションにいれてたので)。セッション内容が混線してるようにも思えたけど、標準のSessionIDManagerが発行するSessionIDはRNGCryptoServiceProviderを使ったランダム値を使っている(公式なものは見つけられてないですが、Reflectorで確認したので間違いないはず)ので、新規セッションで重複したものが発行されてるとはとても考えにくい。試してみるとわかりますが、RNGCryptoServiceProviderはかなり優秀で現実的な範囲で重複しない値をランダムに生成してくれます。SQLServerをセッションストアに使ってるので、セッションテーブルの中身を見たりしてみたんですが、ちゃんと入ってるように見える。

そこで熊さんが「Application名とか違うんじゃない?」とつぶやきました。でもSessionState設定にそんな項目ないじゃないですか。その時ふとappcmdでIISの設定を見比べてる時に違いがあったことを思い出しました。SiteIDが違った気がする、と。熊さんナイス!

sticky

↑赤線で囲ってるところです(これは自マシン)。

そこからいろいろたどって上記KBにたどり着くわけです。で、セッション用のテーブル内容を見てみると、確かにアプリケーションを特定するための情報にSite IDが含まれてる。これがまた公式な資料ではApplicationPathという表現だったりして、SiteIDに直結しない。しかもたぶんですが、IIS6までと発行ルールが違う上に、今回の対象サーバーで対象サーバ群の中でサイトの生成数が違ったりしてて、まさにマルチテナントの罠。クラウド環境を使う場合にもどういう風に構成されてるか知らないと、痛い目を見ることになりますYO。

セッション内容がへンな感じになるのはつまりこんな感じです。

  • URIセッションとして一度生成されたあと、次回以降のリクエストでは別サーバーにLBで振られ、ASP.NETがセッションIDを取り出した後、DBからセッション情報を取り出す際にSite IDの違うサーバー上での処理なので、新規セッション扱いとなるというパターン
  • その後のリクエストでは、サーバー毎に取り出す(または保存する)セッション情報が異なる結果になり、直前のセッションが取り出せないというパターン

まさに科学では説明できないような現象にしか思えない...。呪いなんじゃないかと。起きている事象だけを見るとサッパリなんですが、ここにつながるであろうヒント的な挙動は何度か発生してたんです。それをまた軽く流してたのがよくなかったというのは、原因がわかったからなのか、個人的な問題なのか。いずれにせよ、もっと早くに問題に着手していればここまでの痛手にはなってなかったような気もします。反省。

ここまで読んで「au関係なくね?」と思った人は勘がいいですね。つまりauだけに存在するこれとは別の問題とセッションの問題の2つが同時に発生していたわけです。auだけに存在する問題に関してはプログラム的な不具合だとコードを変えてもいいんですが、それだとテスト環境でうまく動く説明がつかない。まぁCookie関連の問題なんですけどね。全然食ってくれないau。なのでそこは別の問題としてコードを変えて対応しました。テスト環境では動くけど、何か闇の力が働いてたんでしょう。HTTPSだし。

auのSSLでのCookieの挙動がおかしい - maru.cc@はてな

晴れてすべての問題が解決し、枕を高くして安心して眠れる日々を送れることになったと思いきや、別の問題で結局徹夜。なんでやねん!

2010年7月11日日曜日

GridViewも使ってます

MVCだけじゃないよ。WebFormも使ってるよ。最近GridViewで表示結果が1件だけだったら最初から選択状態にしたいな、なんて思ったわけですが、そんなときってSelectedIndexに行番号セットすれば済むよね。

  <asp:GridView runat="server" ID="gridView" 
    DataSourceID="dataSource" AutoGenerateColumns="false" 
SelectedRowStyle-CssClass="selectedRow" ShowHeader="false" DataKeyNames="Name"> <Columns> <asp:CommandField ShowSelectButton="true" /> <asp:BoundField DataField="Name" /> <asp:BoundField DataField="AkumaNoMi" /> </Columns> </asp:GridView> <asp:ObjectDataSource runat="server" ID="dataSource"
TypeName="WebApplication1.People" SelectMethod="Select">
</asp:ObjectDataSource> <asp:Button runat="server" ID="button" Text="ぼたーん" /> <h2 runat="server" id="subject"></h2>

こんなページでしょうか。

gridview1

コードビハインドはこうですよね。

  public class Person
  {
    public string Name { get; set; }
    public string AkumaNoMi { get; set; }
  }

  public class People
  {
    private static List<Person> _people = new List<Person>
               {
                 new Person {Name = "ルフィー", AkumaNoMi = "ゴムゴム"},
                 new Person {Name = "ニューゲート", AkumaNoMi = "グラグラ"},
               };

    public IEnumerable<Person> Select()
    {
      return _people;
    }
  }

  public partial class _Default : System.Web.UI.Page
  {
    protected void Page_Load(object sender, EventArgs e)
    {
      gridView.SelectedIndexChanged += (s, ev) => 
subject.InnerText = gridView.SelectedIndex + "行目"; } }

ObjectDataSourceを使ってるのはデータバインドの処理とか実際にはコード書かないっていう前提だからです。この状態で、選択ボタンを押すとちゃんと「n行目」って選択した行が表示されましょう。そうでしょう。

gridview2

これって普通なんだけど、GridViewのSelectedIndexにセットするようPage_Loadに書いてみても「n行目」とは表示されませぬ。されませぬ。

    protected void Page_Load(object sender, EventArgs e)
    {
      gridView.SelectedIndexChanged += (s, ev) => 
subject.InnerText = gridView.SelectedIndex + "行目"; if(!IsPostBack) { gridView.SelectedIndex = 1; } }

gridview3


↑ちゃんとニューゲート選択されてるけど「1行目」とは表示されない。なぜでしょうか。SelectedIndexプロパティに値をセットしても、GridViewがSelectedIndexChanging/SelectedIndexChangedイベントをRaiseしてくれないからですね。これをRaiseしてあげればいいんだけどそんなことしてくれないと思うので、そんな時にはGridViewにあるprotectedなOnSelectedIndexChanging/OnSelectedIndexChangedを呼べばいいでしょう。GetMethodで取り出せばprotectedだとしても知ったこっちゃないしね。

 

    private void GridViewSelectedIndex(GridView grid, int index)
    {
      grid.SelectedIndex = index;
      var selectedIndexChanged = typeof(GridView).
GetMethod("OnSelectedIndexChanged",
BindingFlags.NonPublic |
BindingFlags.Instance); if (selectedIndexChanged != null) { selectedIndexChanged.Invoke(grid,
new object[] { new EventArgs() }); } }

これをPage_Loadの”gridView.SelectedIndex = 1;”の部分で呼び出してあげるように書き換えちゃえば、ほら出来た!

gridview4

うぬ。悪くない。でも、これと同じことをクライアントサイドで選択ボタンを押したことにすることでも、実現できますね。例えばこう。

  <script type="text/javascript">
    <% if(!IsPostBack){ %>
    $(function () { 
      __doPostBack("<%=gridView.ClientID.Replace("_","$") %>","Select$1");
    });
    <%} %>
  </script>
  

固定でニューゲートを選択させてます。Select$1の部分。これはちょっと気持ち悪いかも。悪くないとは言いがたい。UpdatePanelに入れておけばそれなりな感じでいいかもしれないけど、普通に1回PostBackされるから悩ましい。

サーバーサイドとクライアントサイドのどちらをチョイスしてもいいかなと思うけど、どうでしょうね。いきなりGridViewの話をしているのはフリで、ここからが本題です。

AjaxControlToolkit(ACT)をがんばって使ってます。WebFormでの簡単Ajax導入に関してACTはとてもいい選択肢ではありますね。お金払わなくていいし、クライアントサイドの知識も要らないし。でも気に入らない部分も多くてウキャ~!ってなる。ACTとはいえ、レンダリングされてしまえばただのHTMLとJavaScriptなわけじゃないですか。気に入らなければ書き換えちゃえばいい。どこを書き換えるかがポイントだと思いますが、今回Accordionを書き換えてみました。ボス、ごめんね!

先程のGridViewのサンプルの続きに書き足していきます。

まずはASPXに以下を追加。これでAccordionが使えます(なんつーかツールボックスからのドラッグアンドドロップのやり方を知ってるともっとラクにかけるんだろうけど、そんなやり方はわすれたので普通に手で書いてます)。

  <asp:ToolkitScriptManager runat="server"></asp:ToolkitScriptManager>
  <asp:Accordion runat="server" ID="accordion"
       HeaderCssClass="acd_header" HeaderSelectedCssClass="acd_selected" 
ContentCssClass="acd_content"> <Panes> <asp:AccordionPane runat="server" ID="pane1"> <Header>pane 1</Header> <Content>パネルのコンテンツ その1 <p>The Ajax Control Toolkit contains a rich set of controls that you can use to build highly responsive and interactive Ajax-enabled Web applications. The Ajax Control Toolkit contains more than 40 controls, including the AutoComplete, CollapsiblePanel, ColorPicker, MaskedEdit, Calendar, Accordion, and Watermark controls. Using the Ajax Control Toolkit, you can build Ajax-enabled ASP.NET Web Forms applications by dragging-and-dropping Toolkit controls from the Visual Studio Toolbox onto a Web Forms page.</p> </Content> </asp:AccordionPane> <asp:AccordionPane runat="server" ID="pane2"> <Header>pane 2</Header> <Content>パネルのコンテンツ その2 <p>The Ajax Control Toolkit contains a rich set of controls that you can use to build highly responsive and interactive Ajax-enabled Web applications. The Ajax Control Toolkit contains more than 40 controls, including the AutoComplete, CollapsiblePanel, ColorPicker, MaskedEdit, Calendar, Accordion, and Watermark controls. Using the Ajax Control Toolkit, you can build Ajax-enabled ASP.NET Web Forms applications by dragging-and-dropping Toolkit controls from the Visual Studio Toolbox onto a Web Forms page.</p> </Content> </asp:AccordionPane> <asp:AccordionPane runat="server" ID="pane3"> <Header>pane 3</Header> <Content>パネルのコンテンツ その3 <p>The Ajax Control Toolkit contains a rich set of controls that you can use to build highly responsive and interactive Ajax-enabled Web applications. The Ajax Control Toolkit contains more than 40 controls, including the AutoComplete, CollapsiblePanel, ColorPicker, MaskedEdit, Calendar, Accordion, and Watermark controls. Using the Ajax Control Toolkit, you can build Ajax-enabled ASP.NET Web Forms applications by dragging-and-dropping Toolkit controls from the Visual Studio Toolbox onto a Web Forms page.</p> </Content> </asp:AccordionPane> <asp:AccordionPane runat="server" ID="pane4"> <Header>pane 4</Header> <Content>パネルのコンテンツ その4 <p>The Ajax Control Toolkit contains a rich set of controls that you can use to build highly responsive and interactive Ajax-enabled Web applications. The Ajax Control Toolkit contains more than 40 controls, including the AutoComplete, CollapsiblePanel, ColorPicker, MaskedEdit, Calendar, Accordion, and Watermark controls. Using the Ajax Control Toolkit, you can build Ajax-enabled ASP.NET Web Forms applications by dragging-and-dropping Toolkit controls from the Visual Studio Toolbox onto a Web Forms page.</p> </Content> </asp:AccordionPane> <asp:AccordionPane runat="server" ID="pane5"> <Header>pane 5</Header> <Content>パネルのコンテンツ その5 <p>The Ajax Control Toolkit contains a rich set of controls that you can use to build highly responsive and interactive Ajax-enabled Web applications. The Ajax Control Toolkit contains more than 40 controls, including the AutoComplete, CollapsiblePanel, ColorPicker, MaskedEdit, Calendar, Accordion, and Watermark controls. Using the Ajax Control Toolkit, you can build Ajax-enabled ASP.NET Web Forms applications by dragging-and-dropping Toolkit controls from the Visual Studio Toolbox onto a Web Forms page.</p> </Content> </asp:AccordionPane> </Panes> </asp:Accordion>

acd1

恐ろしいほど簡単。もちろん、サーバーサイドで動的にPaneを追加するような使い方も多いでしょう。そうすると、Paneが10個や20個にナッチャウことも。そうなるとインターフェースとしてちょっと問題あり。そんなに追加しなきゃAccordionっていいのにとつくづく思う。でも、かなり開発が進んでしまうと、おいそれとインターフェースを変更するのはしんどい作業になります。変更箇所多すぎるし。そんなこんなで、クライアントサイドで何とかしてしまいます。

acd2acd3

完成形は↑これです。何をしてるのかというと、AccordionのHeaderをSelect(DropDown)にして、選択したら該当するPaneの中身を表示するという動きです。ヒマがあったら動かしてみてね。

まずは、AccordionがレンダリングするHTMLを確認。

<div>
  <input type=”hidden” value=”選択してるPaneのIndex”/>
  <div class=”header class name”>header 1</div>
  <div class=”content class name”>content 1</div>
  <div class=”header class name”>header 2</div>
  <div class=”content class name”>content 2</div>
</div>

こんな構造ですね。ヘッダとコンテンツが、連続してるdiv要素をクラス名で判別というのがちょっと気に入らないですが、今回そこはどうでもいいです。最初にこういう構造でレンダリングされるというのがわかりさえすればいいです。

続いて、どういうスクリプトが動いてるのかを確認してみると、ページ内では↓これだけですね。

<script type="text/javascript"> 
//<![CDATA[
(function() {var fn = function() {
$get("ctl00_MainContent_ToolkitScriptManager1_HiddenField").value = '';
Sys.Application.remove_init(fn);};Sys.Application.add_init(fn);})();


Sys.Application.add_init(function() { $create(Sys.Extended.UI.AccordionBehavior,
{"ClientStateFieldID":"ctl00_MainContent_accordion_AccordionExtender_ClientState",
"HeaderCssClass":"acd_header",
"HeaderSelectedCssClass":
"acd_selected","id":"ctl00_MainContent_accordion_AccordionExtender"},
null, null, $get("ctl00_MainContent_accordion")); }); //]]> </script>

Sys.Application.add_initのタイミングでAccordionBehaviorオブジェクト(クラスのような作りかな?)を渡してますね。そもそもSystem.Extendedが何者なのかしらないですけど、ソースが公開されてるので確認してみたところ、各種Behaviorオブジェクトのprototype.initializeでいろいろ初期化処理を書いてました。

ということは、そこを壊せば、Accordionの初期化処理は走らなくなるということなので、add_initで追加したものが実行される前に、AccodionBehavior.prototype.initializeを壊してみました。runat="server"なform要素の閉じタグ直前に以下のコードを書き足します。

  <script type="text/javascript">
    if (Sys.Extended !== undefined && 
Sys.Extended.UI.AccordionBehavior !== undefined) { Sys.Extended.UI.AccordionBehavior.prototype.initialize = function () { }; } </script>

やりすぎな感じもしますが、まぁいいでしょう。

続いてドロップダウンを表示する枠と、Paneの中身を表示する枠を用意します。Accordionタグの上にでもおいておきます。

  <div id="dropdown"></div>
  <div id="viewer"></div>

最後に、Accordionそのものが表示されてしまわないように、Accordionを非表示するためのCSSを定義します。Visibleを指定しちゃうとホントになくなっちゃうからCSSだけで消しちゃうのがいいです。

  <asp:Accordion runat="server" ID="accordion"
       CssClass="accordion_hack" HeaderCssClass="acd_header" HeaderSelectedCssClass="acd_selected" 
       ContentCssClass="acd_content">

太字のところで、クラス名指定して、そのクラスはdisplay:none;だけを適用してます。

あとはJavaScriptでDropDownを生成するのと選択肢を変えたときに表示を切り替えるようなコードをゴリゴリ書けば完成。

<script type="text/javascript">
  $(function () {
    var hack = function () {
      var accordion = $(".accordion_hack:first");
      var viewer = $("#viewer");

      var dropdown = $("<select />");
      var currentId = accordion.find(".acd_selected")
.next()
.attr("id"); var prevId = currentId; accordion.find(".acd_content").each(function (idx, e) { $(dropdown).append( $("<option />").text($(e).prev().text()) .val($(e).attr("id"))); }); $(dropdown).appendTo("#dropdown"); $(dropdown).change(function (e) { prevId = currentId; currentId = $(this).val(); viewer.fadeOut("fast",function () { if (currentId != prevId) { $("#" + prevId).appendTo(accordion); } $("#" + currentId).show().appendTo(viewer); }).fadeIn("fast"); // accordion.find("input[type='hidden']:first")
.val($(this).find("[value=" + currentId + "]").index()); }); $(dropdown).val(currentId).trigger("change"); }; hack(); }); </script>

わざわざhack変数にいれてるのは、特に意味ないです。テスト目的なので気にしないでね!長々とMVCとは関係ないことを書きましたが、要はJavaScriptとHTML、DOM操作、CSSを理解してると、ASP.NETもいろいろカスタムなレンダリングを気軽に実装できるよっていう話なのと、ACTをハックすることでちょっと勝てた気になれたので、調子にのってエントリしてみました。

今回の一式もダウンロード出来るようにしておきます。興味があればどうぞ。

だた、注意点があります。VS2010でASP.NET 4をターゲットにして作ってるのに、ACTは4.0じゃなくて3.5向けのものを使ってます。理由はACT4のAccordionPaneがMasterPageを使ってる時に、id属性を正しく吐き出してくれないからです。3.5だと問題ないので、4.0でなんかおかしくなったんじゃないかな。気をつけてね。

2009年9月12日土曜日

runAllManagedModulesForAllRequests

VS2008での開発中のWebサーバーはWebDev.WebServerで、これってsystem.webServerセクションを処理しないで、IIS6と同じ動きになるじゃないっすか。

ドはまった。いきなり全然動かない状況が発生してテンヤワンヤ。そもそもIIS6にデプロイするものを作ってて、でもちょっとテストしたくなってIIS7にデプロイしたらイエロースクリーン。サッパリ原因が分からず泣きそうになった。出てきたエラーは↓これ。

セッション状態は、構成ファイルまたは Page ディレクティブで enableSessionState が true に設定されているときのみ使用できます。System.Web.SessionStateModule またはカスタムセッション状態モジュールがアプリケーション構成の <configuration>\<system.web>\<httpModules> セクションに含まれていることも確認してください。
説明: 現在の Web 要求を実行中に、ハンドルされていない例外が発生しました。エラーに関する詳細および例外の発生場所については、スタック トレースを参照してください。

例外の詳細: System.Web.HttpException: セッション状態は、構成ファイルまたは Page ディレクティブで enableSessionState が true に設定されているときのみ使用できます。System.Web.SessionStateModule またはカスタムセッション状態モジュールがアプリケーション構成の <configuration>\<system.web>\<httpModules> セクションに含まれていることも確認してください。

全然、解決出来そうなtipsが見つからなくて、将来の自分のためにここにコピペしとく。なんでセッションがいきなり動作しないんじゃ!と、お悩みなら、とにかくrunAllManagedModulesForAllRequests属性を確認。IIS7って統合パイプラインだったりするのが魅力なんだけど、こんなことに時間をつぶされるとは...。

    <system.webServer>
      <validation validateIntegratedModeConfiguration="false"/>
      <modules runAllManagedModulesForAllRequests="true">
        <remove name="ScriptModule" />
        <remove name="UrlRoutingModule" />

SessionStateModuleがマネージモジュールなおかげでこの属性を外すと、EnableSessionStateを設定しろと全然違うメッセージになって泣ける。

今後主流になるIIS7.5(Windows 7だけど)の場合は、英語っぽいメッセージになったけど、これまた検索で見つかるようにコピペしとこう。

The SessionStateTempDataProvider requires SessionState to be enabled.
説明: 現在の Web 要求を実行中に、ハンドルされていない例外が発生しました。エラーに関する詳細および例外の発生場所については、スタック トレースを参照してください。

例外の詳細: System.InvalidOperationException: The SessionStateTempDataProvider requires SessionState to be enabled.

そもそも、この属性をいついじってしまったのかさえ覚えてない...。普通にプロジェクト作成したときには指定されてるから自分で消したのは間違いないんだけど、IIS7にデプロイするまで問題が発覚しないから、要注意。

全然関係無いけどBubbleShareにアップしてるアルバムをダウンロードしようとしても0バイトのZIPファイルになってて全然ダウンロード出来ない。早くダウンロードしないとそろそろ閉鎖しちゃう。あわあわ。

2009年4月1日水曜日

EC2のWindows上で日本語Webアプリケーションを動かす

ここ最近、ずっとAmazon AWSでの環境構築をしてて、運用環境の変更を行ってました。クラシックASPや、ASP.NET 1.1、2.0で動かしているものをEC2に移行してしまおうという魂胆です。ハードウェアも古くなりすぎてて、にっちもさっちも行かない感じだったのもあり、いっそのことクラウドへ。Azureにいければ一番良かったけど、用途も合わないし、そもそもがまだ無理だしね。

Amazon EC2でWindowsサーバーを動かすだけなら、前に一度チャレンジしてるし今ならいろんな所でエントリもされてるから検索すれば沢山見つかると思います。もちろん日本語で。実際の運用環境を作ってみるにあたりデプロイ含めて少し。自分が忘れないためのメモエントリです。作業はElasticfoxとS3 Organizer、AWS Management Consoleですべて完結。自動化するなら"Amazon Web Services Developer Community : C# Library for Amazon EC2"を使ってコード書いて対応。今回は2台しかインスタンス立ち上げないので使わないです。

まずはどのインスタンスを使うか。x86(32ビット)かx64(64ビット)が大きな分かれ目。x86ならStandard Smallか、High-CPU Mediumの2択。x64ならStandard Large/Extra Largeか、High-CPU Extra Largeの3択。それぞれのVMスペックは"Amazon EC2 Instance Types"に全部書いてるの詳細はそちらで確認。それぞれのインスタンスにSQL Server 2005か同Expressが入ってるイメージがあるけど、ぶっちゃけStandard Smallでは使い物にならない。SQL Serverを使うならx64しか選べないけど、同Expressを使うならx86でもいいので、High-CPU Mediumが一番安く現実的な選択肢。そうなるとSQLServer AgentがないのでWindowsのタスク機能を使ってバックアップとることになるので、ちょっと作業が増える。それよりなによりライセンス的に大丈夫なのかがよく分からない。ウェブアプリケーションのストレージとしてExpressっていいんでしたっけ?Datacenter Editionだとなんかライセンス違うのかな。ちなみにAuthentication Serviceは今回使ってないのでよく分かりません。インスタンスの料金については"Amazon Elastic Compute Cloud (EC2) Running Microsoft Windows Server and SQL Server"ここで。なにげにSQLServerを使いたいなら最低$1.10必要だから1日$26.4が最低価格になるんだよね。1アプリケーション1DBサーバーなんて贅沢な使い方をしようとすると、それだけで月に$800くらいかかっちゃうから気をつけないとね。

AMIも最初は名前見ただけじゃ何がインストールされるのかよく分からなかったけど、何度もやってると普通の名前に思えてくるから恐ろしい。

ec2_1

OwnerがAmazonになってるWindows AMIはこれで全部。Authenticationの有無(WinAuthと付いてるか付いてないか)の組み合わせが5種類なので全10種類のみ。”i386”が32ビットで”x84-64”が64ビット。SQLServer入り(SqlSvrStd)が2種類で、同Express(SqlSvrExp)入りが4種類。どっちも入って無いのが4種類。
...分かりやすいじゃないか。

これらとインスタンスタイプの組み合わせだけなので、慣れればなんてことないね。

で、日本語化。当然、すべて英語版なのでそのままだと日本語が表示されない(インスタンス上で)ばかりか、各種書式フォーマット(ロケール)も変えないと、アプリケーションで変な表示になってしまう。内部的にも文字列から復元出来なかったりするところが出るかもしてないし。

と、言うわけで"Amazon Web Services Developer Community : Configuring Windows Components on Amazon EC2"に書いてるスナップショットIDをもとにしたVolumeを作成してドライブを割り当てておいてから、コントロールパネルの"Regional and Language Options"で設定を変えること。

ec2_2

ec2_3

ec2_4

それと、Time Zoneも変えた方がいいかも。

とりあえずこれで、サーバー上で日本語が使えるようになるし、書式化フォーマットも見慣れた物になるけど、ASP.NETアプリケーションでの書式が変。ここの設定が反映されてない。おかしいな~、と思って調べて見たら"globalization 要素 (ASP.NET 設定スキーマ)"でカルチャー設定するのが正しい対応でした。てへ。

DBも言語や照合順序の設定が日本語向けのものじゃないから、その辺の設定も忘れずに。だいたいこんな感じでOKなんじゃないでしょうか(ちょいちょいコントロールパネルの設定が英語に戻ってるのは、なにがきっかけなんだろう)。

dotnetConf2015 Japan

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