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

2017年5月17日水曜日

ASP.NET MVCでCQRSを利用してORMをうまく使う

以前(2013年頃)自分が設計した内容のメモです。
今はASP.NETの世界から離れて久しい。

はじめに

CQRS(コマンドクエリ責務分離)という考え方を使って、参照系と更新系でORMとSQLを使い分けていいとこ取りするという趣旨です。

サンプル

サンプルを作りました。
https://github.com/herara-ofnir3/cqrs-sample

CQRS(コマンドクエリ責務分離)とは

CQRSについて詳しくはぜひこちらをどうぞ。

[Greg Young流CQRS - Mark Nijhof]
http://d.hatena.ne.jp/digitalsoul/20100712/1278886009
※ 上記記事のサンプル
https://github.com/MarkNijhof/Fohjin

ORMを使いたい理由と使いたくない理由

更新処理がとても快適

煩雑なSQLの発行処理を書かずとも、エンティティの状態を変化させ、永続化してやればうまいこと更新してくれます。
特に one-to-many や many-to-many の関係をカスケードしてうまく処理してくれるのは、とても素晴らしいメカニズムです。

バッチ処理的な一括更新や大量データを高速に更新するのには向いていませんが、業務アプリケーションの通常の更新処理なら非常にマッチします。

参照系でしぬ

参照系の処理がやりにくいです。
普通なら簡単なSQLを投げてそのまま結果を表示するだけ、みたいな画面ですら、気にすることが多すぎます。

  • 遅延読み込みされるとまずい(N+1問題)から、これとこれをフェッチして...
  • 一回流してみて実際のSQLをチェックしないと...

みたいな調子です。
どうしてもデータの取得が冗長になりがちです。
特にHQLでSQLを再現していると泣けてきます。

よく言われるORMの問題点はこの参照系の問題に集中している気がします。
どう考えてもシンプルにSQL書いたほうがはやくて確実ですし。

ORMの弱点を補うためのCQRS

ORMの得意な更新系の処理だけORM使って、参照系はSQLを書けばいい、という考えに至りました。
そして、参照系と更新系の分割と一貫性のために、CQRSを適用しました。

CQRSはイベントソーシングとセットで語られることが多いのですが、イベントソーシングはスルーします。
データアクセスがクエリ(参照系)とコマンド(更新系)で違うだけの実装です。

その他利点

実際やってみると色々いいところがありました。

良い作りに縛れる
アプリケーションに必要な参照/更新操作がクエリ/コマンドという形でいい感じにコード上に浮き上がってきます。
いわいる「変な作り」というのがやりにくくなります。
テストがしやすい (疎結合になる)

コマンドの発行は全てコマンドバスに投げるだけになるので、
コントローラが依存するのはコマンドバスといくつかのクエリだけとなります。
そのため、アンチパターンとしてありがちなファットコントローラに陥りにくくなります。

依存関係が絞られますし、トランザクションなどの煩雑な処理が隠蔽されるため、テストが非常に簡潔になります。
自然に結合度が下がり、構造がシンプルになります。
おかげでメンバが積極的にテストを書いてくれました。

更新処理のログが簡単にとれる

全てのコマンド(更新操作)はコマンドバスを通して発行されるので、更新処理を簡単に確実にログできます。(コマンドバスにログ処理を挟むだけ)
このあたりの機構にイベントソーシングを組み合わせると強力ですが、ログをとれるだけでも大きな恩恵を得られました。

運用・保守をする上で、現状のデータに至るまでの経緯が全て記録されているというのは、極めて有効でした。

2013年6月16日日曜日

値オブジェクトのためのカスタムモデルバインダー

値オブジェクト

単一の値を表すオブジェクトのことで、いわいる不変オブジェクトのことです。(イミュータブルってやつですね)
例えばこんな感じのヤツですね。

バージョン情報 (e.g: "1.2", "11.101") を単一の値として扱う
public class Version
{
    public Version(int major, int minor)
    {
        Major = major;
        Minor = minor;
    }

