IValidatableObjectを使用するにはどうすればよいですか?


182

という事は承知しています IValidatableObjectは、プロパティを互いに比較できるようにオブジェクトを検証するために使用されることいます。

個別のプロパティを検証するための属性は引き続き必要ですが、特定のケースでは一部のプロパティの失敗を無視したいと思います。

以下の場合、それを間違って使用しようとしていますか?そうでない場合、これをどのように実装しますか?

public class ValidateMe : IValidatableObject
{
    [Required]
    public bool Enable { get; set; }

    [Range(1, 5)]
    public int Prop1 { get; set; }

    [Range(1, 5)]
    public int Prop2 { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (!this.Enable)
        {
            /* Return valid result here.
             * I don't care if Prop1 and Prop2 are out of range
             * if the whole object is not "enabled"
             */
        }
        else
        {
            /* Check if Prop1 and Prop2 meet their range requirements here
             * and return accordingly.
             */ 
        }
    }
}

回答:


168

まず、@ paper1337に適切なリソースを紹介してくれてありがとう。

これが私がやろうとしていたことを達成する方法です。

検証可能なクラス:

public class ValidateMe : IValidatableObject
{
    [Required]
    public bool Enable { get; set; }

    [Range(1, 5)]
    public int Prop1 { get; set; }

    [Range(1, 5)]
    public int Prop2 { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        var results = new List<ValidationResult>();
        if (this.Enable)
        {
            Validator.TryValidateProperty(this.Prop1,
                new ValidationContext(this, null, null) { MemberName = "Prop1" },
                results);
            Validator.TryValidateProperty(this.Prop2,
                new ValidationContext(this, null, null) { MemberName = "Prop2" },
                results);

            // some other random test
            if (this.Prop1 > this.Prop2)
            {
                results.Add(new ValidationResult("Prop1 must be larger than Prop2"));
            }
        }
        return results;
    }
}

使用する Validator.TryValidateProperty()検証に失敗した場合、すると結果のコレクションに追加されます。検証に失敗しなかった場合、成功を示す結果コレクションには何も追加されません。

検証を行う:

    public void DoValidation()
    {
        var toValidate = new ValidateMe()
        {
            Enable = true,
            Prop1 = 1,
            Prop2 = 2
        };

        bool validateAllProperties = false;

        var results = new List<ValidationResult>();

        bool isValid = Validator.TryValidateObject(
            toValidate,
            new ValidationContext(toValidate, null, null),
            results,
            validateAllProperties);
    }

validateAllPropertiesこのメソッドを機能させるには、false に設定することが重要です。validateAllPropertiesがfalseの場合、[Required]属性を持つプロパティのみがチェックされます。これにより、IValidatableObject.Validate()メソッドは条件付き検証を処理できます。


これを使用するシナリオは考えられません。これを使用する場所の例を教えていただけますか?
Stefan Vasiljevic 2016

テーブルに追跡列がある場合(それを作成したユーザーなど)。データベースでは必須ですが、コンテキスト内のSaveChangesに入力してデータを設定します(開発者が明示的に設定することを覚えておく必要はありません)。もちろん、保存する前に検証します。したがって、「作成者」列を必須としてマークせず、他のすべての列/プロパティに対して検証します。
MetalPhoenix

このソリューションの問題は、オブジェクトを適切に検証するために呼び出し元に依存していることです。
cocogza 2017

この回答を拡張するには、リフレクションを使用して、検証属性を持つすべてのプロパティを検索してから、TryValidatePropertyを呼び出します。
Paul

78

Validatorを使用した検証オブジェクトとプロパティに関するJeff Handleyのブログ投稿からの引用:

オブジェクトを検証する場合、Validator.ValidateObjectで次のプロセスが適用されます。

  1. プロパティレベルの属性を検証する
  2. バリデーターが無効な場合、検証を中止して失敗を返します
  3. オブジェクトレベルの属性を検証する
  4. バリデーターが無効な場合、検証を中止して失敗を返します
  5. デスクトップフレームワーク上にあり、オブジェクトがIValidatableObjectを実装している場合は、そのValidateメソッドを呼び出して失敗を返します。

これは、ステップ#2で検証が中止されるため、実行しようとしていることはそのままでは機能しないことを示しています。組み込みの属性を継承する属性を作成し、通常の検証を実行する前に(インターフェースを介して)有効なプロパティの存在を具体的に確認することもできます。または、エンティティを検証するためのすべてのロジックをValidateメソッドに配置することもできます。


36

いくつかのポイントを追加するだけです:

ので、Validate()メソッドシグネチャ戻りIEnumerable<>yield returnレイジー結果を生成するために使用することができる-これは有益で検証チェックのいくつかは、IOまたはCPU集約である場合。

