2010年12月31日金曜日

Feedのアドレス

ちょっとあまりにも配信に時差がでちゃうので、Feedのアドレス変えます。すません。

Blogpost:無聊を託つ

http://feeds.feedburner.com/blogspot/jNRCtw

Tumblr:等閑に付さないように

http://feeds.feedburner.com/tumblr/ijTN

旧FeedのアドレスはTumblrの方に向けます。ホントすません。

sorry009

2010年12月30日木曜日

問題だと思っていることはホントに問題なの?

すっかり忘れてました。新しく開発者を採用するにあたり、会社の方針に合わせてコードを書いてもらいました。で、採用担当としてこんな問題を考えました。

問題内容

2つの文字列を与えられた時、それぞれの文字列の一致率を0~1の間で算出する関数を作成せよ。
※一致率の定義の仕様も含めて考えてください。

いろいろ考え方や思うところはあると思いますが、今回に関しては問題を解く過程をとても重視しました。どういうふうに仕様を決めるのかも含め、です。内容からして勝手に仕様を決めていいように思われるかもしれないですが、そこの解釈も重視です。与えられた情報の範囲内でどこまで飛べるのか、みたいな。

解答例

public static decimal Rate(string value1, string value2)
{
    if (string.IsNullOrEmpty(value1) || string.IsNullOrEmpty(value2))
        return 0m;

    if (string.IsNullOrEmpty(value1.Trim()) || string.IsNullOrEmpty(value2.Trim()))
        return 0m;

    if (string.Compare(value1, value2, StringComparison.CurrentCulture) == 0)
        return 1m;

    var longer2nd = value1.Length < value2.Length;
    var val1 = longer2nd ? value1 : value2; // 短い文字列
    var val2 = longer2nd ? value2 : value1; // 長い文字列
    int match = 0, p1 = 0, p2 = 0, p2Index = 0;

    while (p1 < val1.Length && p2 < val2.Length)
    {
        if (val1[p1] == val2[p2Index])
        {
            match += 2;
            p1++;
            p2 = p2Index++;
        }
        else
        {
            p2Index++;
            if (p2Index >= val2.Length)
            {
                p1++;
                p2Index = p2;
            }
        }
        
        if (val2.Length - 1 < p2Index)    // 進みすぎたときには引き戻す
            p2Index = p2;
    }

    return match / (decimal)(value1.Length + value2.Length);
}

最初のほうはエラーチェックだから、今回別に無くてもよかったです。

あれっすよ、こんなの10分やそこらでコード化するもんじゃないかもね。でも、考え方というか解き方というか、問題はどこでどういう回答をビジネスオーナーは望んでいるのか、ですよね。

問題を考えたものとしては、この問題で先頭一致の割合だけでとくのが一番簡単なのは分かってて、それだけじゃない解き方を答えて欲しいな~、と思ってました。

“どのくらい一致しているか”

の定義は様々だけど「全体文字数に占める一致文字数の割合」が手頃な回答ですよね。んじゃ、一致文字数というのはどういう意味だと解釈するのが面白いか。文字列を前半・中央・後半の3つの集合として考えて、それぞれの文字列の比較開始位置と一致文字数を数えていけば、それらしい数値を出すこと方法として面白いんじゃないかと思うんだけど、どうかな。

前半 中央 後半 文字列1 文字列2

AABBCC AABBCC

AABBCC AABBzz
※AABBも同じ

zzBBCC yyBBCC
※BBCCも同じ

AAzzCC AAyyCC
※AACCも同じ

  zzAAyy xxAAww

※前半だけ、後半だけは前半+中央と中央+後半と同じ。

こんな感じのパターン。ちなみにこのパターンを答えた人がひとりだけいましたよ。なんで同じ答えにたどり着いたのかというと、そういう定義をオーナーが望んでると聞き出したからです。そりゃ同じになるわね。

じゃー、このコードがホントにそれっぽく一致率がでるのか確認するテストコード。

[TestMethod]
public void 空文字渡し()
{
	var rate = MatchRate.Rate(null, null);
	Assert.AreEqual(0, rate);

	rate = MatchRate.Rate("", null);
	Assert.AreEqual(0, rate);

	rate = MatchRate.Rate(null, "");
	Assert.AreEqual(0, rate);

	rate = MatchRate.Rate("", "");
	Assert.AreEqual(0, rate);

	rate = MatchRate.Rate("", "AA");
	Assert.AreEqual(0, rate);

	rate = MatchRate.Rate("AA", "");
	Assert.AreEqual(0, rate);
}

[TestMethod]
public void 空白渡し()
{
	var rate = MatchRate.Rate(" ", "");
	Assert.AreEqual(0, rate);

	rate = MatchRate.Rate("", "  ");
	Assert.AreEqual(0, rate);

	rate = MatchRate.Rate("AA", " ");
	Assert.AreEqual(0, rate);
}

[TestMethod]
public void 完全一致()
{
	var rate = MatchRate.Rate("AA", "AA");
	Assert.AreEqual(1m, rate);

	rate = MatchRate.Rate("aa", "aa");
	Assert.AreEqual(1m, rate);
}

[TestMethod]
public void 前方一致で66パーセント()
{
	var rate = MatchRate.Rate("AA", "AABB");
	Assert.AreEqual(2m / 3m, rate);
}

[TestMethod]
    public void 後方一致で66パーセント()
{
	var rate = MatchRate.Rate("AA", "BBAA");
	Assert.AreEqual(2m / 3m, rate);
}

[TestMethod]
public void 中央一致で66パーセント()
{
	var rate = MatchRate.Rate("AA", "BAAB");
	Assert.AreEqual(2m / 3m, rate);
}

[TestMethod]
public void 前後半一致で66パーセント()
{
	var rate = MatchRate.Rate("AA", "ABBA");
	Assert.AreEqual(2m / 3m, rate);
}

[TestMethod]
public void 前後半一致で75パーセント()
{
  var rate = MatchRate.Rate("ABCDE", "ADE");
  Assert.AreEqual(6m / 8m, rate);
}

だいたいいい感じなんじゃないっすかね。直感的にそんな感じ。セマンティックとか関係なく、ですから。問題はここまでで終了。

だたひとり答えにたどり着いたクマさんに拍手。

ここからさらに考え方を進めてみましょう。もし文字列が文章だとしたら。文章だと”の”とか”を”とかが入ってくるから、普通に一致させるだけだと、それっぽくならないですね。なので、このアルゴリズムを再帰に改造して、前半を無視したほうが一致率が上がるなら無視するというふうに改造してみます。何でかというと、このアルゴリズムだと一致率は上昇して頂点になったあと下降するっていう山型になるはずだからです。現在の位置と次の位置との比較で一致率が上昇してるなら、現在の位置は無視したほうがそれっぽい数値に近づくってことです。そうするとコピペ文章なんじゃん?っていうものに関しての判定が”それっぽく”できます。あくまで”それっぽく”ですよ。

public static decimal Rate(string value1, string value2)
{
    return Rate(value1, value2, null);
}