    public int Major { get; private set; }

    public int Minor { get; private set; }

    public override string ToString()
    {
        return string.Format("{0}.{1}", Major, Minor);
    }
}

Major, Minor といったプリミティブ値を Version という型にまとめ、状態を変えれないようにすることで扱いがシンプルになります。

これはオブジェクト指向プログラミングのプラクティスとしてよく言われるものですが、ASP.NET MVC で不変オブジェクトを扱うと困ることがあります。

困る例

例えばこんなURLを使いたいと思って、アクションを用意するとします。

/Home/SomeAction?version=1.2
public ActionResult SomeAction(Models.Version version)
{
    ViewBag.Version = version; // version は null で、ModelState にもエラーが入る
    return View("SomeView");
}

当然 version 引数にはクエリパラメータで指定した "1.2" が Major と Minor に入ってきてほしいと思うわけですが、これはうまくいきません。

それもそのはずで、Version という型にどのように値を入れていいか、モデルバインダが知らないからです。

カスタムのモデルバインダを作る

せっかく値オブジェクトをコツコツ作っても、モデルバインダで使えないのでは寂しいので、カスタムのモデルバインダを作りましょう。

上記の Version 型の場合はこんな感じのモデルバインダを作ります。

public class VersionBinder : IModelBinder
{
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var valueKey = bindingContext.ModelName;
        var valueResult = bindingContext.ValueProvider.GetValue(valueKey);
        string rawValue = null;

        if (valueResult != null)
            rawValue = valueResult.AttemptedValue;

        if (string.IsNullOrEmpty(rawValue))
            return null;

        Version result;

        if (Version.TryParse(rawValue, out result))
        {
            return result;
        }
        else
        {
            bindingContext.ModelState.AddModelError(
                valueKey,
                string.Format("'{0}' は無効な値です。", rawValue)
                );

            return null;
        }
    }
}

実際の文字列からの変換は TryParse パターン とかで適当に実装します。

上記のように作ったモデルバインダを、Application_Start 時に登録します。
App_Start ディレクトリに、以下の様な起動用クラスをまとめるといいと思います。

public class BinderConfig
{
    public static void RegisterBinders(ModelBinderDictionary binders)
    {
        binders.Add(typeof(Models.Version), new VersionBinder());
    }
}

これを Global.asax の Application_Start メソッドで呼んであげます。

protected void Application_Start()
{
    // その他いろいろな起動設定...
    BinderConfig.RegisterBinders(ModelBinders.Binders);
    // その他いろいろな起動設定...
}

これで Version 型の値がしっかりモデルバインディングされます。

これたくさん作るの面倒だよね?

モデルバインディングしたい値オブジェクトがたくさんあったら、一つ一つカスタムのモデルバインダを実装するのは恐ろしく面倒です。

コードも定形なのでスーパークラスにまとめると楽です。

public abstract class ValueObjectBinder<T> : IModelBinder
{
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var valueKey = bindingContext.ModelName;
        var valueResult = bindingContext.ValueProvider.GetValue(valueKey);
        string rawValue = null;

        if (valueResult != null)
            rawValue = valueResult.AttemptedValue;

        if (string.IsNullOrEmpty(rawValue))
            return null;

        T result;

        if (TryParse(rawValue, out result))
        {
            return result;
        }
        else
        {
            bindingContext.ModelState.AddModelError(
                valueKey,
                GetParseErrorMessage(rawValue)
                );

            return null;
        }
    }

    protected abstract bool TryParse(string input, out T result);

    protected virtual string GetParseErrorMessage(string rawValue)
    {
        return string.Format("'{0}' は無効な値です。", rawValue);
    }
}

さっきの Version 型の場合、コレを使うとこうなります。

public class VersionBinder : ValueObjectBinder<Version>
{
    protected override bool TryParse(string input, out Version result)
    {
        return Version.TryParse(input, out result);
    }
}

これならまあ作ってもいいかなってレベルじゃないでしょうか。もし大規模なアプリケーションでこれでも大変なら、T4 とか使う手もありそうです。

