2010年2月15日月曜日

Sauce IDE+RCはFirefox+Firebugでの使い勝手がとてもいい

Webアプリのテストの定番といえば、SeleniumかWatiNなのかな、というレベルのWebアプリテスト初心者ですが、Seleniumの後継にあたる(この辺あいまい)Sauceっていうのを試してみたよ!

Products - Sauce Labs

まだないけどいつかDimeCastsでビデオが公開されるに違いない。

試すにあたり、何かしらプログラムが必要だろうと、昨日ハンサムスーツを見ながらシンプルブログ(全然ブログじゃないというのは本題じゃないYO!)を作ってみた。

タイトルいれて本文入れるだけね。せっかくだからASP.NET MVC RC2で。話はそれるんだけど、RC2のItem Templateはイマイチかも。TextBoxForなんかは新しいTemplateベースのDisplayForやEditorForに揃えてくれるともうちょっと素敵さが増すんじゃないかな~。せっかくDataAnnotationsでDisplayName属性を指定してもLabelForで展開しないとそこから表示名を取得してくれないじゃないっすか。Listテンプレートは面倒かもしれないけど、そうじゃないところはDataAnnotationsの属性を生かせるヘルパーを使った物に統一してもらいたいデス。せっかくUIHintでカスタムEditorTemplatesを指定しても効いてくれなくてちょっと切なかった。

sauce1

↑一覧ページ。殺風景ですいません。

sauce2 sauce2_2

↑新規ページ。同じフォーマットで編集ページもあるよ。ここで入力エラーがあると、DataAnnotationsがバシっと効いてくれるのがV2の素敵なところ。

sauce3

↑新規登録すると一覧に表示されるよ。殺風景ですいません。

sauce4

↑詳細ページ。コレといって何も無いですね。あ、本文にはHTMLが入るイメージなのでHTMLエディターをEditorTemplatesで作ってもいいね。今ならMarkdownのほうがオシャレかの~。

sauce5

↑削除ページ。削除すると、一覧ページに戻るだけ。

とりあえずこんなのを作って、これに対してテストを実施させてみようじゃないですか。

実行したいテストは以下の通り。

  1. 1件も登録されてない状態で新規登録
  2. 1件以上登録されてる状態で新規登録
  3. 未入力状態でCreateを押してモデル検証をエラーにする
  4. 登録されているすべてのエントリを削除する

今回認証は無しで。

そもそもSeleniumすら使った事ないけど、まずはSauce LabsのProductsトップにあるビデオを確認。ふ~ん、ってなもんです。とにかく、Sauce IDEをFirefoxにインストールして、Sauce RCをローカルPCにインストール。まずはこれでいいっぽい?う~ん残念。Selenium RCをダウンロードしておかないとUnit TestをC#で実行できないみたいです。

Sauce IDEがテストレコーダーで、Sauce RCがテスト実行サーバー、Selenium RCがテストを実行させるのに必要なアセンブリ(Sauce RCサーバーとの通信を行う)を提供、って具合の理解でよろしかろうか。Sauce RCはJavaアプリで、Webベースの管理インターフェースもありそこでテスト実行ブラウザやその他設定の変更ができると。

sauce6