public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
    if (this.Enable)
    {
        // ...
        if (this.Prop1 > this.Prop2)
        {
            yield return new ValidationResult("Prop1 must be larger than Prop2");
        }

また、を使用MVC ModelStateしている場合は、検証結果の失敗をModelState次のようにエントリに変換できます(これは、カスタムモデルバインダーで検証を行う場合に役立ちます)。

var resultsGroupedByMembers = validationResults
    .SelectMany(vr => vr.MemberNames
                        .Select(mn => new { MemberName = mn ?? "", 
                                            Error = vr.ErrorMessage }))
    .GroupBy(x => x.MemberName);

foreach (var member in resultsGroupedByMembers)
{
    ModelState.AddModelError(
        member.Key,
        string.Join(". ", member.Select(m => m.Error)));
}

良いですね!Validateメソッドで属性とリフレクションを使用する価値はありますか?
Schalk 2015

4

検証用の一般的な使用法の抽象クラスを実装しました

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace App.Abstractions
{
    [Serializable]
    abstract public class AEntity
    {
        public int Id { get; set; }

        public IEnumerable<ValidationResult> Validate()
        {
            var vResults = new List<ValidationResult>();

            var vc = new ValidationContext(
                instance: this,
                serviceProvider: null,
                items: null);

            var isValid = Validator.TryValidateObject(
                instance: vc.ObjectInstance,
                validationContext: vc,
                validationResults: vResults,
                validateAllProperties: true);

            /*
            if (true)
            {
                yield return new ValidationResult("Custom Validation","A Property Name string (optional)");
            }
            */

            if (!isValid)
            {
                foreach (var validationResult in vResults)
                {
                    yield return validationResult;
                }
            }

            yield break;
        }


    }
}

1
名前付きパラメーターを使用するこのスタイルが大好きで、コードを非常に読みやすくしています。
drizin 2017

0

受け入れられた回答の問題は、オブジェクトが適切に検証されるかどうかが呼び出し元に依存するようになったことです。RangeAttributeを削除してValidateメソッド内で範囲検証を行うか、必要なプロパティの名前をコンストラクターの引数として取るRangeAttributeをサブクラス化するカスタム属性を作成します。

例えば:

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
class RangeIfTrueAttribute : RangeAttribute
{
    private readonly string _NameOfBoolProp;

    public RangeIfTrueAttribute(string nameOfBoolProp, int min, int max) : base(min, max)
    {
        _NameOfBoolProp = nameOfBoolProp;
    }

    public RangeIfTrueAttribute(string nameOfBoolProp, double min, double max) : base(min, max)
    {
        _NameOfBoolProp = nameOfBoolProp;
    }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        var property = validationContext.ObjectType.GetProperty(_NameOfBoolProp);
        if (property == null)
            return new ValidationResult($"{_NameOfBoolProp} not found");

        var boolVal = property.GetValue(validationContext.ObjectInstance, null);

        if (boolVal == null || boolVal.GetType() != typeof(bool))
            return new ValidationResult($"{_NameOfBoolProp} not boolean");

        if ((bool)boolVal)
        {
            return base.IsValid(value, validationContext);
        }
        return null;
    }
}

0

base.IsValidを呼び出すと、IsValidメソッドに何度も入るため、スタックオーバーフロー例外が発生することを除いて、cocogzaの回答が好きでした。そこで、特定の種類の検証用に変更しました。私の場合は、電子メールアドレス用でした。

[AttributeUsage(AttributeTargets.Property)]
class ValidEmailAddressIfTrueAttribute : ValidationAttribute
{
    private readonly string _nameOfBoolProp;

    public ValidEmailAddressIfTrueAttribute(string nameOfBoolProp)
    {
        _nameOfBoolProp = nameOfBoolProp;
    }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        if (validationContext == null)
        {
            return null;
        }

        var property = validationContext.ObjectType.GetProperty(_nameOfBoolProp);
        if (property == null)
        {
            return new ValidationResult($"{_nameOfBoolProp} not found");
        }

        var boolVal = property.GetValue(validationContext.ObjectInstance, null);

        if (boolVal == null || boolVal.GetType() != typeof(bool))
        {
            return new ValidationResult($"{_nameOfBoolProp} not boolean");
        }

        if ((bool)boolVal)
        {
            var attribute = new EmailAddressAttribute {ErrorMessage = $"{value} is not a valid e-mail address."};
            return attribute.GetValidationResult(value, validationContext);
        }
        return null;
    }
}

これははるかにうまくいきます!クラッシュせず、素敵なエラーメッセージが表示されます。これが誰かを助けることを願っています!


0

iValidateについて私が気に入らないのは、他のすべての検証の後にのみ実行されるように見えることです。
さらに、少なくとも私たちのサイトでは、保存の試行中に再度実行されます。関数を作成し、その中にすべての検証コードを配置することをお勧めします。または、Webサイトの場合は、モデルの作成後にコントローラーで「特別な」検証を行うことができます。例:

 public ActionResult Update([DataSourceRequest] DataSourceRequest request, [Bind(Exclude = "Terminal")] Driver driver)
    {

        if (db.Drivers.Where(m => m.IDNumber == driver.IDNumber && m.ID != driver.ID).Any())
        {
            ModelState.AddModelError("Update", string.Format("ID # '{0}' is already in use", driver.IDNumber));
        }
        if (db.Drivers.Where(d => d.CarrierID == driver.CarrierID
                                && d.FirstName.Equals(driver.FirstName, StringComparison.CurrentCultureIgnoreCase)
                                && d.LastName.Equals(driver.LastName, StringComparison.CurrentCultureIgnoreCase)
                                && (driver.ID == 0 || d.ID != driver.ID)).Any())
        {
            ModelState.AddModelError("Update", "Driver already exists for this carrier");
        }

        if (ModelState.IsValid)
        {
            try
            {
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.