まとめ

これで値オブジェクトをたくさん使えるよ。

2013年3月22日金曜日

ASP.NET MVC のエラーハンドリング(未完)

ASP.NET MVC でのエラーハンドリングを見直す機会があったのでメモします。

これまで

今までエラーハンドリングは customErrors の設定と、Application_Error イベントで行なっていました。

<customErrors mode="RemoteOnly" defaultRedirect="~/Error">
  <error statusCode="404" redirect="~/Error/NotFound"/>
</customErrors>
protected void Application_Error()
{
 var exception = Server.GetLastError();
 // 例外をロギングしたり...
}

こんな感じでエラーページの表示と適当に例外を起こすアクションを用意しました。

public class ErrorController : Controller
{
    public ActionResult Index()
    {
        return View();
    }

    public ActionResult NotFound()
    {
        return View();
    }

    public ActionResult Raise(int? httpCode)
    {
        if (!httpCode.HasValue)
            throw new ApplicationException("エラーだよ。");

        throw new HttpException(
            httpCode.Value, string.Format("ステータスコード {0} のエラーだよ。", httpCode));
    }
}

これで例外が起きると用意したエラーページが表示され、Application_Error で必要な処理(ロギングとか)を実行できます。

なにが問題?

この方法でもひと通りのことはできているんですが、よく見ると気になることがあります。

例外を起こしてエラーページを表示させてみます。このときの HTTP はこんな風になっています。

エラーページのステータスコードが "200"

エラーページなのにステータスコードが 200 になっています。
できれば 500 やその他適切なステータスコードを返したいところです。

リダイレクトによるエラーページのナビゲート

304 で customErrors で指定したエラーページの URL にリダイレクトしています。
これもできれば、リダイレクトせずにエラーページを表示できるほうが理想的です。

というわけで、"適切なステータスコード""リダイレクトせずに" エラーページを表示する方法を探しました。

エラーページのステータスコード

エラーページがそれぞれ適切なステータスコードで返るようにするのは簡単です。
Response.StatusCode を設定してあげれば OK です。

public class ErrorController : Controller
{
    public ActionResult Index()
    {
        Response.StatusCode = (int)HttpStatusCode.InternalServerError;
        return View();
    }

    public ActionResult NotFound()
    {
        Response.StatusCode = (int)HttpStatusCode.NotFound;
        return View();
    }

    public ActionResult Raise ...
}

これだけでこんな感じで設定したステータスコードでレスポンスが返ります。

リダイレクトせずにエラーページを表示する

次はリダイレクトせずにエラーページを表示する方法です。

customErrors の redirectMode="ResponseRewrite" ... ?

customerErrors には redirectMode というプロパティがあり、 リダイレクトしてエラーページをナビゲートする "ResponseRedirect" かレスポンスをエラーページの内容で上書きして表示する "ResponseRewrite" かを設定できます。

<customErrors mode="On" defaultRedirect="~/Error" redirectMode="ResponseRewrite">
  <error statusCode="404" redirect="~/Error/NotFound"/>
</customErrors>
残念ながら ASP.NET MVC ではうまく動かない

まさにコレ!という内容のプロパティですが、ASP.NET MVC では思ったように動作しません

ResponseRewrite ではレスポンスの内容を指定されたページの内容で上書きしてくれるのですが、 この際 ASP.NET は指定されたパスで物理ファイルを探しにいってしまいます。

なので、WebForm であればこれはうまく動作する(ハズ)のですが、ASP.NET MVC では動作しません。ざんねん...

物理ファイルで

物理ファイル探しにいくなら、物理ファイルで指定したら動くわけで... 指定しちゃってみます。

適当にエラーページを aspx で用意して、customErrors で指定します。
このとき aspx にはステータスコードを指定するコードを入れておくと任意のステータスコードでレスポンスを返せます。