テスト実行する場合、ブラウザを起動してそのブラウザをコントロールすることでテスト実行と結果取得する仕組み?Sauce IDEが表示してくれる、C#のコードはNUnitを基本になってて、そこはMSTestでよしなに動くようにチョチョッと変更。大枠はTestInitializeでDefaultSeleniumを初期化するようにするのと、TestCleanupでDefaultSeleniumの停止処理、あとはひたすらテスト書く。

    private ISelenium selenium;
    private StringBuilder verificationErrors;

    [TestInitialize]
    public void Setup()
    {
      selenium = new DefaultSelenium(
"localhost",
4444,
"*firefox",
"http://localhost:20337/"); selenium.Start(); verificationErrors = new StringBuilder(); } [TestCleanup] public void TeardownTest() { try { selenium.Stop(); } catch (Exception) { // Ignore errors if unable to close the browser } Assert.AreEqual("", verificationErrors.ToString()); }

あと、ちゃんとSelenium RCのダウンロードしたアセンブリをテストプロジェクトでは参照設定しとこうね。

DefaultSeleniumの1個目と2個目のパラメータはなんじゃらホイ。3個目はブラウザっぽくて4個目がテスト対象アプリのアドレス。どうもこの1個目と2個目がSauce RCのサーバーらしい。RCの制御用Webインターフェースはhttp://localhost:8421/ でポートが8421。実際のアプリはポートが20337と、いろいろ仕組みを理解しておかないと~。Sauce OnDemandっていうクラウドサービスもあるからね~。

上記初期コードを書いておけば、あとはIDEの出してくれるC#コードをコピペっちょ!なんだけど、それだと思ったようなエレメントを指してくれない。実行結果のHTML全体に文字列が含まれてるかをチェックするIsTextPresentなんかを使うのはテストとして成立しないっすよね。

なので、エレメントの参照はCSS SelectorかID指定かXPath指定がオーソドックスな指定の仕方になるかと思うわけです。でね、Firebugってさ、前バージョンまでは別途Plug inを入れないとXPathの取得ってできなかったけど新しいバージョンになって”XPathをコピー”っていう機能が標準で実装されてるじゃないですか。

sauce7

↑エレメントのXPathが表示されてるから↓右クリックでコピーだ!

 sauce8

で、対象となるエレメントを特定するためのXPathは何の苦労もなく取得できちゃうので、テストコードのアサーション部分やClick対象のエレメントなんかもこれを使って一意に特定だぜ!

    [TestMethod]
    public void A_First_Entry()
    {
      selenium.Open("/");

      selenium.Click("link=Create New");
      selenium.WaitForPageToLoad("30000");
      selenium.Type("Title", "first");
      selenium.Type("Body", "最初のエントリ");
      selenium.Click("//input[@value='Create']");
      selenium.WaitForPageToLoad("30000");

      Assert.IsTrue(
        selenium.GetText("xpath=/html/body/div/div[2]/table/tbody/tr[2]/td[2]") 
        == 
        "first");

      // 最初のDetailsをクリック
      selenium.Click("xpath=/html/body/div/div[2]/table/tbody/tr[2]/td/a[2]");
      selenium.WaitForPageToLoad("30000");

      Assert.IsTrue(
        selenium.GetText("xpath=/html/body/div/div[2]/fieldset/div[4]")
        ==
        "最初のエントリ");
    }

↑こんな感じで入力値と入力結果を比較してちゃんとHTML的に想定通りの結果が生成されてることをテストコードに書けちゃうのが素晴らしい。無知って怖い。これまで自分は一体なにをしてたんだと言いたい。

これらテストを実行する時に、MSTest(かReSharperのテストのどっちがそういう仕様なのかは知らないけど)だとテストメソッド名順に実行されるので、テスト内容が直前の実行結果に依存するような場合は、プレフィックスを決めておく必要があったりしそう。テストクラスそのものを分けてしまうのもありだけどね。

実行すると、ブラウザが起動してあとは勝手に処理してくれるっす。

sauce9  sauce10

↑これがFirefoxで実行してるところ。

sauce11

↑こっちがIEで実行してるところ。

ブラウザの切り替えはTestInitializeの設定じゃなくて、Sauce RCの設定画面が優先、っぽい。何にせよ、初めてのWebアプリのユニットテストとしては好感触。今後も精進していきます。

今回のサンプルアプリとテストコードは↓こちらからどうぞ。

※ちなみにサンプルアプリ自体のテストコードはないのであしからず。

2010年2月13日土曜日

OptionalなRoutingパラメータ

ASP.NET MVC 2 Optional URL Parameters

Philさんのところで取り上げられてるUrlParameter.Optional。

RC2で既に実装されてて、標準のプロジェクトテンプレートが生成するGlobal.asaxのルーティング登録で使われてますね。どういう機能なのかはPhilさんが書かれてる通り、Routing登録時に初期値をセットせず、ルーティングデータにキー自体含めなくするためのモノですね。なるほど~。

      routes.MapRoute(
        "Default", 
        "{controller}/{action}/{id}",
        new { controller = "Home", action = "Index", id = "" }
      );

これまで通りのルーティング登録だと上記のように空文字列を指定したりするところをUrlParameter.Optionalを使って下記のように登録する。

      routes.MapRoute(
        "Default", 
        "{controller}/{action}/{id}", 
        new { controller = "Home", action = "Index", id = UrlParameter.Optional } 
      );

そうすると、デバッグでルートURLにアクセス(~/Home/Indexか何も無しか)すると以下のようなルーティングデータとなりました。

urlparam1

id=””とした場合は、ルーティングデータにidが含まれるのに対し、Optionalの場合はキーすら含まれず、controller名とaction名のみです。

      routes.MapRoute(
        "Default", 
        "{controller}/{action}/{id}", 
        new { controller = "Home", action = "Index" } 
      );

ちなみに↑これはダメっす。idがUrlに含まれない限り解決できないから、ちゃんとすべてのパラメータを指定しなくてもアクセスしたい場合には使えないデス。

なんとなくRoutingの機能が拡張されたのかな?と思えるところですが、MvcHandlerのProcessRequestInitで呼び出してるRemoveOptionalRoutingParametersがOptionalならRouteDataから削除してるんですね。賢い!

これをいじってる時に気がついたんだけど、HtmlHelperにValidation/ValidationForなんてのが追加されてるじゃないですか。RCからなのかな。いったい何に使うのかいまいちよくわからないな~。FieldValidationMetadataをどうしたいんだろう...。クライアントバリデーションに絡んでんのかな~。ViewContext.FormContext.FieldValidatorsに入るはずなのにViewで実行しても特に変化が見られない。使い方間違ってるのかの。FormContextってそもそも誰ぞな。DataAnnotationsModelValidatorのエラーメッセージが日本語なのはなんでっすかね。

2010年2月11日木曜日

お試しAdWords

1月の初め頃だったような、記憶が曖昧なんですけどGoogleからAdWordsのお試し券5000円分が送られてきました。個人的に全く使い道を思いつかなくてすておこうと思ったんだけど、ずいぶん前にamachang(http://d.hatena.ne.jp/amachang/20090113/1231827150 Google Adsense の件について - IT戦記)がAdWordsにブログを広告する実験をしてたのを思い出したので、マネッコマネマネして自分もここのブログを無駄に広告してみようと試してみた。

途中で一度キーワードを変更したり、上限クリック単価を上げてみたりと調整したものの、目的もなくやってるもんだからそれがどうしたと自分にツッコむ以外することもない。

adwords2

↑こんな感じの設定にしておいて、5000円使い切るまで放置!

にしたかったんだけど請求を後払いにしないとお試しできないらしく、使い切るまで完全放置にするのは難しいみたい。すごく長い期間表示するようにして気がついたら請求額が5000円超えなんてことになったら無駄使いにも程がありすぎるし。数日様子をみて1ヶ月くらいは大丈夫かなと期間は1/10~2/5にセット。

adwords1

2種類の数値クリック数と表示回数を表示したのが↑。まぁ、見事な相関関係があります。そりゃそうか。そもそも入札してるキーワードが良くないね。わざわざ広告をクリックしたいようなコンテンツじゃないし、こんな情報を探してる人なら普通に検索しそう。だって”ASP.NET MVC”で検索して出てくる広告でデベロッパーが欲しいのってコンポーネント的なモノだったり書籍だったり以外ないと思うし、そもそもそんなキーワードで検索するようなリテラシーの人が広告をクリックすること自体が疑わしい。

と、言う訳で全くもって試す内容に失敗したAdWords初体験の巻でした。

途中で、ナオキさんに見つかって”クリックしないで!”というやりとりが唯一面白かったところかの~。

  • クリック数 ー 119
  • 表示回数 ー 326,365
  • クリック率 ー 0.04%
  • ご利用金額 ー \4,763

結構ギリギリ金額じゃん。

2010年2月6日土曜日

動的なRouting登録の素敵な方法

MVC V2 RC2が出てるのに関係ない話です。

Editable Routes Using App_Code

少し前にエントリされてたけど、ずっと放置してたです。そもそもEditable Routesで最初の実装サンプルが出てた話。

何をしてるのかというと、動的なRoute登録をするのにいちいちビルドし直さずに、Routes.cs ファイルをBuildManager.GetCompiledAssemblyで実行時にコンパイル。Routes.cs自体にCacheDependencyでファイルの変更監視をさせておき、csを書き換えた時点で動的にコンパイルしなおす(上記BuildManagerで)。そこから取り出される IRouteRegistrar実装がルーティングを登録するようにしておくことで、Global.asaxにルーティング情報を保持しなくて良くなるし、アプリケーションの機能とルーティング情報を分離して、柔軟な環境にできる(Subtextがそうなってるのかね)んですね。

で、 CacheDependencyでファイルを監視させるときにFileSystemWatcherを中で使ってて、それがFullTrustじゃないと実行できないから不便だねっていうのがこのエントリの本題みたい。解決方法としてはシンプルに~/App_Code下にRoutes.csをおいて、自分では監視しないようにするところ。そうしとけば、MediumTrustでも動くんだよと。なるほど。

~/App_Code配下のファイルなら更新時にAppDomainがシャットダウン(CodeDirChangeOrDirectoryRename)して再起動されるから、その ASP.NETの仕組みに任せとかばいいじゃんと。素敵だす。その場合、コンパイルは勝手にされるから、 BuildManager.GetCompiledAssemblyじゃなくてBuildManager.GetTypeでAppDomainに読み込まれてるTypeを取得(もちろんIRouteRegistrarの実装)して実行すればよろしな流れ。

2010年1月24日日曜日

ControlBuilder.ProcessGeneratedCodeを活用

Angle Bracket Percent : Take your MVC User Controls to the next level

確かにNext Level。

普通、ascxをレンダリングするときにはRenderPartialヘルパーを使うところだけど、 T4MVCとか使ってないとパスがマジックストリングになりますね。T4MVC使ってれば、そんなことにはならないけど、そもそもascx単位の RenderPartial的なヘルパーを用意してしまうほうがいいんじゃないの?という話。

で、それを実現するためにいちいちコントロール毎にヘルパーを定義するのはバカらしいってことで、動的生成ですよ。これもまさに黒魔術。

App_Codeの動的コンパイルに介入(ControlBuilder.ProcessGeneratedCodeのoverride)して、実行時動的に拡張メソッドクラスを作り出してます。CodeSnippetTypeMemberとか初めて見た。

まずは、FileLevelUserControlBuilderを派生させたUserControlHtmlHelperControlBuilderを作成。このクラスが動的生成をおこなうクラスで、ProcessGeneratedCodeをoverrideしてCodeTypeMemberCollectionにヘルパークラスのCodeSnippetTypeMemberを追加してます。動的クラスのコーディングはサンプルと言うことでシンプルな実装ですね。

            dummyClass.Members.Add(new CodeSnippetTypeMember(String.Format(@"
        }}
    }}
    public static class {2}Extensions {{
        public static void Render{2}(this System.Web.Mvc.HtmlHelper htmlHelper{3}) {{
            var uc = new {0}();
    {5}
            uc.RenderView(htmlHelper.ViewContext);
        }}

        public static string {2}(this System.Web.Mvc.HtmlHelper htmlHelper{3}) {{
            return {1}.RenderHelper(htmlHelper.ViewContext, () => Render{2}(htmlHelper{4}));
    ", codeGenUserControlFullTypeName, userControlHtmlHelperFullTypeName, helperMethodBaseName, paramBuilder, callParamBuilder, fieldAssignementBuilder)));

 

続いて、このControlBuilderを使ってくれるようにASP.NETに指示をださなきゃいけないんだけど、それってどこでやってるんでしょうね。ぱっと見、以下のクラスの属性指定?

    [FileLevelControlBuilder(typeof(UserControlHtmlHelperControlBuilder))]
    public class UserControlHtmlHelper : ViewUserControl {
        // Helper method which renders the code in a temporary writer and returns it as a string
        public static string RenderHelper(ViewContext viewContext, Action render) {
            TextWriter oldWriter = viewContext.Writer;
            var tmpWriter = new StringWriter(CultureInfo.CurrentCulture);
            viewContext.Writer = tmpWriter;
            try {
                render();
            }
            finally {
                viewContext.Writer = oldWriter;
            }

            return tmpWriter.ToString();
        }
    }

ここが実行時に勝手に解釈されるようになってるのかな~。動的ヘルパーを生成したいascxのInherits="MvcUserControlHtmlHelpers.UserControlHtmlHelper"という記述が通常と違う部分なのはわかるんだけど。

FileLevelControlBuilderAttribute コンストラクタ (System.Web.UI)

実行時に初めてコードが生成されるから、コーディング時には↓こんな感じでメソッドないぜと怒られる。

cb

実行したところで、VSはそんなコードしらないんだってさ。

cb2

そもそもMVCのViewUserControlクラスがこれと同じようにViewUserControlControlBuilderを使ってるんじゃんね。でも、BaseTypeをうわがいてるくらい(目的はよくわかってないデス)ですね。

追記:ascxなりaspxのベースクラス(ViewPage,ViewUserControl)の派生元(親)クラスを指定してるんでした。そりゃそうだ。

<%@ Control Language="C#" Inherits="MvcUserControlHtmlHelpers.UserControlHtmlHelper" ClassName="Gravatar" %>

<script runat="server">
    // Declare the paramaters that we need the caller the pass to us
    public string Email;
    public int Size;
</script>

<%
    // Build hash of the email address
    // Note: in spite of its name, this API is really just a general MD5 encoder
    string hash = FormsAuthentication.HashPasswordForStoringInConfigFile(Email.ToLower(), "MD5").ToLower();

    // Construct Gravatar URL
    string imageURL = String.Format("http://www.gravatar.com/avatar/{0}.jpg?s={1}&d=wavatar", hash, Size);
%>

<img src="<%= imageURL %>" alt="<%= Email %>" title="<%= Email %>" />

※人さまのコードをこんなにコピペしていいのだろうか...。

上記のApp_Codeに作成されてるGravator.ascxのscript blockから正規表現でpublicなプロパティ定義を取り出して、ヘルパーのパラメータ生成に利用してるところも見逃せない。

T4MVCでの動的生成、Emitによる黒魔術に次いで、新たな黒魔術として覚えておくのもいいんではないでしょうか。

※ASP.NET MVCに限らずASP.NETならすべてに適用できるのがミソですよ!

2010年1月23日土曜日

ハーフゾーン+ハーフマンツー

ホッケーを愛する日本全国の人々へ:COVERAGE(カバリッジ)~最終ステップ「5 on 5」 - livedoor Blog(ブログ)

必読。

一度で理解できないとあきらめず、理解できるまで何度でも読もう。アイス・インラインの区別、人数の差異は関係ない内容。

難しい・わからない・理解できない。それなら、理解できるまで考えればいいだけだから。

あぁ~、プレーオフもないし暇すぎる。来期な~。いいんだけど。いい年こいて精神論でホッケーするのはバカらしいと思う今日この頃。

2010年1月4日月曜日

PostSharp興味深い

PostSharp - Bringing AOP to .NET

AOPフレームワークと言われるんだって。

ボスの大嫌いなRemotingのProxy(.NET Framework 2.0 コア機能解説 ~ 第 1 回 .NET リモーティング ~)でのフックや(A basic proxy for intercepting method calls (Part –1) - Mehfuz's WebLog)、Invokerを必ず呼び出すASP.NET MVCでのFilterAttribute達のどっちかが一般的なのかと思ってたけど、世の中スゴイ事思いつく人たちがいるモンで、ビルド後にILを書き換えてフックポイントを追加してしまえばいいじゃないかという発想で作られてます。まさにEmitにを超える黒魔術。

ドキュメントに書かれてる実装パターン。

int MyMethod(object arg0, int arg1)
{
  OnEntry();
  try
  {
    // Original method body. 

    OnSuccess();
    return returnValue;
  }
  catch ( Exception e )
  {
    OnException();
  }
  finally
  {
    OnExit();
  }
}

Reflectorなんかでビルド後のアセンブリを見ると上記のパターンにそったものが出力されてるのが確認出来ます。

postsharp1 

確かに~、確かに~。

ドキュメントを見つつ、どんなことが出来るのかチラッとコード書いてみたんです。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using PostSharp.Laos;

namespace TestPostSharp
{
  class Program
  {
    [ThirdAspect]
    private static int _intFldValue;

    static void Main(string[] args)
    {
      Console.WriteLine("Start program.");

      PrintMessage("Hello, World! - first");
      PrintMessage("Hello, World! - second");
      PrintMessage("Hello, World! - third");
      
      var res = Invocation(100,"まじか!",DateTime.Now);
      Console.WriteLine("res = {0}",res);

      _intFldValue = 100;
      Console.WriteLine("Get field {0}", _intFldValue);

      Console.WriteLine("End program.");
      Console.ReadKey();
    }

    [FirstAspect]
    static void PrintMessage(string message)
    {
      Console.WriteLine(message);
    }

    [SecondAspect]
    static int Invocation(int p1, string p2, DateTime dateTime)
    {
      Console.WriteLine("何かか実行されるようだ");
      Console.WriteLine("{0} - {1} - {2}",p1,p2,dateTime);

      return p1;
    }
  }

  [Serializable]
  public class FirstAspectAttribute:OnMethodBoundaryAspect
  {
    public override void OnEntry(MethodExecutionEventArgs eventArgs)
    {
      Console.WriteLine(eventArgs.Method.Name + " - OnEntry");
      base.OnEntry(eventArgs);
    }

    public override void OnExit(MethodExecutionEventArgs eventArgs)
    {
      Console.WriteLine(eventArgs.Method.Name + " - OnExit");
      base.OnExit(eventArgs);
    }

    public override void OnSuccess(MethodExecutionEventArgs eventArgs)
    {
      Console.WriteLine(eventArgs.Method.Name + " - OnSuccess");
      base.OnSuccess(eventArgs);
    }

    public override void OnException(MethodExecutionEventArgs eventArgs)
    {
      Console.WriteLine(eventArgs.Method.Name + " - OnException");
      base.OnException(eventArgs);
    }

    public override void RuntimeInitialize(System.Reflection.MethodBase method)
    {
      Console.WriteLine(method.Name + " - RuntimeInitialize");
      base.RuntimeInitialize(method);
    }

    public override void CompileTimeInitialize(System.Reflection.MethodBase method)
    {
      Console.WriteLine(method.Name + " - CompileTimeInitialize");
      base.CompileTimeInitialize(method);
    }

    public override bool CompileTimeValidate(System.Reflection.MethodBase method)
    {
      Console.WriteLine(method.Name + " - CompileTimeValidate");
      return base.CompileTimeValidate(method);
    }
  }

  [Serializable]
  public class SecondAspect : OnMethodInvocationAspect
  {
    public override void OnInvocation(MethodInvocationEventArgs eventArgs)
    {
      Console.WriteLine("Calling {0}", eventArgs.Method.Name);

      var args = eventArgs.GetArgumentArray();
      if (args[0].GetType() == typeof(int))
        args[0] = (int) args[0] * 2;

      eventArgs.Proceed(args);
      var res = eventArgs.ReturnValue;
      eventArgs.ReturnValue = (int) res + 100;
    }
  }

  [Serializable]
  public class ThirdAspect : OnFieldAccessAspect
  {
    public override void OnGetValue(FieldAccessEventArgs eventArgs)
    {
      Console.WriteLine("Getter {0} = {1}", eventArgs.FieldInfo.Name, eventArgs.StoredFieldValue);
      
      base.OnGetValue(eventArgs);
    }

    public override void OnSetValue(FieldAccessEventArgs eventArgs)
    {
      Console.WriteLine("Setter {0} = {1} -> {2}", eventArgs.FieldInfo.Name, eventArgs.StoredFieldValue, eventArgs.ExposedFieldValue);
      var value = eventArgs.ExposedFieldValue;
      eventArgs.ExposedFieldValue = (int) value*2;
      
      base.OnSetValue(eventArgs);
    }
  }
}

postsharp2

最初から用意されてる便利クラス。

  • OnExceptionAspect
  • OnMethodBoundaryAspect
  • OnMethodInvocationAspect
  • OnFieldAccessAspect

それぞれ、だいたい名前の通り。引数や戻り値を書き換えたりも出来るよ!IOn~のインターフェースも用意されてるから、用意されてるのが気に入らないなら最初から実装してしまうのも可能。OnExceptionAspectについてはその他のクラスのOnExceptionでも取れるから単体で使う場面はそんなに無かったりするのかな~。どうなんでしょう。

属性クラスとしてAspectクラスを実装して、クラスやメソッド、フィールドに指定してビルドするのがオーソドックスな使い方だと思うけど、更に[assembly:自作Aspectクラス(~)]でBCLにゴッソリ指定出来たりするのが恐ろしい。

めっぽう気になってしかたがないのがCompositionAspect。これってDIなんですかね?

フック出来るタイミングや、シリアライズのカスタマイズやら、プラグイン(ビルド後の処理時にだと思われるけどよく分かってない)、msbuild実行されないASP.NETの対応なんかもあって、イロイロ遊べそうな感じデス!

dotnetConf2015 Japan

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