public static Func<string, int, string, int, bool> RecursiveCommand = (val1, idx1, val2, idx2) =>
{
    var p1Rate = Rate(val1.Substring(idx1), val2.Substring(idx2), null);
    var p1IncRate = Rate(val1.Substring(idx1 + 1), val2.Substring(idx2), null);
    return p1Rate < p1IncRate;
};

public static decimal Rate(string value1, string value2, Func<string, int, string, int, bool> command)
{
    if (string.IsNullOrEmpty(value1) || string.IsNullOrEmpty(value2))
        return 0m;

    if (string.IsNullOrEmpty(value1.Trim()) || string.IsNullOrEmpty(value2.Trim()))
        return 0m;

    if (string.Compare(value1, value2, StringComparison.CurrentCulture) == 0)
        return 1m;

    var longer2nd = value1.Length < value2.Length;
    var val1 = longer2nd ? value1 : value2; // 短い文字列
    var val2 = longer2nd ? value2 : value1; // 長い文字列
    int match = 0, p1 = 0, p2 = 0, p2Index = 0;

    while (p1 < val1.Length && p2 < val2.Length)
    {
        // p1とp1+1で比較してp1+1のほうがRateがよければp1はスキップする
        if (command != null)
        {
            p1 += command(val1, p1, val2, p2Index) ? 1 : 0;
        }

        if (val1[p1] == val2[p2Index])
        {
            match += 2;
            p1++;
            p2 = p2Index++;
        }
        else
        {
            p2Index++;
            if (p2Index >= val2.Length)
            {
                p1++;
                p2Index = p2;
            }
        }

        if (val2.Length - 1 < p2Index)    // 進みすぎたときには引き戻す
            p2Index = p2;
    }

    return match / (decimal)(value1.Length + value2.Length);
}

で、最初のテストがパスするのを確認したうえで、以下のテスト。

[TestMethod]
public void 再帰のほうがそれっぽくなる()
{
  var rate = MatchRate.Rate(
      "ABCDEF",
      "AECDEF"
  );
  var rateR = MatchRate.Rate(
      "ABCDEF",
      "AECDEF",
      MatchRate.RecursiveCommand
  );
  Console.WriteLine("Rate={0} < RateR={1}", rate, rateR);
  Assert.IsTrue(rate < rateR);
}

[TestMethod]
public void コピペしてちょっと手直しした文章()
{
  var rate = MatchRate.Rate(
		"衆院は31日の夜、会議を開き、日本郵政3社への再編などを柱とする郵政改革法案を賛成多数で可決し、参院に送付した。与党は参院でも短時間で審議し、6月16日までの会期内成立を目指す構え。自民、公明、みんな、たちあがれ日本の野党4党は1日、与党の意向を受け、本会議開催を強行した横路孝弘衆院議長に対する不信任決議案を提出する。",
		"衆院は31日深夜、本会議を開き、日本郵政グループの3社への再編などを柱とする郵政改革法案を民主、国民新党などの賛成多数で可決し、参院に送付した。与党は参院でも短時間で審議を終え、6月16日までの会期内成立を目指す構え。自民、公明、みんな、たちあがれ日本の野党4党は1日、与党の意向を受け、本会議開催を強行した横路孝弘衆院議長に対する不信任決議案を提出する。",
            MatchRate.RecursiveCommand
	);
	Console.WriteLine(rate);
}

ソースアップしておきまーす。何かの役に立つコードではないけどね!

2010年12月29日水曜日

Razor Templating Engine

Razor Templating Engine

CodePlexで新しいのが出てたよ~。面白いね~。テンプレートエンジンっていろいろ用途があるけど需要はどーなんだろね。まぁ、いいや。Razor記法でWeb以外にも使えるって楽しいっすよね。だっていろいろ覚えなくて済むし。ちなみにASP.NETの<%...%>とか<%$…%>とか<%#…%>なんかの記法も用途特化したテンプレートエンジンが解釈してクラスファイルを生成してますよね。

んでね、Razorは純粋にテンプレートエンジンって言ってるくらいだからWebのコンテキストや実行Hostに依存しないわけですね。つまりConsoleアプリでも使えるってことです。もちろんHtmlHelperとかWeb関連のアセンブリに依存するものはあるとしても、それ自体はRazorの機能じゃなくてHelperの機能。TemplateBaseクラスが生成されるクラスの親クラスになるって言う仕組みなので、親クラスにいろいろWeb関連の機能を保持するっていう、Razorとは直接関係なっしんぐ。

サンプル見てみるとこんな感じですね。

rte1

string template = "Hello @Model.Name! Welcome to Razor!";
string result = Razor.Parse(template, new { Name = "World" });

Console.WriteLine(result);
Console.ReadLine();

簡単ですね~。お気楽です。

同じくサンプルのHtmlHelperを使うパターン。

rte2

Razor.SetTemplateBaseType(typeof(HtmlTemplateBase<>));
string template =
@"<html>
    <head>
    <title>Hello @Model.Name</title>
    </head>
    <body>
    Email: @Html.TextBoxFor(m => m.Email)
    </body>
</html>";

var model = new PageModel { Name = "World", Email = "someone@somewhere.com" };
string result = Razor.Parse(template, model);

Console.WriteLine(result);
Console.ReadLine();

はい、これも簡単ですね。イメージ通りです。

PageModelは自分で作ってね。あと、RazorEngine.Coreだけじゃ足りなくて、RazorEngine.Templatesアセンブリも必要なので、ソースのダウンロードはここからどーぞ。

