2012年10月20日土曜日

[C#]IDataReader をちょっと使いやすくする

IDataReader を使うときそのままだとこんな感じだと思うんですが、

int intValue = (int) reader["int_field"];
string strValue = (string) reader["str_field"];
 

NULL のときとかちょっと面倒です。(なんで IDataReader.IsDBnull には IsDBNull(string name) みたいなオーバーロードがないんだろう

Nullable<int> nullableValue = null;
if (!reader.IsDBNull(reader.GetOrdinal("nullable_field")))
{
    nullableValue = (int) reader["nullable_field"];
}
 

なのでこんな風にできるようにしたい。

int intValue = reader.Field<int>("int_field");
string strValue = reader.Field<string>("str_field");
Nullable<int> nullableValue = reader.Field<Nullable<int>>("nullable_field");
int ifNullValue = reader.Field<int>("if_null_field", ifNull:-1);
 

3.5以上なら拡張メソッド作ればいいんですが... 残念ながら 2.0 な環境だったのでこんなヘルパクラスを作って実装しました。

まあ実際は VB.NET なんですが... これで IDataReader が少し使いやすくなりました。

2012年10月3日水曜日

[C#]式木を触ってみる

メタプログラミングには以前から興味があったので、ぜひ式木について知りたい!というわけで。
こんなケースで考えてみました。

Equals と GetHashCode の実装

Equals と GetHashCode を実装するとき、大抵はプロパティや内部フィールドの比較を繋げるだけのことが多いので、その式を作ってくれる EqualityComparer があると嬉しいなあ。
※ まあ ReSharper があれば自動で実装してくれますが…

たぶんこんなクラスがあったら、


class Version
{
    public Version(int major, int minor)
    {
        Major = major;
        Minor = minor;
    }

    public int Major { get; set; }

    public int Minor { get; set; }
}

こんな感じに実装すると思います。

class Version
{
    public Version(int major, int minor)
    {
        Major = major;
        Minor = minor;
    }

    public int Major { get; set; }

    public int Minor { get; set; }

    public override bool Equals(object obj)
    {
        if (ReferenceEquals(null, obj)) return false;
        if (ReferenceEquals(this, obj)) return true;
        if (obj.GetType() != this.GetType()) return false;
        return Equals((Version) obj);
    }

    private bool Equals(Version other)
    {
        return Major == other.Major && Minor == other.Minor;
    }

    public override int GetHashCode()
    {
        return Major.GetHashCode() ^ Minor.GetHashCode();
    }
}

これの実装を EqualityComparer に移したらこんな感じになるでしょうか。※ null判定は省略します

class VersionEqualityComparer : IEqualityComparer<Version>
{
    public bool Equals(Version x, Version y)
    {
        return x.Major == y.Major && x.Minor == y.Minor;
    }

    public int GetHashCode(Version obj)
    {
        return obj.Major.GetHashCode() ^ obj.Minor.GetHashCode();
    }
}

この EqualityComparer の Equals と GetHashCode の式を作ってくれる汎用EqualityComparer を作りたいと思います。

使い方をきめる

どんな風に使うかですが…

class Version
{
    public Version(int major, int minor)
    {
        Major = major;
        Minor = minor;
    }

    public int Major { get; set; }

    public int Minor { get; set; }

    private static readonly ComplexEqualityComparer<Version> Comparer = new ComplexEqualityComparer<Version>()
        .AddMember(v => v.Major)
        .AddMember(v => v.Minor)
        .Compile();

    public override bool Equals(object obj)
    {
        return Comparer.Equals(this, obj as Version);
    }

    public override int GetHashCode()
    {
        return Comparer.GetHashCode(this);
    }
}

こんな感じで実装に利用するプロパティやフィールドをラムダ式で指定できるようにします。

Compile メソッドを呼ぶと指定されたプロパティやフィールドを用いた式がコンパイルされて、あとは使うだけになる、という感じです。

書きやすくするために安直な感じですが、thisを返してメソッドチェーンできるようにしておきます。

メンバの指定を実装する

Equals に利用する “x.PropertyName == y.PropertyName” という式と、GetHashCode に利用する “obj.GetHashCode()” という式を作ります。

それぞれあとで “&&” や “^” でつなぐので、リストに放り込んでいきます。

    private readonly ParameterExpression _paramX = Expression.Parameter(typeof(T), "x"); // Equals(x, y) の引数 "x" を表す式
    private readonly ParameterExpression _paramY = Expression.Parameter(typeof(T), "y"); // Equals(x, y) の引数 "y" を表す式
    private readonly ParameterExpression _paramObj = Expression.Parameter(typeof(T), "obj"); // GetHashCode(obj) の引数 "obj" を表す式

    private readonly IList<BinaryExpression> _equalsList = new List<BinaryExpression>();
    private readonly IList<Expression> _getHashCodeList = new List<Expression>();

    public ComplexEqualityComparer<T> AddMember<TMember>(Expression<Func<T, TMember>> member)
    {
        if (member.Body.NodeType != ExpressionType.MemberAccess)
            throw new ArgumentException("メンバの指定はメンバアクセスの式でお願いします。");

        var memberExpression = (MemberExpression) member.Body;
        var memberInfo = memberExpression.Member;

        var memberX = Expression.PropertyOrField(_paramX, memberInfo.Name); // x.PropertyName
        var memberY = Expression.PropertyOrField(_paramY, memberInfo.Name); // y.PropertyName
        var equals = Expression.Equal(memberX, memberY); // x.PropertyName == y.PropertyName

        var paramMember = Expression.PropertyOrField(_paramObj, memberInfo.Name); // obj.PropertyName
        var getHashCode = Expression.Call(paramMember, typeof(TMember).GetMethod("GetHashCode")); // obj.PropertyName.GetHashCode()

        _equalsList.Add(equals);
        _getHashCodeList.Add(getHashCode);

        return this;
    }

式を完成させる

Complile メソッドで式を完成させて、コンパイルをかけます。

メンバの指定の際にリストに放り込んでおいた式を “&&” や “^” で繋いで組み立てます。
最後に Expression.Lamda<>() でコンパイルして出来上がったデリゲートを保存します。

    // コンパイルした式のデリゲート
    private Func<T, T, bool> _compiledEquals;
    private Func<T, int> _compiledGetHashCode;

    public ComplexEqualityComparer<T> Compile()
    {
        // x.Property1 == y.Property1 && x.Property2 == y.Property2 && ...
        BinaryExpression equalsExpression = null;
        foreach (var equals in _equalsList)
        {
            equalsExpression = equalsExpression == null
                                   ? @equals
                                   : Expression.AndAlso(equalsExpression, @equals);
        }

        if (equalsExpression == null)
            throw new InvalidOperationException();

        _compiledEquals = Expression.Lambda<Func<T, T, bool>>(equalsExpression, _paramX, _paramY).Compile();

        // obj.Property1.GetHashCode() ^ obj.Property2.GetHashCode() ^ ...
        Expression getHashCodeExpression = null;
        foreach (var getHashCode in _getHashCodeList)
        {
            getHashCodeExpression = getHashCodeExpression == null
                                        ? getHashCode
                                        : Expression.ExclusiveOr(getHashCodeExpression, getHashCode);
        }

        if (getHashCodeExpression == null)
            throw new InvalidOperationException();

        _compiledGetHashCode = Expression.Lambda<Func<T, int>>(getHashCodeExpression, _paramObj).Compile();
        
        _compiled = true;
        return this;
    } 

できあがり

だいたいこんな感じになりました。ほんとは null のチェックとかも必要ですが面倒なのでとりあえず。

public class ComplexEqualityComparer<T> : IEqualityComparer<T>
{
    public ComplexEqualityComparer()
    {
        _compiled = false;
        _paramX = Expression.Parameter(typeof(T), "x");
        _paramY = Expression.Parameter(typeof(T), "y");
        _paramObj = Expression.Parameter(typeof(T), "obj");
        _equalsList = new List<BinaryExpression>();
        _getHashCodeList = new List<Expression>();
    }

    private bool _compiled;

    private readonly ParameterExpression _paramX; // Equals(x, y) の引数 "x" を表す式
    private readonly ParameterExpression _paramY; // Equals(x, y) の引数 "y" を表す式
    private readonly ParameterExpression _paramObj; // GetHashCode(obj) の引数 "obj" を表す式

    private readonly IList<BinaryExpression> _equalsList;
    private readonly IList<Expression> _getHashCodeList;

    public ComplexEqualityComparer<T> AddMember<TMember>(Expression<Func<T, TMember>> member)
    {
        if (member.Body.NodeType != ExpressionType.MemberAccess)
            throw new ArgumentException("メンバの指定はメンバアクセスの式でお願いします。");

        var memberExpression = (MemberExpression) member.Body;
        var memberInfo = memberExpression.Member;

        var memberX = Expression.PropertyOrField(_paramX, memberInfo.Name); // x.PropertyName
        var memberY = Expression.PropertyOrField(_paramY, memberInfo.Name); // y.PropertyName
        var equals = Expression.Equal(memberX, memberY); // x.PropertyName == y.PropertyName

        var paramMember = Expression.PropertyOrField(_paramObj, memberInfo.Name); // obj.PropertyName
        var getHashCode = Expression.Call(paramMember, typeof(TMember).GetMethod("GetHashCode")); // obj.PropertyName.GetHashCode()

        _equalsList.Add(equals);
        _getHashCodeList.Add(getHashCode);

        return this;
    }

    // コンパイルした式のデリゲート
    private Func<T, T, bool> _compiledEquals;
    private Func<T, int> _compiledGetHashCode;

    public ComplexEqualityComparer<T> Compile()
    {
        // x.Property1 == y.Property1 && x.Property2 == y.Property2 && ...
        BinaryExpression equalsExpression = null;
        foreach (var equals in _equalsList)
        {
            equalsExpression = equalsExpression == null
                                   ? @equals
                                   : Expression.AndAlso(equalsExpression, @equals);
        }

        if (equalsExpression == null)
            throw new InvalidOperationException();

        _compiledEquals = Expression.Lambda<Func<T, T, bool>>(equalsExpression, _paramX, _paramY).Compile();

        // obj.Property1.GetHashCode() ^ obj.Property2.GetHashCode() ^ ...
        Expression getHashCodeExpression = null;
        foreach (var getHashCode in _getHashCodeList)
        {
            getHashCodeExpression = getHashCodeExpression == null
                                        ? getHashCode
                                        : Expression.ExclusiveOr(getHashCodeExpression, getHashCode);
        }

        if (getHashCodeExpression == null)
            throw new InvalidOperationException();

        _compiledGetHashCode = Expression.Lambda<Func<T, int>>(getHashCodeExpression, _paramObj).Compile();
        
        _compiled = true;
        return this;
    } 

    private void CompileIfNotCompiled()
    {
        if (_compiled) return;
        Compile();
    }

    public bool Equals(T x, T y)
    {
        CompileIfNotCompiled();
        return _compiledEquals(x, y);
    }

    public int GetHashCode(T obj)
    {
        CompileIfNotCompiled();
        return _compiledGetHashCode(obj);
    }
}

テスト

    [Test]
    public void UseCase()
    {
        var eqComparer = new ComplexEqualityComparer<Version>()
            .AddMember(v => v.Major)
            .AddMember(v => v.Minor)
            .Compile();

        var version_1_0 = new Version(1, 0);
        var version_2_3 = new Version(2, 3);

        Assert.IsTrue(eqComparer.Equals(version_1_0, version_1_0));
        Assert.IsTrue(eqComparer.Equals(version_2_3, version_2_3));
        Assert.IsTrue(eqComparer.Equals(version_2_3, new Version(2, 3)));
        Assert.IsTrue(eqComparer.Equals(new Version(2, 3), version_2_3));

        Assert.IsFalse(eqComparer.Equals(version_1_0, version_2_3));
        Assert.IsFalse(eqComparer.Equals(version_2_3, version_1_0));
    }

テスト結果

ちゃんと動くっぽいですね。

これで Equals と GetHashCode の実装は楽できそうです。

式木おもしろい!

式木とてもおもしろいですね。メタプログラミングって面白いなあと思います。

使い方を誤ると魔物が生まれそうですが、上手に使えばすごく役立ちそうです。(ぼくの頭ではなかなかアイデアが浮かびませんが…

参考にさせていただきました

大変参考にさせていただきました。

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年9月15日土曜日

イベント・ソーシング

たまたま「イベント・ソーシング」について知る機会があり、調べて試してみました。
非常に興味深い考え方です。



実際に動作するコードを書いてみようと、軽い気持ちでやってみたところなかなか難しくて時間がかかってしまいました。途中で別の作業をしなければならなくなったので、一旦中断...

ちなみに以下の記事が大変わかりやすいです。


また、以下に DDD, CQRS, Event Sourcing のサンプルがあります。こちらのサンプルを参考に実際にコードを書いてみています。


2012年7月23日月曜日

[C#]エンティティのEqualsとGetHashCode

しばらくの間、曖昧な理解のまま Equals と GetHashCode を実装していたので、反省。

まず、Equals と GetHashCode の実装については、以下の記事が参考になります。

概ね理解しているつもりだったんですが、今日ハッシュのコレクションを扱うときに、コレクション内のある要素を削除(Remove)しようとしても、削除できない(Contains(item) が false になってしまう)、というバグが出たので調べていたら、こちらの記事に行き当たりました。

  1. 2 つのオブジェクトが等しい (== 演算子の定義によって等しい) 場合、それらのオブジェクトからは同じハッシュ値が生成されなければならない。同じハッシュ値が生成されない場合、コンテナからオブジェクトを探しだすためのキーとしてハッシュコードを使用することができない。
  2. 任意のオブジェクト A に対して、A.GetHashCode() は各インスタンス毎に異なる値でなければならない。A のどんなメソッドが呼ばれたとしても、A.GetHashCode() は常に同じ値を返さなければならない。これによって、オブジェクトが格納されている正しいバケツを常に見つけだすことができる。
  3. ハッシュ関数は任意に入力された複数の整数値に対し、ランダム分布となる返り値を返す必要がある。この性質により、ハッシュベースのコンテナへ効率的にオブジェクトを格納できる。

この実装ポリシーのうち、2番を思いっきり無視していました。つまり、ハッシュのコレクションを生成したあとに、コレクション内のある要素の GetHashCode の演算に利用してるプロパティが変化して、ハッシュ値が変わってしまったようです。

また、以下の様な記事もありました。

 

エンティティにおける、Equals と GetHashCode の実装

今回このハッシュのコレクションというのは、いわいるエンティティのコレクションだったわけですが、その実装が以下のようなもの。

public class User
{
    public int Id { get; set; }

    public string FirstName { get; set; }

    public string LastName { get; set; }

    public override bool Equals(object obj)
    {
        var other = obj as User;
        return 
            other != null && 
            FirstName == other.FirstName && 
            LastName == other.LastName;
    }

    public override int GetHashCode()
    {
        return 
            (FirstName ?? string.Empty).GetHashCode() ^ 
            (LastName ?? string.Empty).GetHashCode();
    }
}

テーブルのほうはこんな感じです。

CREATE TABLE Users (
    Id INT IDENTITY PRIMARY KEY,
    FirstName NVARCHAR(50) NOT NULL,
    LastName NVARCHAR(50) NOT NULL,
    UNIQUE (FirstName, LastName)
)

Id が人工キーで、スキーマ上のメインの識別子になります。自然キーとして FirstName と LastName が利用できる(実際には名前は被るので使えないと思いますが…例です)ので、FirstName と LastName に一意キー制約を付けてあります。

この Equals 及び GetHashCode の実装では、以下のガイドラインの推奨に従って、人工キーではなく自然キーによる等価比較の実装を行なっています。(ちなみに ORM は NHibernate です

ビジネスキーの等価性 を使って、 equals() と hashCode() を実装することをお勧めします。ビジネスキーの等価性とは、 equals() メソッドが、ビジネスキー、つまり現実の世界においてインスタンスを特定するキー(自然 候補キー) を形成するプロパティだけを比較することを意味します。

このとき、FirstName と LastName が変化する場合、上記のような実装だとハッシュ値が変わってしまってポリシー違反を起こします。

なので、FirstName と LastName が変化する属性としての側面をもつ場合は、Equals と GetHashCode の実装に使うとマズイということになります。(FirstName と LastName が不変ならオッケーなんですが

このような場合は、素直に変化しない(はずの) Id を用いたほうがよさそうです。

public class User
{
    public int Id { get; set; }

    public string FirstName { get; set; }

    public string LastName { get; set; }

    public override bool Equals(object obj)
    {
        var other = obj as User;
        return
            other != null &&
            Id == other.Id;
    }

    public override int GetHashCode()
    {
        return Id.GetHashCode();
    }
}

本来なら自然キーでの実装のほうが、なにかと扱いやすいのですが仕方ないですね。

ちなみにこの例でも、Id のセッタが公開されてしまっているので、キチンと実装する場合は、Id が不変になるように工夫したほうがよさそうです。

2012年7月18日水曜日

[C#]Enumに振る舞いを

Enumにプロパティとかメソッドとか、豊富な振る舞いを持たせたい時どうすればいいんだろうという疑問です。

拡張メソッドを使う

public enum AircraftType
{
    Fighter,
    Attacker,
    Bomber
}

public static class AircraftTypeHelper
{
    public static string DisplayName(this AircraftType value)
    {
        return _displayNames[value];
    }

    private readonly static IDictionary<AircraftType, string> _displayNames = new Dictionary<AircraftType, string>()
    {
        { AircraftType.Fighter, "戦闘機" },
        { AircraftType.Attacker, "攻撃機" },
        { AircraftType.Bomber, "爆撃機" }
    };
}
 string displayName = AircraftType.Fighter.DisplayName();

こんな感じに拡張メソッドを使うと、プロパティは無理だけど、それなりに振る舞いをつけることができます。

素直にクラスで実装する

public class AircraftType
{
    private AircraftType(AircraftTypeValue value)
    {
        value = value;
    }

    private enum AircraftTypeValue
    {
        Fighter,
        Attacker,
        Bomber
    }

    public static readonly AircraftType Fighter = new AircraftType(AircraftTypeValue.Fighter);
    public static readonly AircraftType Attacker = new AircraftType(AircraftTypeValue.Attacker);
    public static readonly AircraftType Bomber = new AircraftType(AircraftTypeValue.Bomber);

     private readonly AircraftTypeValue _value;

    public override bool Equals(object obj)
    {
        var other = obj as AircraftType;
        return other != null && _value == other._value;
    }

    public override int GetHashCode()
    {
        return _value.GetHashCode();
    }

    public override string ToString()
    {
        return _value.ToString();
    }
}

こんな感じでシングルトンで実装する方法です。Equals とか ToString なんかの基本的なメソッドの実装は内部に隠してある Enum に丸投げ。

これならプロパティでもなんでも追加できます。(当たり前だけど...)

    public string DisplayName
    {
        get { return _displayNames[_value]; }
    }

    private readonly static IDictionary<AircraftTypeValue, string> _displayNames = new Dictionary<AircraftTypeValue, string>()
    {
        { AircraftTypeValue.Fighter, "戦闘機" },
        { AircraftTypeValue.Attacker, "攻撃機" },
        { AircraftTypeValue.Bomber, "爆撃機" }
    };
    string displayName = AircraftType.Fighter.DisplayName;

どっちがいいんだろう

拡張メソッドを使う方はまんま Enum を使うので手軽ですね。でも拡張メソッド以上のことはできないので、複雑な振る舞いを持たせたりするのには向いていないですね。

シングルトンでの実装は、自由にプロパティでもメソッドでも追加できるので柔軟性はあります。でも、実装がちょっと面倒です。

また、Enum をそのまま使うと FlagsAttributes でビット演算できたりして重宝することもあります。

個人的には後者(シングルトン)を使うことが多いのですが、振る舞いなんかがあまりない純粋な列挙値だったら普通に Enum を使います。あと、ビット演算したいときとか…

ケース・バイ・ケースなのかな?

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

Factorio: Space Exploration クリア記録

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