protected override void OnLoad(EventArgs e)
{
    Response.StatusCode = (int)HttpStatusCode.InternalServerError;   
    base.OnLoad(e);
}
<customErrors mode="On" defaultRedirect="~/Views/Error/Index.aspx" redirectMode="ResponseRewrite">
  <error statusCode="404" redirect="~/Views/Error/NotFound.aspx"/>
</customErrors>

動きますね。

これでいいのか

「レイアウト(_Layout.cshtml)とか使えないじゃん、コレ」とか「エラーページも Razor で書こうよ。」とか...

IIS7とかであれば httpErrors を使ってできたりするようです。

また、カスタムの HandleError を作ることでもできます。が、HandleError はあくまで ActionFilter なので、アクションメソッドの外側で起きるエラーには手出しできません。

一番いいのは ASP.NET MVC が redirectMode="ResponseRewrite" のときに物理ファイルをアテにするのではなく、パスの実行結果を上書きしてくれるようになることなんですが...

ここで時間切れ

いろいろ調べてみたんですが、なかなかコレ!っていう解決策が見つからない&思いつきませんでした。
もしいい方法をご存知の方がいらしたら教えて下さい。

2013年1月22日火曜日

ASP.NET MVC 4 の StyleBundle で注意すること

ASP.NET MVC 4 の StyleBundle を利用する際に気をつけたいことがあったのでメモします。

こんな感じでディレクトリとファイルを構成し、スタイルシートを用意した場合を見ていきます。

ファイル

├─Content
   ├─images
   │  └─picture.jpg
   └─site.css

スタイルシート(site.css)

.picture {
  background-image: url("images/picture.jpg");
}

この構成で次のようなバンドルを設定すると困ったことになります。

bundles.Add(new StyleBundle("~/Content/Site/css").Include(
    "~/Content/site.css"));

このバンドルで site.css が出力されるパスは "~/Content/Site/css" です。
背景に指定されている画像は相対パスで参照しているので、"/Content/Site/images/picture.jpg" というパスになってしまい、画像が参照できません。

このようにバンドル時のパスが実際のファイル構成と異なると、スタイルシートから相対パスで参照している画像などが読めなくなってしまうので注意が必要です。

実際のファイル構成と同じパスでバンドルしてあげる必要があります。

bundles.Add(new StyleBundle("~/Content/css").Include(
    "~/Content/site.css"));

また、違うパスにあるスタイルシートをまとめてバンドルするのも避けたほうがいいですね。

2013年1月21日月曜日

ASP.NET MVC 4 の ScriptBundle はデバッグ時に .min.js を出力しない

ASP.NET MVC 4 の ScriptBundle はファイル名の末尾で利用するファイルを選択します。

例えば以下の様な3つのファイルを用意した場合、

  • sample.js
  • sample.debug.js
  • sample.min.js

バンドルの設定を以下のようにした場合を見ていきます。

bundles.Add(new ScriptBundle("~/bundles/sample").Include("~/Scripts/sample*"));

デバッグ時

デバッグ時(web.config の compilation要素の debug属性が true のとき)は、sample.debug.js が選択されます。

<script src="/Scripts/sample.debug.js"></script>

デバッグ時でないとき

デバッグ環境でないとき(web.config の compilation要素の debug属性が false のとき)は、sample.min.js が選択されます。

また、sample.min.js の内容は minify されて出力されます。今回は一つのファイルですが、複数のファイルを一つのバンドルに設定した場合は、それらのファイルを全てくっつけて一つのファイルとしてレスポンスしてくれます。

<script src="/bundles/sample?v=Rk-zyJQ66YShCJwocw4z0jCjm_jhceIZ5m55SxsnveY1"></script>

sample.js のみの場合

sample.js のみの場合を見てみます。

デバッグ時

普通に sample.js が選択されます。

<script src="/Scripts/sample.js"></script>

デバッグ時でないとき

もちろん sample.js が選択されますが、スクリプトの内容は minify されて出力されます。

<script src="/bundles/sample?v=FleCpml6dOgPyNvg3fS5cE-_9YPt2osVzl-MIlVAIPU1"></script>

sample.min.js のみの場合

