2009年5月26日火曜日

UnityとEntityのAttach

第2回 スキャフォールディング機能で軽々DB連携アプリケーション - @IT

まだ2回目だけど、楽しみにしてる連載です。今回の記事で少し気になる部分があったので、実際に動くコードを書いてみました。暇人...。いやいや、お勉強デス!

その気になる部分っていうのが、ページ5の「既存のレコードを変更したい今回のようなケースでは、UpdateModelメソッドを使用する必要がある。」という部分。いや、もちろん、ModelBinderのみでDBに保存なんてしないし、これが間違ってるという分けじゃないけど、単純に出来る出来ないという話なら出来るっていう話です。

EntityKey and ApplyPropertyChanges() - Stack Overflow

なんだかんだと、実際にコードを書いてみたわけじゃなく、たんなる耳年増なだけだと、ちょっとカッコ悪し。

efunity2

Home/Indexが一覧。Editを用意。

efunity3

"Chai"を"Chai XXX"に編集。

efunity4

ちゃんと保存されました。

↓これがObjectContextにアタッチされていないエンティティを使って、DB更新してみるコード。

   public void Save<TEntity>(TEntity entity, Func<TEntity, TEntity> setIDFunc) 
where TEntity : new() { var entitySetName = _dataContext.DefaultContainerName + "." + EntitySetName(typeof(TEntity)); setIDFunc(entity); // 空のエンティティをアタッチしておいて、更新情報はクリア _dataContext.AttachTo(entitySetName, setIDFunc(new TEntity())); _dataContext.AcceptAllChanges(); _dataContext.ApplyPropertyChanges(entitySetName, entity); }

オブジェクトのアタッチ (Entity Framework)

AttachToでObjectContextに空のエンティティをアタッチしておいて、ModelBinderで復元したエンティティの値をApplyPropertyChangesで反映させる流れです。ObjectStateManagerを使うともっと綺麗にできるのかも?

↑このコードはRepositoryクラスに。コレを呼び出すControllerコードは↓。

    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult Edit(int id, Product entity)
    {
      if (ModelState.IsValid)
      {
        _repository.Save<Product>(entity, e =>
        {
          e.ProductID = id;
          return e;
        });
        _repository.SaveChanges();

        return RedirectToAction("Index");
      }

      return View(entity);
    }

認証、認可、入力検証ははしょってるんですが、何となく雰囲気は伝わるかな~、と思います。データベースはNorthwindで、Entity Frameworkを使ってProducts/Categoriesだけのエンティティクラスを作成してます。

efunity

HomeControllerのIndexが一覧ページになるようにしておいて、Editを追加しただけのプロジェクトで試してます。今回はコレに加えてUnityを使ってDIでObjectContextをRepositoryにコンストラクタインジェクションと、RepositoryをControllerにコンストラクタインジェクションさせるようにしてみました。

Unityを使ったControllerFactoryなんかはMvcContribにもコードがあったりするので、そちらを参考にするのが近道です。

でもMvcContribのUnityControllerFactoryはLifetimeManagerを指定しないので、Resolveの度に新しいインスタンスを作ります(TransientLifetimeManager)。HttpContextにObjectContextを入れておいて、同じコンテキストなら無駄使いしないようにするためのHttpContextLifetimeManagerクラスを用意。「ASP.NET MVC Tip: Dependency Injection with Unity Application Block - Shiju Varghese's Blog」に書かれてる、HttpContextLifetimeManagerを使わせてもらいました。Get/Set/RemoveのoverrideでHttpContextを使うようにしてるだけですね。

ObjectContextのインスタンスがどうなってるのかを確認するコードをHomeControllerに書いて確認。

    INorthwindRepository _repository;

    public HomeController(INorthwindRepository repository)
    {
      _repository = repository;
      _repository.Debug("Constructor");
    }

    protected override void OnActionExecuting(ActionExecutingContext filterContext)
    {
      _repository.Debug("OnActionExecuting");
      base.OnActionExecuting(filterContext);
    }

    protected override void OnActionExecuted(ActionExecutedContext filterContext)
    {
      _repository.Debug("OnActionExecuted");
      base.OnActionExecuted(filterContext);
    }

    public ActionResult Index()
    {
      var httpApp = HttpContext.ApplicationInstance as IUnityContainerAccessor;
      new NorthwindRepository(
        (NorthwindEntities)httpApp.Container.Resolve(typeof(NorthwindEntities))
      ).Debug("Other-1");

      var list = _repository.All<Product>();

      new NorthwindRepository(
        (NorthwindEntities)httpApp.Container.Resolve(typeof(NorthwindEntities))
      ).Debug("Other-2");

      return View(list);
    }

Debug内ではObjectContext.GetHashCode()を出力するようにしてます。コンストラクターで渡されるRepositoryはUnityに任せたもので、OnActionExecutingとOnActionExecutedではコンストラクタで渡されたものを出力し、Index内でそれぞれ(Other-1/2)新しくリポジトリのインスタンスを作成(Resolve)して出力させてます。

TransientLifetimeManagerを使った出力。

Constructor:6658142
OnActionExecuting:6658142
Other-1:5603269
Other-2:50559794
OnActionExecuted:6658142

Otherはそれぞれ違うインスタンスが生成されてますね。

HttpContextLifetimeManagerを使った出力。Other-1/2は全然違うのが出力されますね。

Constructor:60183783
OnActionExecuting:60183783
Other-1:60183783
Other-2:60183783
OnActionExecuted:60183783

Otherも同じインスタンスです。お利口さんです。Global.asaxに以下のコードを追加してUnityに登録してます。

    void InitializeContainer()
    {
      if (_container == null)
        _container = new UnityContainer();

      IControllerFactory controllerFactory = new UnityControllerFactory(_container);
      ControllerBuilder.Current.SetControllerFactory(controllerFactory);

      // Register
      _container.RegisterType<NorthwindEntities>(
        /*
         * ContainerControlledLifetimeManagerを使うとSingleton。
         * デフォルトはTransientLifetimeManager
         */
          new HttpContextLifetimeManager<NorthwindEntities>()
                )
                .Configure<InjectedMembers>()
                .ConfigureInjectionFor<NorthwindEntities>(
                  new InjectionConstructor(
                    ConfigurationManager.ConnectionStrings["NorthwindEntities"].ConnectionString
                    )
                );
      _container.RegisterType<INorthwindRepository, NorthwindRepository>();
    }

とりあえず、今回はProductデータだけを利用したけど、他にもいろいろなテーブルを同じコードで簡単に処理できるように「An Irishman Down Under - Polymorphic Repository for ADO.Net Entity Framework - Keith Patton's blog」に書かれてるようなジェネリックなリポジトリを書いてみようかな~と試してみたんですが、どうですかね。あんまり便利な気がしない。簡単な機能実装ならいいかもしれないけど、この辺は機能ドメイン毎にちゃんと書いた方がいい気がする。ところで、EntitySet名ってエンティティクラスから簡単に取得する方法ってないんですかね。EntityKeyには入ってるみたいだけど、アタッチして無いとnullで取り出せないじゃないですか。今回はずるっこして、エンティティクラス名+"s"として生成してます。

んん~。今回もプロジェクト添付しておくので、動かしてみたい方はどーぞー。あ、データベースファイルは添付してないです。

2009年5月23日土曜日

イケメンはしゃべりも上手い

久しぶりにアメージングで試合。最近になってゴーリーが誰一人これなくなり、代わりにオグさんがやるらしく、プレーヤーが足りなくなる。ほら、ビシッと決めれるプレーヤーがいないと、あれじゃないっすか。

今期になって色々あって全然試合にも出れず、チームにも迷惑をかけてたな。なんてことは、これっぽっちも思ってないんだけどさ。だって普通に勝ち越してるし。いてもいなくても勝敗に関係ないじゃん...。でも前期のギリギリプレーオフに比べれば、勝ち越してる今期のほうが全然気が楽だから、小さい事は気にしない。

9:30開始だから心が折れそうだったけど、試合が久しぶりだったからかワクワクしすぎて目覚ましより早く起きれた。ちょっと嬉しい。今日はいい感じでプレーできる気がする。そんな気がするときはたいていいい感じでプレーできないんだけどね!

レフェリーがタカチさんでなんか不安だったけど試合開始。で、一点目を決めたんだけど、タカチさんなかなか笛を吹かず、こっち見てる。あれ?入ってるよ?なんで認めないんだ!早く笛を吹いてオレの得点だとコールしたまえよ!イヤそうな顔しながらしょうがなくコールするレフェリー...。久しぶりなんだからもう少し優しくして...。

2点目をセト君のが決めたんだけど、ここで事件発生。セト君は開始時間に間に合って無くて、背番号空白でロースターだしてたのをすっかり忘れてた。あちゃ~。ベンチマイナーで無駄にキルプレーっす。それは自分の責任なのでしっかりキープして時間を使い守りきれました。あぶね~。

でも、なんやかんやと前半結構点を取られまして。そもそもシュートを打たせたらダメなのに、守り方間違えてた。難しいな~。前半終わったところで2-4。負けてるし。後半しっかり攻めて点を取ればいいから、これ以上失点さえしなければ逆転できる。気楽に考えてたけど、アメージングが久しぶりなのをすっかり忘れてたよ。滑っても滑っても全然スピードにのらない。ウィールがぼろいのもあるけど、滑りかたが戸塚の堅いリンク用の滑りかたになってたみたい。全然相手を振り切れない滑りになってて、無駄に疲れた。ちょっと不安になってきた。

でもそんな不安をシンゴが吹き飛ばしてくれた。前回の試合もそうだったらしいんだけど、あれよあれよと追いつき逆転。シンゴ...。君はそんなしっかり者のプレースタイルじゃなかったじゃないか。ここぞというときに外すのがシンゴスタイルじゃなかったのか?どうやら、たけはら不在の間に、チームを引っ張ってたのはシンゴだったみたいね。おぬし、なかなかのもんだな。それに引き替えたけはらさんは、足で蹴り入れたんじゃないかという疑惑のゴールがあったり。でも、相手のペナルティーによるパワープレーで疑惑を晴らすかのようなディフェンスラインからのスラップで名誉挽回(汚名返上?)。

最終的に相手チームのスタミナ切れで何とか逃げ切って勝てたけど、内容的には反省点の多い試合でした。次回以降もう少し丁寧に大人のホッケーをしないとね。

そうそう、マリちゃんとは初めて一緒に試合出たけど、今日の試合ではマリちゃん含め、なんと全員ポイント!ミカさんもイマイちゃんも、意外なことにミチも!全員にポイントが付く試合はあんまりないけど、それが出来た時は一番嬉しい。最後にちょっとイマイちゃんがケガしちゃって、最後までプレー出来なかったが残念でした。

話は変わるんだけど5/20にボーランドソリューションセミナーっていうのがあってそれに行ってみた。無料だったし。でも、個人参加は一人だけで、少し場違いな気がしなくもなく...。テストツールに興味があったからというより、ケースケがしゃべるってことなんで行ってみた。まるで追っかけ。ファンにはたまらない。無料イベント最高!いやいや、そうじゃなくて、ケースケがしゃべる内容がRIAってことだったんで行ったわけです。RIAの話もリサーチした数値の説明があって現状どういうとらえ方をしてるのかを分かりやすく説明してくれたり、今後もっといろんなデバイスにRIAを展開していく話とか、デザイナーとデベロッパー間での効率を良くするためのCatalystのデモをやったりと盛りだくさんで面白かったよ。でも、一番面白かったのは市ヶ谷の駅前で迷子になって地図とにらめっこしてるたけはらさんをケースケが見つけて会場までつれてってくれたところだね!ちなみに駅前迷子の前に、電車の乗り換え間違えて全然違う方向に向かってて途中慌てて乗り換えてなんとかたどり着いたという事件があったりなかったり。最近3回東急東横線に乗ったんだけど、そのうち2回電車が遅れるということもあったりなかったり。電車って怖い...。

2009年5月13日水曜日

HandleErrorの使い方

そういえば、今月に入って全然エントリを書いてなかった事に気がついた。人に言われて気がついた。ずっと本を読んでて、頭がプシューってなっててさ~。

そんなこんなで、ASP.NET MVCでHandleErrorを使って、HTTPステータスコードに合わせたエラーページを用意する方法が日本語情報として無いかもとふと思ったので書いておきます。

使い方はアクションにHandleError属性を指定して、web.configのcustomErrorsのモードをOn/RemoteOnlyに設定しておけば、defaultRedirectが表示されるというものです。

  [HandleError]
  public class HomeController : Controller
  {
    public ActionResult Index()
    {
      ViewData["Message"] = "Welcome to ASP.NET MVC!";

      return View();
    }

コントローラに指定しても、アクションに指定してもいいですよね。それぞれに指定した場合、コントローラの普通はHandleErrorが優先されます。 例えば、上記コントローラに以下のアクションを追加してみると確認できますね。

    [HandleError(ExceptionType=typeof(InvalidOperationException),View="Exception")]
    public ActionResult ThrowException()
    {
      throw new InvalidOperationException("ぶぶ~");
    }

ViewにThrowExceptionへのリンクを作成して表示させてみたのが↓こちら。

handleerror2

普通に標準のエラーページが表示されます。ちゃんとアクションのHandleErrorにはViewを指定しているにもかかわらず。これをアクションに指定してるほうを優先させるにはコントローラに指定してるHandleErrorの優先順位を下げればOKです。

  [HandleError(Order=1)]
  public class HomeController : Controller
  {
    public ActionResult Index()
    {
      ViewData["Message"] = "Welcome to ASP.NET MVC!";

      return View();
    }

↑こんな感じで。Orderは0スタートなので1以上なら何でもいいです。そうすると今度はどういう表示になるか確認。

handleerror1 

グレート!

ちなみにリリース版のVSテンプレートが出力する~/Views/Shared/Error.aspxでは例外情報の表示コードが無くなってるので分かりにくいですが、このページは少し特殊でInheritsが単なるViewPageじゃなく、例外情報が渡されるように型付きViewPageになってます。

<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" 
Inherits="System.Web.Mvc.ViewPage<System.Web.Mvc.HandleErrorInfo>" %>

なので、Viewページ内でModelを参照するとアクション内で発生したExceptionが参照出来るようになってます。あら便利。自分でエラーページを定義する場合のInheritsを上記のように変更しておけば、自動で参照できるという寸法です。

ちなみに上記スクリーンショットのViewは~/Views/Shared/Exception.aspxというのを作成して表示してます。

で、ですね、例外を捕捉して専用エラーページを表示するのももちろん必要だと思うんだけど、そこはやっぱりHTTPステータスコードに合わせたエラーページを定義して、レスポンスのステータスコードも合わせた方がサーチエンジンにもエラーが認識できるし、RESTfulな感じがして素敵だと思いませんか。例えば、上記のページの場合レスポンスされるステータスコードはなんになってるか確認してみる。

handleerror3

500ですね。例外だからそれで良し。でも、例えばidを指定してDB検索して一致するデータがない、なんて時は404で返したいな~、なんて思いませんか。

試しに、web.configのcustomErrorsを以下のように変更。

    <customErrors mode="On" defaultRedirect="Errors">
      <error statusCode="400" redirect="/Errors/400" />
      <error statusCode="403" redirect="/Errors/403" />
      <error statusCode="404" redirect="/Errors/404" />
    </customErrors>

で、HomeControllerに以下のコードを追加。

    public ActionResult BadRequest()
    {
      throw new HttpException(400, "Bad Request");
    }

    public ActionResult Forbidden()
    {
      throw new HttpException(403, "Forbidden");
    }

    public ActionResult NotFound()
    {
      throw new HttpException(404, "Not Found");
    }

ErrorsControllerを追加して、HTTPステータスコードに合わせてViewを返すようにします。

  public class ErrorsController : Controller
  {
    public ActionResult Index(int? statusCode, string aspxerrorpath)
    {
      var viewName = string.Format("Error{0}", statusCode);

      return View(viewName);
    }
  }

handleerror4 

で、Home/BadRequestにアクセスしたのが↑。拡大すると見えるけど、200が返ってます。400が返って欲しい。そんなときはViewResultを書き換えてしまいましょう。

  public class StatusViewResult : ViewResult
  {
    public int StatusCode { get; set; }

    public StatusViewResult(ViewResult viewResult)
    {
      ViewName = viewResult.ViewName;
      MasterName = viewResult.MasterName;
      ViewData = viewResult.ViewData;
      TempData = viewResult.TempData;
    }

    public override void ExecuteResult(ControllerContext context)
    {
      context.HttpContext.Response.StatusCode = StatusCode;
      
      base.ExecuteResult(context);
    }
  }

こんな感じのクラスを作成しておいて、Errorsコントローラでこっちを利用するように変更。

    public ActionResult Index(int? statusCode, string aspxerrorpath)
    {
      var viewName = string.Format("Error{0}", statusCode);
      var resStatusCode = statusCode ?? 500;

      return new StatusViewResult(View(viewName)) {
        StatusCode = resStatusCode
      };
    }

で、同じアクションを実行したのが↓こちら。

handleerror5 

うへへ。ちゃんと400が返ってます。そりゃそーだ。

エラー用のViewとして400/403/404の3種類だけ用意してみました。

handleerror6

それぞれの実行結果は省略しときますが、例えばこの状態で405とかエラー発生させたらどうでしょうね。HomeControllerに以下を追加。

    public ActionResult MethodNotArrowed()
    {
      throw new HttpException(405, "Method Not Arrowed");
    }

これにアクセスしてみたのが↓こちら。

handleerror7

ErrorsControllerのIndexアクションでError405をViewとして返そうとするけど、そんなViewは定義してないのでInternal Server Errorが発生して、テンプレートで用意されてる~/Views/Shared/Error.aspxがレンダリングされてます。すばらしいじゃないですか。

と、いうわけで、今回のプロジェクト一式もダウンロード出来るようにしておきます。

最近こんな話題ばっかりで、周りからはブログつまらないという声もちらほら。しょうがないので前に書いてた今は無き「オレがルールだ!」からサルベージしてごまかそうかと計画中。

dotnetConf2015 Japan

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