これらは全て実行時にクラスファイル(特に指定が無い限りc#)をオンメモリで生成してコンパイルします。そのタイミングでAppDomain内からはクラスのインスタンスを生成できるようになるって言う流れはASP.NETのパイプラインそのもの。

試しに最初のサンプル(Hello World!のほう)をステップ実行していくと、どういうクラス名で生成されるのかというのを追いかけていくと...。

rte3

雑ですね。Guid.NewGuid()から数字とハイフンを除外したのがクラス名。それにNamespaceをくっつけてます。で確認。

rte4

ちゃんといます。で、AppDomainの仕様を見てみるとこんなことがかかれてます。

既定のアプリケーション ドメインにロードされたアセンブリは、プロセスの実行中にメモリからアンロードすることはできません。 しかし、アプリケーション ドメインをもう 1 つ開いてアセンブリをロードおよび実行すると、そのアプリケーション ドメインがアンロードされたときに、アセンブリもアンロードされます。 このテクニックを使用すると、大きな DLL を使用する場合のある、長時間実行されるプロセスのワーキング セットを最小限に抑えることができます。

AppDomain クラス (System)

ん~、どーだろ。どうなるんだろね。

rte6

rte5

めちゃめちゃ分かりにくいですね。2個目のサンプル(HtmlHelper使うほう)を使ってRazor.Parseを繰り返してる途中です。

Razor.SetTemplateBaseType(typeof(HtmlTemplateBase<>));
var model = new PageModel { Name = "World", Email = "someone@somewhere.com" };

for (var i = 0; i < (singleExec ? 1 : Repeat); i++)
{
    var sw = new Stopwatch();
    sw.Start();
    var result = Razor.Parse(Template, model);
    sw.Stop();

    Console.WriteLine(
        "{0} - {1} {2} ms {3:0,###} bytes",
        result.GetHashCode(),
        i,
        sw.ElapsedMilliseconds,
        Process.GetCurrentProcess().PrivateMemorySize64);
}

少しずつメモリ使用量と動作時間が長くなっていきます。最初は200msとかで処理。すごく単純で1種類だけの生成なのにね。ちなみに一度コンパイルしたものをその後何度も使用する場合は超早い。

rte8

rte7

10000回実行してもすぐ終了。

これってつまり、ASP.NETでも同じでaspxを何度も書き換えてると処理時間はどんどん増加。いつか破綻?でもリサイクルも走るし、そもそも運用環境でそんなことしないから問題にはならないか。

AppDomainをアンロードしない限りアセンブリもアンロードされないってことはつまり常時動いてるようなシステムで頻繁にコンパイルを繰り返すのはよろしくないね!ってなりましょう。それをひっくり返すのがAppDomainを別に作ってそっちで動かす方法。

リファレンスにもちゃんとかかれてますね。ボスもタイムリーに(MEFだけど)、そんなことをブログに書いてました。

MEFでディレクトリカタログを追いかける(C# Advent Calender jp:2010 12/02) « kazuk は null に触れてしまった

なのでAppDomainを作って実行するように試してみた。

rte9

けど無理!遅すぎ!

var sw = new Stopwatch();
sw.Start();

AppDomainSetup ads = new AppDomainSetup();
ads.ApplicationBase = Environment.CurrentDirectory;
ads.DisallowBindingRedirects = false;
ads.DisallowCodeDownload = true;
ads.ConfigurationFile = AppDomain.CurrentDomain.SetupInformation.ConfigurationFile;

AppDomain executeAppDomain = AppDomain.CreateDomain("AD#" + i, null, ads);
var razor = executeAppDomain.CreateInstanceAndUnwrap(
    Assembly.GetEntryAssembly().FullName,
    typeof(RazorTemplateTest).FullName
) as RazorTemplateTest;

razor.ExecuteParse(i);

AppDomain.Unload(executeAppDomain);

sw.Stop();

Console.WriteLine(" {0} : {1} ms",
    AppDomain.CurrentDomain.FriendlyName,
    sw.ElapsedMilliseconds);

でも、まぁ、思ったような結果が出なかった残念な調査でした。ホントはもっとメモリ開放されないことを想定してたんだけどな~。

オチがない...。

using System;
using System.Diagnostics;
using System.Reflection;
using RazorEngine;
using RazorEngine.Templating;

namespace ConsoleApplication1
{
    public class PageModel
    {
        public string Name { get; set; }
        public string Email { get; set; }
    }

    public class RazorTemplateTest : MarshalByRefObject
    {
        private static int Repeat = 10000;
        static string Template =
@"<html>
    <head>
        <title>Hello @Model.Name</title>
    </head>
    <body>
        Email: @Html.TextBoxFor(m => m.Email)
    </body>
</html>";

        public void ExecuteParse()
        {
            ExecuteParse(-1);
        }

        public void ExecuteParse(int counter)
        {
            Razor.SetTemplateBaseType(typeof(HtmlTemplateBase<>));
            var model = new PageModel { Name = "World", Email = "someone@somewhere.com" };

            for (var i = 0; i < (counter!=-1 ? 1 : Repeat); i++)
            {
                var sw = new Stopwatch();
                sw.Start();
                var result = Razor.Parse(Template, model);
                sw.Stop();

                Console.WriteLine(
                    "{0} - {1} {2} ms {3:0,###} bytes",
                    AppDomain.CurrentDomain.FriendlyName,
                    counter != -1 ? counter : i,
                    sw.ElapsedMilliseconds,
                    Process.GetCurrentProcess().PrivateMemorySize64);
            }
        }

        public void ExecuteCompile()
        {
            Razor.SetTemplateBaseType(typeof(HtmlTemplateBase<>));
            var model = new PageModel { Name = "World", Email = "someone@somewhere.com" };

            Razor.Compile(Template, typeof(PageModel), "test");

            for (var i = 0; i < Repeat; i++)
            {
                var sw = new Stopwatch();
                sw.Start();

                var result = Razor.Run(model, "test");

                sw.Stop();

                Console.WriteLine(
                    "{0} - {1} {2} ms {3:0,###} bytes",
                    result.GetHashCode(),
                    i,
                    sw.ElapsedMilliseconds,
                    Process.GetCurrentProcess().PrivateMemorySize64);
            }
        }

        public void ExecuteAnotherAppDomain()
        {
            for (var i = 0; i < Repeat; i++)
            {
                var sw = new Stopwatch();
                sw.Start();

                AppDomainSetup ads = new AppDomainSetup();
                ads.ApplicationBase = Environment.CurrentDirectory;
                ads.DisallowBindingRedirects = false;
                ads.DisallowCodeDownload = true;
                ads.ConfigurationFile = AppDomain.CurrentDomain.SetupInformation.ConfigurationFile;

                AppDomain executeAppDomain = AppDomain.CreateDomain("AD#" + i, null, ads);
                var razor = executeAppDomain.CreateInstanceAndUnwrap(
                    Assembly.GetEntryAssembly().FullName,
                    typeof(RazorTemplateTest).FullName
                ) as RazorTemplateTest;

                razor.ExecuteParse(i);
                
                AppDomain.Unload(executeAppDomain);

                sw.Stop();

                Console.WriteLine(" {0} : {1} ms",
                    AppDomain.CurrentDomain.FriendlyName,
                    sw.ElapsedMilliseconds);
            }
        }
    }

    class Program
    {

        static void Main(string[] args)
        {
            //new RazorTemplateTest().ExecuteParse();
            //new RazorTemplateTest().ExecuteCompile();
            new RazorTemplateTest().ExecuteAnotherAppDomain();

            Console.ReadLine();
        }
    }
}

こんなコードで~す。

2010年12月23日木曜日

サウザンダー

so

やったよ!長かった。とても長かった。458日。ちょっとづつ。

so2

ジリジリと。やっとサウザンダーの仲間入り。MVCだけでもなんとかなるもんです!

2010年12月21日火曜日

Norwegian Wood

NoSQLの成功は1:10問題にかかっている:Kenn's Clairvoyance - CNET Japan

面白い記事ですね。

NoSQLをRDBの代わりに使うと、どういう恐ろしいことが起こるか。PARTAKEの作者が語る - Publickey

NoSQLでは↑こんな話もありますが、ちゃんとDomain Specific Database(という言葉があってるのかどうかは知らないけど)であることを認識して、使い方を間違えなければいいじゃんよ、ってなもんです。恐ろしいのは何でもRDBMS、いやいや何でもNoSQLという発想やそのメンタリティ。

言いたいことはそこじゃなくてですね、最初のリンクに書かれている

「コンピュータサイエンスに難問は二つしかない。それは概念に名前をつけることと、キャッシュの失効である」

“There are only two hard things in Computer Science: cache invalidation and naming things”

の部分です。「概念に名前をつける」これはコンピュータサイエンスに限らず重要なことじゃないでしょうか。

先日はじめて知った言葉に「絶対領域」というものがありました。ATフィールドの和訳というそれっぽいこともWikipediaには書かれていましたが、そんなことにココロオドルかっちゅう話ですよ。詳細はグーグルさんに教えてもらってくださいね。この胸の高鳴りはなんなのか?それが定義され名前がある、その事実に衝撃を覚えたものです。

そして「森ガール」という謎の単語。これに関しては相変わらず意味がよくわかんないんだけど、「森にいそうな格好」とはようするに森に住む妖精みたいなもんだろーか?

森ガールといえば最近そんなよーな名前の映画が公開されてた気がする。たぶんこんな話なんだろうな。

目が覚めるとそこは不思議の森。なんでこんな場所に寝てたんだろう?確かこたつでみかんを食べながらテレビを見ていはず。う~ん、記憶が一部飛んでいる。とりあえず、自分がいまどこにいるのか把握しておいたほうがいいな。そう思っておもむろに立ち上がり散策を開始。ところで今何時だ?確かテレビでサングラスのおじさんがウキウキウォッチングがどーのこーの言ってたからお昼過ぎのはずなんだけど。

途中で出会った森の中に住む妖精「森ガール」との珍道中。あれはオーロラか!?まさかここは日本じゃない?外国?そして最初の被害者が...。スリルとサスペンスのドタバタ大活劇。じっちゃんの名にかけて!

ノルウェイの森

森ガールの謎を解くべく観に行ったんだけど全然こんな話じゃなくて愕然とした。

2010年12月18日土曜日

Razor Helper的なForEachでテンプレート

今年も終わりですね。タイトル意味分かんないですね。

来週から夏休み&年末年始休暇に入るので、今年の仕事終了です。てへ。ブログサイトがあるのをすっかり忘れるくらい追い込まれる毎日。よくもまぁ、こんな状態が続くもんだと感心するばかりです。え?きれてねーよ。

そんなことはいいんですけどね、ちょっとここ見てみてくださいよ。

Razor, Nested Layouts and Redefined Sections - Marcin On ASP.NET - Site Home - MSDN Blogs

ナイスエントリーにも程があるデスヨ。

Layout用のビューを直接指している、子供ビューのセクションはみえるけど、多段Layoutになってて孫ビューのセクションはスコープの範囲外でエラーになっちゃうっていうエントリー。

実際に試してみたらたしかに動かない。Section not defined。そ

foreach

りゃそうだ。それを克服するために中間LayoutでRedefineSectionで孫のセクションを生成(WebPageBase.DefineSection)してしまおうよっていう強引さには度肝を抜かれますがなによりそこじゃないんです。

その部分より何よりセクション初期値をヘルパーに渡す部分ですよ!

Func<object, HelperResult> defaultContent

これを知っておくと何が出来るかというと...

@{
    Layout = "~/Views/Shared/_Layout.cshtml";
}
@helper ForEach(IEnumerable<int> values, Func<object, HelperResult> template){
    var partialHtml = new System.Text.StringBuilder();
    foreach(var value in values){
        partialHtml.Append( template(value));
    }

    Write(MvcHtmlString.Create(partialHtml.ToString()));
}
<div>
<ul>
@ForEach(Enumerable.Range(1,5),
    @<li>@item</li>
)
</ul>
</div>

↑こんなコード書けるってことですよ。ヘルパーにRazor構文のテンプレートを渡してるんですよ。いいでしょ?ん~、もんどりうつ。

追記:もっと簡単でよかった。

@helper ForEach(IEnumerable<int> items, Func<object, HelperResult> template){
    foreach(var item in items){
        Write(template(item));
    }
}

@helperだとGenericメソッドに出来なさそうなので、もうちょっと調べてみたら、普通に@functionsで実装するサンプルをAndrew君が...。

VibrantCode - Inside Razor - Part 3 – Templates

この場合は↓こうね。

@{
    Layout = "~/Views/Shared/_Layout.cshtml";
}
@functions{
    static IHtmlString ForEachGeneric<T>(IEnumerable<T> items, Func<T, HelperResult> template)
    {
        var sb = new System.Text.StringBuilder();
        foreach(var item in items){
            sb.Append(template(item));
        }
        
        return MvcHtmlString.Create(sb.ToString());
    }
}

<div>
<ul>
@(ForEachGeneric<int>(Enumerable.Range(1,5),
    @<li>@item</li>
))
</ul>
</div>

 

foreach2

小っちゃ!
クリックして拡大してみてね。

Razorのコンパイル結果を見てみると分かるけど、ヘルパーに渡すテンプレートブロックにはobject型のitemが必ずわたりまする。

2010年10月24日日曜日

MVC 3 Betaのステップイン

前回のエントリーでも行ったように、デバッグ時にソースにステップインできると便利ですよね。ソースサーバーに上がってるならそっちのほうが手軽だけど。

とりあえず、ASP.NET MVC3 Betaのソースにステップインしてみましょう!

aspnet - Release: ASP.NET MVC 3 Beta

stepin1

ソースのダウンロードをしたら、まずは新規プロジェクトの作成。

stepin2

stepin3

とにかく作る。何も考えずにクリック・クリック。そしたら↓こうなりますね。

stepin4

ここからです!

参照設定から、

  • System.Web.Mvc
  • System.Web.Helper
  • System.Web.WebPages

の3つを削除。男らしくね。乙女らしくでもいい。

stepin5

つづいて、ソリューションにダウンロードしたMVCのソースからプロジェクトを追加。既存プロジェクトの追加ですね。

結構沢山必要です。

  • mvc3-beta-source\
    • mvc3\
      • src\
        • SystemWebMvc\System.Web.Mvc.csproj
    • webpages\
      • src\
        • System.Web.Helpers\System.Web.Helpers.csproj
        • System.Web.Razor\System.Web.Razor.csproj
        • System.Web.WebPages\System.Web.WebPages.csproj
        • System.Web.WebPages.Razor\System.Web.WebPages.Razor.csproj
        • WebMatrix.Data\WebMatrix.Data.csproj

stepin6

あとは、アプリケーションプロジェクトで削除した参照設定の分をここで追加。

stepin7

ほらね、これでReSharperも黙った。

stepin8

最後の一手が必要なんだけど、この段階でステップインしてみましょう。とりあえず、RazorViewのRenderViewを開いてブレークポイント。F5を押してみる。

stepin9

残念ながら例外ラッシュ。

stepin10

stepin11

最後の一手を打つ時が来ました。

エラー内容の通り、デフォルトのweb.configだとGACのアセンブリを見るようになってるので、ちゃんとローカルのアセンブリを見るよう変更する必要があります。

アプリケーションルートのweb.configのassembliesセクションを以下のように。

<compilation debug="true" targetFramework="4.0">
  <assemblies>
    <add assembly="System.Web.Abstractions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
    <add assembly="System.Web.Routing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
    <!--<add assembly="System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
    <add assembly="WebMatrix.Data, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
    <add assembly="System.Web.WebPages, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
    <add assembly="System.Web.Helpers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />-->
    <add assembly="System.Web.Mvc" />
    <add assembly="WebMatrix.Data" />
    <add assembly="System.Web.WebPages" />
    <add assembly="System.Web.Helpers" />
  </assemblies>
</compilation>

~/Views/web.configも同じようにGAC見ないように変更。

<configSections>
  <!--<sectionGroup name="system.web.webPages.razor" type="System.Web.WebPages.Razor.Configuration.RazorWebSectionGroup, System.Web.WebPages.Razor, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
    <section name="host" type="System.Web.WebPages.Razor.Configuration.HostSection, System.Web.WebPages.Razor, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" />
    <section name="pages" type="System.Web.WebPages.Razor.Configuration.RazorPagesSection, System.Web.WebPages.Razor, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" />
  </sectionGroup>-->
  <sectionGroup name="system.web.webPages.razor" type="System.Web.WebPages.Razor.Configuration.RazorWebSectionGroup, System.Web.WebPages.Razor">
    <section name="host" type="System.Web.WebPages.Razor.Configuration.HostSection, System.Web.WebPages.Razor" requirePermission="false" />
    <section name="pages" type="System.Web.WebPages.Razor.Configuration.RazorPagesSection, System.Web.WebPages.Razor" requirePermission="false" />
  </sectionGroup>
</configSections>

<system.web.webPages.razor>
  <!--<host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />-->
  <host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc" />
  <pages pageBaseType="System.Web.Mvc.WebViewPage">
    <namespaces>
      <add namespace="System.Web.Mvc" />
      <add namespace="System.Web.Mvc.Ajax" />
      <add namespace="System.Web.Mvc.Html" />
      <add namespace="System.Web.Routing" />
    </namespaces>
  </pages>
</system.web.webPages.razor>

いずれも、VersionとCultureとPublicTokenを消すだけね。そうすると...。

stepin12

stepin13

ヒャッホーイ!ステップインするし、ちゃんと動くね!

※ホントはfavicon.icoのnot foundの例外が起きててちょっとウザイけど。

これで何か気になるところがあると何でも調べられて便利この上ないですな。WebPage.InitializePageなんていうvirtualな空メソッドがいて、必ず初期化時に呼び出されてたりするなんてのもみえてきて、ココでも処理の横取りできるな~とか。ワクワクすっぞ。

ちなみに”_ViewStart.cshtml”があるから初期化処理なんかはココに書くんだけど(ギャフン)、RazorViewEngine.ViewStartFileNameにinternal static readonlyでセットされてる。

スクリーンショット祭りだね...。

2010年10月23日土曜日

RazorのLayout

MVC 3 Betaのソースが公開されましたね!凄いリファクタリングが進んでます。WebMatrix namespaceのソース(MVC関係なくWebPageとして)も含まれてるし、マニアにはたまらないですね!

aspnet - Release: ASP.NET MVC 3 Beta

Databaseや各種Helperなんかはドキュメント整理また無くてもすべてまるっとお見通しだ!な状態で使っていけるのは嬉しい限りです。

Scottguのブログで以下のようなエントリがありました。

ASP.NET MVC 3: Layouts with Razor - ScottGu's Blog

WebFormsでいうところの.MasterはWebPagesでLayoutというのですが(基底クラスの違うcshtml/vbhtmlですね)、どうやって使いましょうかという内容です。@RenderBody(他にもSectionとかいいものもたくさんありますよ!)が魔法の言葉です。

まず、簡単にIndex.htmlにLayoutプロパティを指定する方法。これは直感的で分かりやすいですね。対象となるViewのヘッダ部で直書きですから。

で、全部のViewにいちいちLayout書くの面倒だ!ってなった場合、StartPageクラスの隠しViewとして_ViewStart.cshtml(~/Viewsに置いとくとよしなに処理してくれる優れものだけど、規約嫌いの人には気持ち悪いと言われそう)というのがあるので、それを使って一括で指定してしまう方法があります。

ここまでをScottguは紹介してくれてますが、MVCerならだれもが気になるところのViewメソッドのMaster指定オーバーロードの場合どうなるんだべ?というところ。

public ActionResult Index(string name, string country)
{
  ViewModel.Message = "Welcome to ASP.NET MVC!";

  return View();
}

↑これが普通だけど、↓こうするパターンです。

public ActionResult Index(string name, string country)
{
  ViewModel.Message = "Welcome to ASP.NET MVC!";

  return View("Index","_Layout");
}

部分的にLayoutを変更したい時などによく使いますが、StartPageで指定したものと、WebPage内で指定したもの、ViewResultで指定したものの優先順位が気になりますよね!

ね?

試してみましょう。まずは_ViewStart.cshtmlとIndex.cshtmlの両方でLayoutを指定した場合どちらが優先されるのか。比較するために以下のような単純な_SimpleLayout.cshtmlと_PinkLayout.cshtmlを~/Views/Shared直下に置いておきます。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>@View.Title</title>
</head>
<body>
    <h1>Simple Layout</h1>
    <div>
        @RenderBody()
    </div>
</body>
</html>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>@View.Title</title>
</head>
<body style="background-color:pink;">
    <h1>Pink Layout</h1>    
    <div>
        @RenderBody()
    </div>
</body>
</html>

 

この状態で、_ViewStart.cshtmlでは"_Layout.cshtml"を指定し、Index.cshtmlでは"_SimpleLayout.cshtml"を指定してみる。

結果は↓こうです。

layout1

Index.cshtmlに指定したLayoutが優先されますね。

この状態で、ControllerでPinkLayoutを指定してみます。

public ActionResult Index(string name, string country)
{
  ViewModel.Message = "Welcome to ASP.NET MVC!";

  return View("Index", "_PinkLayout");
}

layout2

おぉ~。Controller指定(ViewResult)最強の優先順位。理由は公開されたソースを見ていくとわかるんでしょうね。なんか深くてみるの面倒だな~。見てみるか。えと...。

RazorViewEngineからWebViewPageへ潜って、RazorViewへさかのぼるとLayoutPathをOverridenLayoutPathにセットしてますね。WebPageBaseにLayoutを保持し、WebViewPageにOverridenLayoutPathを保持するという2段構え。RazorViewのコンストラクタでControllerで指定するLayoutパスをRazorViewのLayoutPathに保持して、その値をWebViewPageのOverridenLayoutPathにさらにセットすることで、Razor単体でのLauout設定(_ViewStart.cshtmlとIndex.cshtmlでのLayout)を無視して適用できるようにするってことですね。いつ無視するのかというとWebViewPageのExecutePageHierarchyでしょうか。WebPageBase.Layoutプロパティを!String.IsNullOrEmpty(OverridenLayoutPath)の時に上書いてます。

Razorテンプレートがコンパイルされた時点で、Layoutプロパティの値をセットするというコードが生成されてるけど、それを実行時(Execute実行後)に上書くことでControllerからLayoutを指定できるようになっているという仕組みということですね。

ベースクラスのExecutePageHierarchyを実行後(WebPageBase.ExecutePageHierarchy内部でExecuteを呼び出します)、OverridenLayoutPathを上書きです。

#pragma checksum "…\Views\Home\Index.cshtml" "{406ea660-64cf-4c82-b6f0-42d48172a799}" "AE4546615BCDE09DA34E6447CC0AFD71"
//------------------------------------------------------------------------------
// <auto-generated>
//     このコードはツールによって生成されました。
//     ランタイム バージョン:4.0.30319.1
//
//     このファイルへの変更は、以下の状況下で不正な動作の原因になったり、
//     コードが再生成されるときに損失したりします。
// </auto-generated>
//------------------------------------------------------------------------------

namespace ASP {
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Net;
    using System.Web;
    using System.Web.Helpers;
    using System.Web.Security;
    using System.Web.UI;
    using System.Web.WebPages;
    using System.Web.Mvc.Ajax;
    using System.Web.Mvc;
    using System.Web.Mvc.Html;
    using System.Web.Routing;
    
    
    public class Index_cshtml : System.Web.Mvc.WebViewPage {
        
        protected ASP.global_asax ApplicationInstance {
            get {
                return ((ASP.global_asax)(Context.ApplicationInstance));
            }
        }
        
        public override void Execute() {

            
            #line 1 "C:\Users\takehara\Documents\My SkyDrive\Develop\MySamples\Mvc3bApplication1\Mvc3bApplication1\Views\Home\Index.cshtml"
  
    Layout = "~/Views/Shared/_SimpleLayout.cshtml";
    View.Title = "Home Page";


            
            #line default
            #line hidden
WriteLiteral("\r\n<h2>");


            
            #line 6 "C:\Users\takehara\Documents\My SkyDrive\Develop\MySamples\Mvc3bApplication1\Mvc3bApplication1\Views\Home\Index.cshtml"
Write(View.Message);

            
            #line default
            #line hidden
WriteLiteral("</h2>\r\n<p>\r\n    To learn more about ASP.NET MVC visit <a href=\"http://asp.net/mvc" +
"\" title=\"ASP.NET MVC Website\">http://asp.net/mvc</a>.\r\n</p>\r\n</script>");


        }
    }
}

 

ん?なんか違う気がしてきた。BuildManagerから出力されたコードだと、Execute内でLayoutセットしてるよね...。おやや~?モヤモヤする。まぁ、いっか。また今度調べてみよう。

追記

で、ちょっと調べたらPageContextっていうのがいた。Push/Popで積んでいく感じで実行するんだけど、これがなにかするくさい。普通に指定されたLayoutを実行したあとにOverridenLayoutPathを実行するのか、実行そのものを置き換えるのか。今度はそこか。Layoutの状態で実行した後、OverridenLayoutPathで実行?そんな無駄な動きするかな~。

しょうがないのでステップ実行。

layout4

RenderView.ExecutePageHierarchyでWebPageBase.ExecutePageHierarchyを実行。ここでStartPageを登録しているので_ViewStart.Executeを実行。続いてChildPage(WebViewPage)として登録されているIndex.cshtmlのExecutePageHierarchyを実行。この時の出力をスタックに積んでいるStringWriterに置き換える(もとはHttpWriter)。で、Index.cshtmlの引数なしWebPageBase.ExecutePageHierarchyを実行(深い)。この中からIndex.cshtmlのExecuteがやっと実行される。このExecuteがRazorテンプレートの出力結果でoverrideされてる実体ですね。なので、ここでページ単位でのLayout指定が実行されます。ちなみにこの段階ではまだ_Layout.cshtmlは実行されてないですね。

ここでやっとOverridenLayoutPathの登場。自身のLayoutをこの値で上書き。WebPageBase.ExecutePageHierarchyに戻ってPopContext()。誰やねん!これが重要で、この中でStringWriterに書き出したRazorのExecute結果を退避。やっとLayoutの実体が見えてきた。

layout3

RenderSurroundingに退避したコンテンツの実体と、Layoutを渡してます。この中で@RenderBodyにコンテンツの実体を埋め込んでレスポンスに流れていくということでした。長い...。

と、言うわけで、Index.cshtmlを実行後、対象となるLayoutをOverridenLayoutPath(Controllerで指定)を優先で取り出し、結果的に_PinkLayout.cshtmlが実行されるというわけですね。スッキリ!

WebPageExecutingBaseに階層がコメントとして残されてます。この辺はソースを見ることの特権ですね。流石のクオリティです。

/*
WebPage class hierarchy

WebPageExecutingBase      The base class for all Plan9 files (_pagestart, _appstart, and regular pages)
    AppStartBase          Used for _appstart.cshtml
    WebPageRenderingBase
        PageStartBase     Used for _pagestart.cshtml
        WebPageBase
            WebPage       Plan9Pages
            ViewWebPage?  MVC Views
*/

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年9月1日水曜日

TFS on CodePlex

CodePlexにBoFのデモで使用したお絵描きアプリ「MVC Graffiti」をアップロードしました。SkyDriveに上げたならこっちには必要ないじゃないかと思われるところですが、オープンソースとして公開しておくといろいろとお得な特典が待ち受けています。

例えば、Visual StudioのIDE内で利用出来るSVNクライアント、VisualSVNなんかもその一つで、オープンソースを公開していれば無料でライセンスを貰えます。貰いました。てへ。他にReSharperなんかもオープンソースオーナーにはもらえるみたいですよね。MVPじゃなくてもオープンソースを公開してるとライセンスをもらえるツール類はいろいろあるものです。とりあえず今狙ってるのはdotTrace。V4出るみたいだしね!

dotTrace Profiler :: Frequently Asked Questions

これまでSubversion、Mercurialと使ってみたので今回はTFSで。その手順を書いておきます。

codeplex1 codeplex2codeplex3 codeplex4codeplex5 codeplex6

http://www.codeplex.comにアクセスして、上記の通り順番に。ここまでは何の変哲もない操作です。英語ですけど気にせずチャレンジしちゃいましょう!

codeplex7

で、ソースタブを確認してみると、なんかダメなんよ的なメッセージ。しばらくしないとダメなのかな?と、思って一晩寝かせてみても変わらずアクセス出来ません表示のままです。

こりゃいかんじゃないですか。なので、早速CodePlexにコンタクト。

CodePlex - Contact Us

やっぱり英語なんですけど、あんまり気にせず適当な英語でアクセス出来ないことをアピール。返事が来るまで気長に待ちましょう。今回は週末を挟んだので2日くらいで返事が来ました。

This issue has been resolved. Your project should be successfully created.

Sorry for the inconvenience,
Matt

こんな感じで返事が来たので、早速VS2010からアクセス!

tfs1

一度ソースタブに行ってみると、↑こんな感じで設定方法を見ることが出来ます。

ここスルーするとあとでログインできなくて悲しいことになります。画像をズームすると分かるんですが、CodePlexのアカウントと、TFSのアカウントは別モンで”takepara”でCodePlexにログインするならTFSには”snd\takepara_cp”となります。snd\と_cpを忘れるとずっと怒られ続けます。

これに従って、TFSサーバーをチームエクスプローラに追加。

tfs2

ソリューションをソース管理に追加して出来上がり!

tfs3

なんですが、このままだとVS2010起動してTFSにアクセスするたびにログインを要求されてしまいます。切ないですね。面倒ですね。鬱陶しいことこのうえない!チームじゃなくてひとりだから文句いわれるのか??

tfs9

tfs4

そういうわけじゃないみたいで、ちゃんと対応方法がありました。

まずはコントロールパネルのユーザーアカウントで「資格情報マネージャ」を開いてみましょう(こんな機能があるのを初めて知りました)。そこには過去アクセスしたことのあるないようが記憶されてました。ここにCodePlexの設定も追加します。

tfs5

「Windows資格情報の追加」をクリックして資格情報を追加します。

tfs6

ここで気をつけないとイケないのがsnd\と_cp。忘れずに!これを登録しておくとVS2010を起動するたびにログイン要求されることもなくなり、とても優雅な開発を味わうことができるようになります。

tfs7

ちなみにTFS使ったことないです。チェックアウトとか意識しなくてもいいらしいとは聞いたんですが、ソリューションエクスプローラに鍵マークでてるとドキドキします...。普通に開いて編集できるんだけど、SVNになれてるとなんか気になる。

そんなこんなで、画像ばっかりで中身の薄っぺらいエントリーですが、個人利用リポジトリとしてのCodePlex利用+オープンソースコミッター特典狙いで、気楽にソースを公開してみてはどうでしょうか。

2010年8月29日日曜日

MVC Graffiti

宇宙兄弟面白いよね!ムッタと日々人の宇宙飛行士を目指す兄弟の話。

...。

去年に引き続き、今年もTechEdでBoFに呼ばれたので行ってきました。TechEd自体は3日間の開催期間があるんですけど、月曜から水曜まで会社で合宿状態で、初日は参加できず。2日目は現地入りしたものの、ずっと障害調査でパシフィコにWiFi借りに行っただけみたいな状態。ちなみにこの時点でBoFで使う予定のデモプログラムはまだちゃんと動かず。貝になりたい。

IMG_0057

去年はオープンしてなかったけど、今年はオープンしてるクリスピー・クリームにココロオドル。

今回のデモのテーマは「人類の進歩と調和」です。ウソですね。はいはい。テーマなんて特に無いです。

bof1

↑これです。多人数で同じキャンバスに絵を書くアプリで、一般的にはなんていうのかな、お絵描きチャットとでも言うんでしょうか。そんなようなものです。

仕組みとしてはいたってシンプルです。

WebアプリケーションとしてASP.NET MVC 3 Preview1をベースにプロジェクトを作成。もちろんViewはRazorです。ここは実質ほとんど処理もなく、Webアプリケーションの構造を制御するためと、サーバーサイドでサムネイル画像を生成するくらいのシンプルな構造です。

データ通信はWCF Data Servicesを使ってます。XMLではなくクライアントサイドのJavaScriptからJSONでデータの取得・更新・削除をRESTで行うのに、これより簡単に実装する方法はないんじゃないでしょうか。実質EFでモデルを定義し、標準テンプレートにジェネリッククラスに指定するくらいしか作業はない感じですよね。今回は少しWebGetで機能を追加してます。

残るはクライアントサイドのJavaScriptで、HTML5のCanvas要素にレンダリングしたりXHRを使った通信部の実装です。

「マイクロソフトのWeb テクノロジ最前線と現実解を語ろう」

が、BoFのタイトルでしたよね。覚えてますか?

ということで、今回のデモで使ったテクノロジは以下の通り。

  • ASP.NET MVC 3 Preview1(Razor)
  • WCF Data Services
  • HTML5(Canvas)
  • CSS3
  • jQuery

MVCの中ではViewに対してデータをほとんど送ってないです。これはつまりMVCでデータ制御をほとんど行ってないからです。データに関してはすべてWCF Data Servicesで通信してるので、クライアント部をSilverlightにしたとしても、そのままデータは利用できるので、もっとリッチなキャンバスを作るのも面白いかもです。

四の五の言わずにソースコードですよね。今回はCodePlexにTFSでアップしようと思って、いろいろ試したんですが...。ちょっとまだうまくできてないです。なのでとりあえずSkyDriveに置いておきます。

↑ここからダウンロードして展開してソース見てみてね!データベースのセットアップはスクリプトになってるので、MvcGraffitiという名前でデータベースを作成後(なんでもいいです)Databases\MvcGraffiti.sqlを実行してテーブルを作成してください。BoFで使ったデータもそれぞれスクリプトにしています。作ったデータベースに合わせてWeb.configのconnectionStringを適当にいじってください。これで動くはず。うまくいかない場合は連絡いただければできる範囲で対応していきます。このエントリのコメント欄によろしくです。

今回のサンプルはwebkit系ブラウザにのみ最適化して作ってます。なので、HTML5対応してるとうたっている他の実装ではちゃんと動きません。そこは手抜きです。座標の計算とかの部分がちょっとへんちくりん。PCでのお勧めはSafariではなくChrome。なぜかというとSafariではWheelイベントが発生しなくてマウスのコロコロをつかったズームが効かない(Zoom自体はCSSのZoomを使ってます)から。後はMobile SafariとしてiPhone4とiPadで動作確認してます。こちらはWheelイベントではなくTouch系イベントをハンドリングしてます。なのでJSのソースを見てもらうとわかるんですが、PC向けのMouseInputクラスとMobile向けのTouchPanelInputという2種類のクラスを用意して入力デバイスごとの処理を記述してます。といっても、イベントのハンドリングと座標の取得方法が違うくらいですが(そこが一番面倒...)。

Mobile Safariを使った場合、指2本でズームしたり、キャンバスをドラッグできるようになってるので試してみてください。この辺、意外と知られてないと思いますが、イベントハンドリングして自分ですべて実装する必要があります。ただのHTMLに対してはそんなことしなくてもいいんですけど、今回Canvasお絵かきなので楽はできないってことですね。

MVC3になって目に見えて違うのはViewDataのほかにViewModelプロパティ経由でViewにデータを渡せるようになってるところでしょうか。ModelBinderは今回のデモで使ってないので。ViewModelはdynamic型なので、データだけじゃなくFunc<T>渡せます。今回のデモの中でIPアドレスをViewで表示してるところが無駄にこの機能を使って、IPアドレスそのものではなく、IPアドレスを返すデリゲート経由でViewは表示してます。GraffitisControllerの36行目と、List.cshtmlの31行目。可能性は無限ですね。使い過ぎ注意ですけど(特にデータをどうのこうのするようなものには適用しないほうがいいかも)。

VS2010だとしても、まだRazorに対する対応はできていないので、コードハイライトもインテリセンスも効きません。なにも設定しないと、VS2010がただのメモ帳です。なので、コードハイライトだけでも行いたい場合は拡張しcshtmlにたいしてHTMLエディタを関連付けしてみましょう。

bof2 bof3

そうすると↑こんな感じにはなります。あとは拡張機能マネージャで"Razor Syntax Highlighter"を入れるとかでしょうか。黒バックの場合切ない表示になるので個人的には使ってないですが、白バックならきれいに表示されます。

あと、Razorの構文については英語の資料になりますが、以下のサイトからダウンロードできます。

Download details: ASP.NET Web Pages with Razor Syntax

200ページ越えです。読み応え抜群です。後はASP.NET MVC3 Preview1のソースをダウンロードするとRazorのパーサーとWebPage関連一式のソースも含まれてる(MVC専用ソースなのがPreview1のかわいげのあるところです)ので、パーサーマニアにはたまらないでしょう。

Tipsとしては困ったときの<text/>。これで囲むといったんパースのコンテキストがCodeParserからMarkupParserに切り替わるので、c#の終わりがわからない!と怒られた時には試してみましょう。

残るはWCF Data Services。かつてADO.NET Data Servicesと言われていたものです。大きなところではAtomPub/JSONだったのがAtomPub改であるODataに対応してるところですね。それがどううれしいかというと会場でも話しましたが、PowerPivotによるセルフサービスBIの促進による無駄エネルギー使用(レポート作成のエネルギーは顧客が使えばよろし)の撤廃です。この辺話し出すと長くなるのではしょります。デジタルナーバスシステムです。ね!ゲイツさん!

Microsoft Developer Network Weekly News Japan = Vol.46 = 2/ 25/98

ふるっ!

WCFのレイヤで介入するIDispatchMessageInspectorを使うと、Data Servicesの処理に到達する直前に介入できるのと、Data Servicesのレイヤで介入するQueryInterceptorやChangeInterceptorなんかを利用するとスマートにデータアクセスに対して機能拡張が施せます。

今回はInspectorの使用例としてJSONP and URL-controlled format support for ADO.NET Data Services - Homeで公開されているQueryStringによるformat指定Behaviorを使ってみました。プログラム中ではまったく有効利用しません。これってどういうことかというと、WCF Data Services自体が無効なQueryStringは例外を出してくるので、好き勝手なQueryStringを使うことはできない仕様なんですが、InspectorでData Servicesの処理が始まる前に$format指定をリクエストヘッダ"accept"に移動させるということをしています。そうするとData Servicesからすれば普通にContentTypeを指定してリクエストされたものだと判断して処理を進めてくれるので、自分でそれ以上の処理をする必要がなくなるわけですね。今のところAtom/JSONしかないですけど、多分うまいこと介入すればCSVをレンダリングとかもできるんじゃないかとにらんでます。どこかのProviderなんじゃないかな~。適当ですいません。

デモアプリケーションのJavaScriptの構造としては以下のような感じです。

bof5

network部はtimerでsend/receiveをくるくる回してます。あんまりリアルタイムにはしてないです。

サーバーサイドの構造としては以下のような感じです。

bof6

雑にもほどがある...。

あれですよ、デベロッパーたるもの「コードで語ってくれ」ですよ。九十九も「拳で語ってくれ」って言ってたじゃないですか。

BoF後に「WebSocketとかもいいですよね!」と声を掛けてくれた方がいましたが、まさにその通りで、今回のようなアプリケーションの場合XHRで処理するよりWebSocketを使ったほうがより効率のいい通信ができるでしょう。あと、コードの9割がJavaScriptだし、iPad持ち出してデモしたりとMSのテクノロジがあまり使われてないじゃないか!なんて野暮なことは言いっこなしです。ちゃんと使ってるんですよ。ただコード量が少なくて済むというだけです。なので全体比率としては少ないです。localStorageやsessionStorageなんかを使うと完全にスタンドアロンで動かすこともできるようになるので面白そうですね。

それと、今回EFを使ってWCF Data Servicesでアクセスするようにしてますが、リードオンリーなデータ公開ならLINQ to SQLをベースにするかPOCOテンプレートを使うほうが使い勝手はいいと思ってます。更新系はMVCでバリデーションかけてそっちから更新する感じです(更新はUIにフィードバックしやすいほうが開発が楽だと思いますがどうでしょう?)。バランスですね。

Ask the Speakerのほうでも話題になりましたが、SQLをジェネレート任せにせず、自分でチューニングしたものをEFなりLINQ to SQLで使いたい場合はどうするのさ、についてはストアドで書いて、DataContext/ObjectContextにはやす感じですね。カスタムなエンティティとして返すもよし、他のテーブルと同じ定義を戻すもよし、です。その後動的に条件を付ける場合はIQueryableにどんどこ追加してしまいましょう。最初のクエリが効率よく絞れて抽出できてればあとは、オンメモリのLINQ to Objectで!

だいたい、こんな感じです。わからない・わかりにくい点など多々あると思うので、そんな時には質問いただければと思います。

いきなり話は変わりますが、WebMatrixなどの他のツールについての話が出てましたが、最近読んだ本で「Amazon.co.jp: 続・ハイパフォーマンスWebサイト ―ウェブ高速化のベストプラクティス: Steve Souders, 武舎 広幸, 福地 太郎, 武舎 るみ: 本」っていうのがあるんですが、ちょっといい感じの記述があるので紹介。付録Bの"Yahoo!JAPANが実践するWebの高速化"のB.5.1役割分担。

大きく分けて3つの開発フェーズがあり

  • コンテンツの情報を設計する「インタラクションデザイン」フェーズ
  • コンテンツの見た目をデザインする「ビジュアルデザイン」フェーズ
  • コンテンツのHTML/CSSを実装する「ウェブデベロップメント」フェーズ

となります。

ここでWD(ウェブデベロッパー)とFEE(フロントエンドエンジニア)という役割がありまして、それぞれ担当するフェーズも違うわけですね。ここが難しいところなんですが、「デザインはデザイナー」というのが口癖のこの業界、デザインとは何を指しているのでしょう。見た目だけ?動きのある場合は動きもデザインするのがデザイナー?そもそもの情報設計はデザイナー関与せず?HTML/CSSの実装は?その辺のふわっとした垣根のおかげで、お互いが密に連携できず、特に会社が別々だったりすると誰がどこまで担当してるのかで、ごにょごにょな状態になりやすかったりね。WebMatrixはフェーズの違う部分の発生するロスを減らしていくためにフェーズをまたいだツールとしての意味もあったりするんじゃないかと勝手に妄想してます。

最後に、BoFに参加してくれたたくさんの開発者の方々、いきなりキャンバス作りまくったり削除しまくったりと、想定外の操作で会場を盛り上げてくれてありがとうございました!質疑応答があまりできなくてすいませんでした。

小野さん、ナオキさん、今回も声掛けてくれてありがとうございます。Techdaysのトラウマがはれそうです。

それとチャック。急きょWiFiルーターを貸してくれてほんとにありがとう。あれがなかったら会場に来てくれた方々は見るだけで、実際にキャンバスに描くこともできず、企画倒れになるところでした。

なんのその、男は裸百貫の波に立つ獅子であれ

dotnetConf2015 Japan

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