Bagaimana cara saya menggunakan IValidatableObject?

182

Saya mengerti bahwa IValidatableObjectini digunakan untuk memvalidasi objek dengan cara yang memungkinkan satu membandingkan properti terhadap satu sama lain.

Saya masih ingin memiliki atribut untuk memvalidasi properti individual, tetapi saya ingin mengabaikan kegagalan pada beberapa properti dalam kasus tertentu.

Apakah saya mencoba menggunakannya secara tidak benar dalam kasus di bawah ini? Jika tidak, bagaimana saya menerapkan ini?

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.
             */ 
        }
    }
}
zrg
sumber

Jawaban:

168

Pertama, terima kasih kepada @ paper1337 karena mengarahkan saya ke sumber yang tepat ... Saya tidak terdaftar sehingga saya tidak dapat memilihnya, silakan lakukan jika ada orang lain yang membaca ini.

Inilah cara mencapai apa yang saya coba lakukan.

Kelas yang sah:

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;
    }
}

Menggunakan Validator.TryValidateProperty()akan menambah koleksi hasil jika ada validasi gagal. Jika tidak ada validasi yang gagal maka tidak ada yang akan ditambahkan ke koleksi hasil yang merupakan indikasi keberhasilan.

Melakukan validasi:

    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);
    }

Penting untuk disetel validateAllPropertieske false agar metode ini berfungsi. Ketika validateAllPropertiessalah, hanya properti dengan [Required]atribut yang diperiksa. Ini memungkinkan IValidatableObject.Validate()metode menangani validasi bersyarat.

zrg
sumber
Saya tidak bisa memikirkan skenario di mana saya akan menggunakan ini. Bisakah Anda memberi saya contoh di mana Anda akan menggunakan ini?
Stefan Vasiljevic
Jika Anda memiliki kolom pelacakan di tabel Anda (seperti pengguna yang membuatnya). Ini diperlukan dalam database tetapi Anda masuk dalam SaveChanges dalam konteks untuk mengisinya (menghilangkan kebutuhan pengembang untuk mengingat untuk mengaturnya secara eksplisit). Anda tentu saja akan memvalidasi sebelum menabung. Jadi, Anda tidak menandai kolom "pembuat" seperti yang diminta tetapi memvalidasi terhadap semua kolom / properti lainnya.
MetalPhoenix
Masalah dengan solusi ini adalah bahwa sekarang Anda bergantung pada pemanggil objek Anda untuk divalidasi dengan benar.
cocogza
Untuk meningkatkan jawaban ini, orang dapat menggunakan refleksi untuk menemukan semua properti yang memiliki atribut validasi, lalu panggil TryValidateProperty.
Paul Chernoch
78

Kutipan dari Posting Blog Jeff Handley tentang Objek Validasi dan Properti dengan Validator :

Saat memvalidasi objek, proses berikut ini diterapkan di Validator.ValidateObject:

  1. Validasi atribut tingkat properti
  2. Jika ada validator yang tidak valid, batalkan validasi mengembalikan kegagalan
  3. Validasi atribut tingkat objek
  4. Jika ada validator yang tidak valid, batalkan validasi mengembalikan kegagalan
  5. Jika pada kerangka kerja desktop dan objek mengimplementasikan IValidatableObject, maka panggil metode Validasinya dan kembalikan setiap kegagalan

Ini menunjukkan bahwa apa yang Anda coba lakukan tidak akan berhasil karena validasi akan dibatalkan pada langkah # 2. Anda bisa mencoba membuat atribut yang mewarisi dari bawaan dan secara khusus memeriksa keberadaan properti yang diaktifkan (melalui antarmuka) sebelum melakukan validasi normal. Atau, Anda bisa meletakkan semua logika untuk memvalidasi entitas dalam Validatemetode.

Teriakan Chris
sumber
36

Hanya untuk menambahkan beberapa poin:

Karena Validate()metode tanda tangan kembali IEnumerable<>, yang yield returndapat digunakan untuk menghasilkan hasil malas - ini bermanfaat jika beberapa pemeriksaan validasi IO atau CPU intensif.

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

Selain itu, jika Anda menggunakan MVC ModelState, Anda dapat mengonversi kegagalan hasil validasi menjadi ModelStateentri sebagai berikut (ini mungkin berguna jika Anda melakukan validasi dalam binder model kustom ):

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)));
}
StuartLC
sumber
Yang bagus! Apakah layak menggunakan atribut dan refleksi dalam metode Validasi?
Schalk
4

Saya menerapkan kelas abstrak penggunaan umum untuk validasi

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;
        }


    }
}
guneysus
sumber
1
Saya suka gaya menggunakan parameter bernama, membuat kode lebih mudah dibaca.
drizin
0

Masalah dengan jawaban yang diterima adalah bahwa itu sekarang tergantung pada pemanggil untuk objek yang akan divalidasi dengan benar. Saya akan menghapus RangeAttribute dan melakukan validasi rentang di dalam metode Validasi atau saya akan membuat atribut kustom subclassing RangeAttribute yang mengambil nama properti yang diperlukan sebagai argumen pada konstruktor.

Sebagai contoh:

[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;
    }
}
cocogza
sumber
0

Saya menyukai jawaban cocogza kecuali basis panggilan itu. IsValid menghasilkan pengecualian stack overflow karena akan memasukkan kembali metode IsValid berulang kali. Jadi saya memodifikasinya untuk jenis validasi tertentu, dalam kasus saya itu untuk alamat email.

[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;
    }
}

Ini bekerja lebih baik! Itu tidak crash dan menghasilkan pesan kesalahan yang bagus. Semoga ini bisa membantu seseorang!

rjacobsen0
sumber
0

Hal yang saya tidak suka tentang iValidate adalah sepertinya hanya menjalankan SETELAH semua validasi lainnya.
Selain itu, setidaknya di situs kami, itu akan berjalan lagi selama upaya penyelamatan. Saya sarankan Anda cukup membuat fungsi dan menempatkan semua kode validasi Anda di dalamnya. Sebagai alternatif untuk situs web, Anda dapat memiliki validasi "khusus" di dalam pengontrol setelah model dibuat. Contoh:

 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
            {
John Lord
sumber