かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

DataAnnotationsの属性を自分で手の入れれないコードに対して追加する方法

DataAnnotationsあたりを使うとプロパティに属性とかを付けることで入力値の検証が楽に出来るようになります。でも、Entity Frameworkで生成されるコードやWCFでサービス参照の追加をしたときに自動生成されるコードに属性を付けると、再生成のタイミングで消されてしまいます。


DynamicDataやWCF RIA Servicesでは、MetadataType属性を使うことで、その問題に対して対処しています。ということでDynamicDataやWCF RIA Servicesじゃない環境でMetadataTypeを使ってみてもいいんじゃないか?と思うので試してみます。

EntityFrameworkやWCFを使うとコードが煩雑になるので、コンソールアプリケーションで疑似的にやってます。

プロジェクトの作成

MetadataTypeTestという名前でコンソールアプリケーションを作成します。そして、System.ComponentModel.DataAnnotationsを参照に追加します。

自動生成されたっぽいクラスの作成

Entity FrameworkやWCFのサービス参照をやると説明がめんどくさいのでPersonというクラスを新規作成して、こいつを自動生成されたものと見立ててやることにします。
Personクラスは以下のような内容にしました。こいつには、これ以上手を入れれないものとして進めていきます。

namespace MetadataTypeTest
{
    public partial class Person
    {
        public string Name { get; set; }
    }
}

メタデータクラスの作成

自動生成されたクラスにみたてたPersonクラスのメタデータを作っていきます。ここらへんは、WCF RIA ServicesとかDynamicDataの記事をあさればわんさか出てくるので説明は省略します。とりあえずNameプロパティを必須入力項目になるようにしました。

namespace MetadataTypeTest
{
    using System.ComponentModel.DataAnnotations;

    [MetadataType(typeof(Person.Metadata))]
    public partial class Person
    {
        class Metadata
        {
            [Required]
            public object Name { get; set; }
        }
    }
}

メタデータが有効になってるか確認

メタデータも設定したのでMainメソッドでデータの検証をやるコードを書いて動作を確認します。書いたコードは、Validatorを使ってエラーが出るかどうかを見ているだけの単純なコードです。

namespace MetadataTypeTest
{
    using System;
    using System.ComponentModel.DataAnnotations;

    class Program
    {
        static void Main(string[] args)
        {
            // Nameプロパティは初期状態でNullなので検証でひっかかるはず
            var p = new Person();

            try
            {
                Validator.ValidateObject(
                    p,
                    new ValidationContext(p, null, null));
                Console.WriteLine("検証エラー無し");
            }
            catch (ValidationException e)
            {
                // エラーメッセージを出力
                Console.WriteLine(e.ValidationResult.ErrorMessage);
            }
        }
    }
}

これを実行するとエラーメッセージが出力されるのを期待しているのですが、残念ながら検証エラー無しと表示されてしまいます。以下に実行結果を示します。

検証エラー無し
続行するには何かキーを押してください . . .

AssociatedMetadataTypeTypeDescriptionProviderの使用

ということで、DynamicDataやWCF RIA Services以外では使えないのか??という話になるのですが、そこらへんは便利なクラスが実は用意されてました。AssociatedMetadataTypeTypeDescriptionProviderというクラスを使えばMetadataTypeで指定した内容が、ちゃんと有効になるように細工してくれるみたいです。
このクラスは、名前からもわかるようにTypeDescriptionProviderです。PersonクラスのTypeDescriptionProviderをAssociatedMetadataTypeTypeDescriptionProviderにしてやればOKです。

ただ、TypeDescriptionProvider属性で指定するのではなく、コードから指定するやりかたで適用します。これはAssociatedMetadataTypeTypeDescriptionProviderにデフォルトのコンストラクタが用意されてないという理由からです。AssociatedMetadataTypeTypeDescriptionProviderを継承してデフォルトのコンストラクタを用意してやればTypeDescriptionProvider属性で指定してやることも出来ますが、コードで指定したほうが後々便利なこともあるので、とりあえずコードでやります。

namespace MetadataTypeTest
{
    using System;
    using System.ComponentModel;
    using System.ComponentModel.DataAnnotations;

    class Program
    {
        static Program()
        {
            // PersonクラスのTypeDescriptionProviderを設定する
            TypeDescriptor.AddProviderTransparent(
                new AssociatedMetadataTypeTypeDescriptionProvider(typeof(Person)),
                typeof(Person));
        }

        static void Main(string[] args)
        {
            // Nameプロパティは初期状態でNullなので検証でひっかかるはず
            var p = new Person();

            try
            {
                Validator.ValidateObject(
                    p,
                    new ValidationContext(p, null, null));
                Console.WriteLine("検証エラー無し");
            }
            catch (ValidationException e)
            {
                // エラーメッセージを出力
                Console.WriteLine(e.ValidationResult.ErrorMessage);
            }
        }
    }
}

Programクラスのstaticコンストラクタで指定してるコードがポイントです。ここでTypeDescriptionProviderを設定してます。
この状態で実行すると以下のような結果になります。

Name フィールドが必要です。
続行するには何かキーを押してください . . .

ちゃんと目的通り、DataAnnotationsを自分でいじれないコードに対しても適用することが出来るようになりました。因みに、このAssociatedMetadataTypeTypeDescriptionProviderクラスは.NET Framework 3.5 SP1と.NET Framework 4でしか使えません。Silverlight無いのが残念な気もしますがSilverlightにはWCF RIA Servicesがあるのでいいでしょう多分。

1つ1つクラスにTypeDescriptionProviderを指定するのがメンドクサク無い?

ここで、1つ心配なのが100個自動生成されるクラスがあったら100クラスぶんTypeDescriptionProviderを登録しないといけないのか?というのがあります。これは、ちょっとした方法で回避することが出来ます。

Entity Frameworkとかで自動生成されるクラスは、大体基底クラスがあったりします。(無ければpartial classで自分でつければいいですね)この基底クラスのほうにTypeDescriptionProviderを登録してやることで、継承先のクラスにも一括でTypeDescriptionProviderを登録することが出来ます。

ということで、早速やってみました。自動生成されるという設定のPersonクラスを以下のようにします。

namespace MetadataTypeTest
{
    // 自動生成されるクラスの継承元になるクラスのつもり
    public class EntityObject { }

    public partial class Person : EntityObject
    {
        public string Name { get; set; }
    }
}

そして、TypeDescriptionProviderを登録するコードをEntityObjectに対してTypeDescriptionProviderを登録するように変更します。

static Program()
{
    // EntityObjectクラスのTypeDescriptionProviderを設定する
    TypeDescriptor.AddProviderTransparent(
        new AssociatedMetadataTypeTypeDescriptionProvider(typeof(EntityObject)),
        typeof(EntityObject));
}

この状態で、実行してもいい感じに動いてくれます。実行結果を以下に示します。

Name フィールドが必要です。
続行するには何かキーを押してください . . .

ということで、自動生成されるクラスの基底クラスにTypeDescriptionProviderを登録してやることで、何百個クラスが生成されても登録処理は1つだけで済みます。(DataAnnotationsの指定は何百クラスぶんやらないといけないですが・・・)

まとめ

自動生成されるクラスについてDataAnnotationsの属性を追加したい時は、MetadataTypeのほかにAssociationMetadataTypeTypeDescriptionProviderを基底クラスに対して登録してやればいい。ということになりました。