sample.min.js のみだったらどうなるでしょうか。

デバッグ時

この場合注意が必要です。何も読み込まれません。 Scriptタグもレンダリングされません。

デバッグ時でないとき

sample.min.js が選択され、minify されて出力されます。

<script src="/bundles/sample?v=Rk-zyJQ66YShCJwocw4z0jCjm_jhceIZ5m55SxsnveY1"></script>

sample.debug.js のみの場合

あまりないと思いますが、sample.debug.js のみではどうなるでしょうか。

デバッグ時

sample.debug.js が選択されます。

<script src="/Scripts/sample.debug.js"></script>

デバッグ時でないとき

スクリプトタグは出力されますが、中身は空です。

<script src="/bundles/sample?v="></script>

注意点

というわけで、xxxxx.min.js のみだとデバッグ時に利用できなくなってしまうので注意が必要です。
xxxxx.js を入手して一緒に入れておくか、xxxxx.debug.js が用意されていれば合わせて入れておくとよさそうです。

この記事を書く前は、xxxxx.min.js はデバッグ時でないときは minify されずにそのまま出力されると思っていましたが、minify されるようです。そのまま出してくれてもいいような気がするんですが...

2013年1月11日金曜日

Jenkinsでのビルド時にNuGetパッケージを復元する

NuGet でパッケージ管理をしているプロジェクトを Jenkins でビルドしようとするとうまくいきませんでした。その解決方法をメモします。

うまくいかない理由

NuGet のパッケージは通常バージョン管理下に置かれません。
なのでソースコードをチェックアウトした段階ではパッケージがなく、ビルド時にパッケージを復元しないといけないことになります。

NuGetパッケージの復元を有効化

Visual Studio からパッケージの復元を設定できます。
ソリューションエクスプローラから NuGet パッケージの復元を設定できるので有効にしてあげます。

ソリューションを右クリックして、「NuGetパッケージの復元の有効化」を選択します。

こんな感じの確認ダイアログが出るのでOKします。

少し待つとソリューションフォルダが作成されてこんなファイルが追加されます。
これらを使ってビルド時にパッケージを復元してくれます。

Jenkinsでビルドしてみる

これでバッチリ!ということで Jenkins でビルドすると、今度はこんなエラーになります。

パッケージの復元は既定で無効になっています。確認のため、Visual Studio の [オプション] ダイアログ ボックスを開き、Package Manager ノードをクリックして、[NuGet がビルド中に存在しないパッケージをダウンロードするのを許可する] チェック ボックスをオンにします。また、環境変数 'EnableNuGetPackageRestore' を true に設定して確認することもできます。

メッセージの通りではありますが、ソリューションで復元を有効にするだけでは復元を実行してくれません。
サーバの環境変数「EnableNuGetPackageRestore」を true に設定すると復元してくれるようになります。

これで解決

これでパッケージが復元され、無事ビルドを行うことができます。

おまけ: アンオフィシャルなパッケージソース

NuGet公式サイトにて配布されいるパッケージは以上の通りでOKですが、もし自前の NuGet サーバを利用して、そちらからインストールしているパッケージがある場合、もうひと手間必要です。
※ 私は社内に NuGet サーバを立ててパッケージ管理していたのでちょっとハマりました。

「NuGetパッケージの復元の有効化」したときに追加された「NuGet.targets」を開きます。このファイル中に以下のような部分があります。

    <ItemGroup Condition=" '$(PackageSources)' == '' ">
        <!-- Package sources used to restore packages. By default, registered sources under %APPDATA%\NuGet\NuGet.Config will be used -->
        <!-- The official NuGet package source (https://nuget.org/api/v2/) will be excluded if package sources are specified and it does not appear in the list -->
        <!--
            <PackageSource Include="https://nuget.org/api/v2/" />
            <PackageSource Include="https://my-nuget-source/nuget/" />
        -->
    </ItemGroup>

丁寧なコメントの通りですが、自前の NuGet サーバを利用したい場合は、コメントアウトしてある箇所をコメントインして、パッケージソースを指定してやる必要があります。

    <ItemGroup Condition=" '$(PackageSources)' == '' ">
        <!-- Package sources used to restore packages. By default, registered sources under %APPDATA%\NuGet\NuGet.Config will be used -->
        <!-- The official NuGet package source (https://nuget.org/api/v2/) will be excluded if package sources are specified and it does not appear in the list -->
        <PackageSource Include="https://nuget.org/api/v2/" />
        <PackageSource Include="https://my-nuget-source/nuget/" />
    </ItemGroup>

「https://my-nuget-source/nuget/」の部分を自前のパッケージソースに変えてあげればOKですね。

2012年9月24日月曜日

Jenkins で ASP.NET のWebアプリケーションをデプロイする

Untitled Page

Jenkins を使って自動ビルドや自動テストを走らせていたので、デプロイも Jenkins からできるようにしてみました。

ちなみに ASP.NET MVC のプロジェクトにて行いました。おそらく WebForms でも同じようにできると思います。

必要なもの

Jenkins で Visual Studio のソリューションやプロジェクトをビルドするには、Jenkins の MSBuild プラグインがあると便利です。Jenkins のプラグインマネージャから簡単にインストールできます。

また、デプロイには MSDeploy を利用しますので、デプロイ先の環境に MSDeploy がインストールされている必要があります。

パッケージの作成

MSDeploy でデプロイするためのパッケージを MSBuild で簡単にパッケージを作ることができます。“ビルド手順の追加” から “Build a Visual Studio project or solution using MSBuild” をこんな感じで追加して設定。

パッケージの作成

ターゲットを “Package” にすると、MSDeploy を利用して配置するためのパッケージが ”obj\Release\Package” のようなパスに作成されます。

ちなみにターゲット “Package” が必要なので、パッケージの作成は .NET Framework 4 の MSBuild で行う必要があります。たぶん…

ステージングとかプロダクションとかそれぞれの環境に応じたビルド構成(Staging, Production, etc)や構成変換ファイル(Web.Staging.config とか)がある場合は、Configuration=Staging とかでビルドすればOKです。

MSDeploy

MSDeploy については、こちらのMSDNの記事(http://msdn.microsoft.com/ja-jp/library/dd483479(v=vs.100).aspx)が詳しいです。

先ほどの MSBuild で作られたパッケージの中に、”プロジェクト名.deploy.cmd” というコマンドファイルが生成されています。(obj\Release\Package\Sample.Mvc.deploy.cmd みたいな)
これを実行すると配置が実行されます。が、その前に配置先の設定や接続文字列を設定したりする必要があります。

※ ちなみに “プロジェクト名.deploy-readme.txt” みたいなファイルが一緒に生成されています。非常に丁寧な解説が載っていますのでぜひ目を通して下さい。

配置先と接続文字列の設定

先ほどのコマンドファイルと同じディレクトリに、”プロジェクト名.SetParameters.xml” という下のようなXMLファイルが作られます。デプロイのための設定などはこのファイルを利用して指定します。

SetParametersの中身

配置先は、”IIS Web Application Name” パラメータで指定します。value を配置先のアプリケーションにします。接続文字列も同じように設定してやります。

このパラメータファイルの設定ですが、私はあらかじめステージング用とかプロダクション用などそれぞれ用意しておいて、MSDeploy を走らせる前に上書きしています。

MSDeploy の実行

パラメータファイルが用意できたら、あとはこんな感じで実行するだけです。

Sample.Mvc.deploy.cmd /Y

この/Yパラメータを指定すると、実際にパッケージがデプロイされます。/T パラメータなど指定することで、パッケージをデプロイせずにシミュレートだけを行ったりできます。(詳しい解説が上記の deploy-readme.txt に載っています

MSDeploy の実行を Jenkins に設定する

上記の MSDeploy の実行手順を Jenkins に設定しましょう。
ビルド手順を追加からWindowsバッチコマンドの実行を追加します。

たぶんこんな感じの内容になると思います。


まとめ

とても簡単です。あとは Jenkins でビルドを実行すればめでたく配置されるはずです。

最後に

このようにデプロイを自動化することで、継続的インテグレーションが捗ります。
ASP.NET でのこういった記事が少ない気がしたので書いてみました。(もしかして当たり前すぎるんでしょうか…)

私の環境では、ステージングにはビルドマシンからアクセスできるため、紹介したような方法でデプロイしていますが、プロダクションは別のネットワークにあるため直接デプロイできません。

なので、プロダクション用のパッケージを作ってZIPファイルで圧縮してダウンロードできるようにし、実際のデプロイではそれをプロダクション環境に持って行って解凍、先ほどのコマンドファイルを実行しています。

ビルドトリガでSCMをポーリングしてやれば、コミットした内容がすぐにデプロイされるのでいろいろな環境を素早く最新の状態に保てます。

Jenkinsさんのおかげで面倒なビルドやデプロイの作業から開放されてずいぶん楽になりました。これからも Jenkins さんと仲良くやって行きたいと思います。

2012年7月17日火曜日

[ASP.NET MVC]検証周りの設計

ASP.NET MVC では、ModelState を利用してビューに検証結果を表示したり、コントローラ側からエラーを追加したりできます。

ModelBinder を使えば、DataAnnotation で面倒な検証作業を省略でき、エラーを ModelState に手動で追加する必要もなく、とても素晴らしいフレームワークです。

でも、実際に作ってみると躓く(というか躓いた)のが、モデル内部の検証結果をどのようにビューへ伝達するか、でした。

ASP.NET MVC の検証機構と、モデルでの検証

まず ModelBinder や ModelState はあくまでも、コントローラとビュー側の仕組みです。うまく設計されたMVCアーキテクチャであれば、モデルは単体で独立していて、コントローラやビューに依存していないはずです。

そうなると、モデルはしっかり内部に検証機構を備える必要があります。コントローラやビューが変わっても、ちゃんと動作する必要がありますから。

ASP.NET MVC は ビューとコントローラについてはフレームワークとしてサポートしていますが、モデルについては開発者が自身で設計していかないといけません。

どうしたか

検証自体の実装は省略します。DataAnnotation を使ったり、オリジナルの検証機構を実装したりしてみました。個人的には DataAnnotation がいいと思います。基本的な検証なら用意された検証属性を使うだけで実装できますし、カスタム検証属性を作れば、多様なケースに対応できます。

肝心なのは検証結果をいかにビューまで伝達するかです。私が最初に試したのは…

ModelState をモデルに渡す

最初はよくわからずにこうなりました。ModelState をビューとコントローラ、モデルまで一貫した検証結果のコンテナとして利用しました。

しかし ModelState は ASP.NET MVC の仕組みの一つなので、それをモデルに入れてしまうのはなんとも言えない気持ち悪さ、ビューやコントローラへの依存が生まれてしまいます。

検証結果のコンテナを自前で作る

次は自前で検証結果を扱うコンテナを用意しました。モデルが返してくるこの独自の検証結果のコンテナを、コントローラ側で ModelState にマップしてビューへ伝達します。

この方法は最初うまくいったように思いました。でも大きな問題が。すごく面倒です。

また、最終的には ModelState を利用するので、ModelState を意識せざるを得なくなり、最初の試みとだんだん大差なくなってしまいました。

モデルは特別なことをしない

最終的にどうなったかというと…

例外結果のコンテナの仕組みをモデルから全部排除しました。モデルで検証に失敗したら単に例外をスローするだけです。つまり、モデルでの検証はデータを守る最後の砦としてのみ機能させるということです。

その代わり、ModelBinder による検証を充実させ、モデルでの処理の際、極力検証に失敗しないように作ることにしました。ユーザの通常の操作の範囲なら、ModelBinder による検証で十分拾えると思います。

ModelBinder による検証が充実していれば、モデルでの検証が失敗するケースは、特殊なケースになります。例えば悪意ある操作などです。

そんな特殊なケースまで丁寧に検証結果を表示したりする必要はないということにしてしまいました:)

こうすることでモデルの作りがシンプルに保たれ、モデルは必要最小限の仕事に集中すればよくなり、随分すっきりしたように思います。

まとめ?

結局モデルの検証結果を伝達するのをやめるという、なんとも身も蓋もない結論になってしまいましたが、個人的にいろいろ試したところでは、これがいちばんしっくりきました。

ModelBinder は非常に優れた仕組みなので、カスタムバインダや検証属性を作って有効活用しましょう!

2012年4月18日水曜日

DisplayNameを表示するHtmlHelperの拡張メソッド

たぶん誰もが思いつくであろう、DisplayName属性の値を表示するHtmlHelperの拡張メソッドです。

どうして標準でないんだろう。あれ、知らないだけなのか...?


2012年4月14日土曜日

ASP.NET MVC ひとり反省会

最近、ちょこちょこASP.NET MVC(主に2)の開発を経験して、失敗とそれに対する自分なりの正解を覚え書きします。

今まで試行錯誤とWebで先輩方の記事を読んだり、ソースコードを読んだりして、頭の中だけでやっていたんですが、やっぱり少しづつでも文章にして、整理していかないといけないと思ったので。



反省と対策


まずURL、URLをしっかり考える

ちゃんとURLを設計してから、組み立てる。


なぜ

しっかりしたURL設計がないと、ルーティングやコントローラの構成が破綻してしまいがち。
あとでこれを調整するのは辛い作業だった。


どうする

それぞれの機能をちゃんと考えて、相応しいURLを設計する。全てはそれから。


ビューのモデル(ビューモデル)は、ビュー毎に作成する

似た構成のビューのモデルは、つい共用してしまいがちだけど...

なぜ

片方に仕様変更が入った時、もう片方にも影響が出て大変。
この歪を下手に吸収しようとすると、どんどんワケのわからない状態になって、分けておいたほうがよほど楽だったということになってしまった。


どうする

最初からビューごとにビューモデルを分けて作っていく。


追記(2012/04/17)

@onosさんにご指摘頂いたので追記です。
単にビューごとにビューモデルを分けて作るのではなく、どこを切り離し、どこを共通化するのか、しっかり設計する必要がある。

例えば、検証周りの設定は単に分割して作ってしまうと、同じ設定を多方でバラバラに設定することになってしまう可能性がある。
そういった共通化と分離のバランスを取っていく。



クライアント側の検証と、サーバ側(ドメインモデル)の検証は別に考える

検証のやり方はともかく、これは混同しないほうがいいと思った。


なぜ

そもそも、クライアント側とサーバ側とでは、検証の目的が違うはず。
クライアント側は、ユーザビリティの向上が主な目的。
サーバ側は、データを守ることが主な目的。


どうする

検証の設定を無理に共有させようとしない。
それぞれのコンテキストに必要十分な検証を別々に設定する。また、各検証の目的を曖昧にしない。


ドメインのエンティティは、コントローラやビューに露出させない


なぜ

エンティティの制御はモデル内で完結しないと、何が起こるのかわからなくなってしまう。
また、ビューでエンティティを使っていると、エンティティがビューに依存していってしまう。
そうして、ビジネスロジックとは関係ないモノがモデル内で増殖してしまった。


どうする

モデルのアウトラインとなる、サービス層のインターフェイスにエンティティを使用しない。
DTO的なものを、それぞれのコンテキスト別に用意し、それにエンティティのデータを移して使う。
AutoMapperはとても便利。


まとめ

ふと書いておこうと思いついて、パッとでてくるものだけ殴り書きです。
こうやってベストプラクティス的なものを構築していくのは楽しい。

Factorio: Space Exploration クリア記録

 工場建設クラフトゲーム、Factorio の MOD である Space Exploration のクリア記録です。 はじめに プレイ時間は約 350 時間、2023年10月から2025年2月にかけて15ヶ月間に及びました。この期間中3人の友人と毎週末、工場勤務に明け暮れました...