Làm cách nào để sử dụng IValiditableObject?


182

Tôi hiểu rằng nó IValidatableObjectđược sử dụng để xác nhận hợp lệ một đối tượng theo cách cho phép một người so sánh các thuộc tính với nhau.

Tôi vẫn muốn có các thuộc tính để xác thực các thuộc tính riêng lẻ, nhưng tôi muốn bỏ qua các lỗi trên một số thuộc tính trong một số trường hợp nhất định.

Tôi đang cố gắng sử dụng nó không chính xác trong trường hợp dưới đây? Nếu không làm thế nào để tôi thực hiện điều này?

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

Câu trả lời:


168

Trước hết, cảm ơn @ paper1337 vì đã chỉ cho tôi đúng tài nguyên ... Tôi chưa đăng ký nên tôi không thể bỏ phiếu cho anh ấy, vui lòng làm như vậy nếu có ai đọc được điều này.

Đây là cách thực hiện những gì tôi đã cố gắng làm.

Lớp hợp lệ:

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

Sử dụng Validator.TryValidateProperty()sẽ thêm vào bộ sưu tập kết quả nếu có xác nhận thất bại. Nếu không có xác nhận thất bại thì sẽ không có gì được thêm vào bộ sưu tập kết quả, đó là dấu hiệu của sự thành công.

Thực hiện xác nhận:

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

Điều quan trọng là đặt validateAllPropertiesthành false để phương thức này hoạt động. Khi validateAllPropertiessai chỉ các thuộc tính với một [Required]thuộc tính được kiểm tra. Điều này cho phép IValidatableObject.Validate()phương thức xử lý các xác nhận có điều kiện.


Tôi không thể nghĩ ra một kịch bản mà tôi sẽ sử dụng nó. Bạn có thể cho tôi một ví dụ về nơi bạn sẽ sử dụng này?
Stefan Vasiljevic

Nếu bạn có các cột theo dõi trong bảng của mình (chẳng hạn như người dùng đã tạo nó). Nó được yêu cầu trong cơ sở dữ liệu nhưng bạn bước vào SaveChanges trong ngữ cảnh để đưa vào cơ sở dữ liệu (loại bỏ yêu cầu các nhà phát triển phải nhớ để đặt nó một cách rõ ràng). Tất nhiên, bạn sẽ xác nhận trước khi tiết kiệm. Vì vậy, bạn không đánh dấu cột "người tạo" là bắt buộc nhưng xác thực với tất cả các cột / thuộc tính khác.
MetalPhoenix

Vấn đề với giải pháp này là bây giờ bạn phụ thuộc vào người gọi để đối tượng của bạn được xác nhận hợp lệ.
cocogza

Để nâng cao câu trả lời này, người ta có thể sử dụng sự phản chiếu để tìm tất cả các thuộc tính có thuộc tính xác thực, sau đó gọi TryValidateProperty.
Paul Chernoch

78

Trích dẫn từ Bài đăng trên Blog của Jeff Handley về các đối tượng và thuộc tính xác thực với Trình xác thực :

Khi xác thực một đối tượng, quy trình sau được áp dụng trong Trình xác thực.ValidateObject:

  1. Xác thực các thuộc tính cấp thuộc tính
  2. Nếu bất kỳ trình xác nhận nào không hợp lệ, hủy bỏ xác thực trả về (các) lỗi
  3. Xác thực các thuộc tính cấp đối tượng
  4. Nếu bất kỳ trình xác nhận nào không hợp lệ, hủy bỏ xác thực trả về (các) lỗi
  5. Nếu trên khung máy tính để bàn và đối tượng thực hiện IValiditableObject, thì hãy gọi phương thức Xác thực của nó và trả về bất kỳ (các) lỗi nào

Điều này cho thấy rằng những gì bạn đang cố gắng sẽ không hoạt động vượt trội vì việc xác thực sẽ hủy bỏ ở bước # 2. Bạn có thể thử tạo các thuộc tính kế thừa từ các thuộc tính tích hợp và kiểm tra cụ thể sự hiện diện của một thuộc tính được kích hoạt (thông qua giao diện) trước khi thực hiện xác thực thông thường. Ngoài ra, bạn có thể đặt tất cả logic để xác thực thực thể trong Validatephương thức.


36

Chỉ cần thêm một vài điểm:

Bởi vì Validate()chữ ký phương thức trả về IEnumerable<>, yield returncó thể được sử dụng để tạo ra kết quả một cách lười biếng - điều này có lợi nếu một số kiểm tra xác thực là IO hoặc CPU chuyên sâu.

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

Ngoài ra, nếu bạn đang sử dụng MVC ModelState, bạn có thể chuyển đổi các lỗi kết quả xác thực thành ModelStatecác mục như sau (điều này có thể hữu ích nếu bạn đang thực hiện xác thực trong một liên kết mô hình tùy chỉnh ):

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

Đẹp quá Có đáng sử dụng các thuộc tính và sự phản chiếu trong phương thức Xác thực không?
Schalk

4

Tôi đã triển khai một lớp trừu tượng sử dụng chung để xác nhận

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
Tôi thích phong cách sử dụng các tham số được đặt tên, làm cho mã dễ đọc hơn nhiều.
Drizin

0

Vấn đề với câu trả lời được chấp nhận là bây giờ nó phụ thuộc vào người gọi để đối tượng được xác nhận hợp lệ. Tôi sẽ xóa RangeAttribution và thực hiện xác thực phạm vi bên trong phương thức Xác thực hoặc tôi sẽ tạo một thuộc tính tùy chỉnh phân lớp RangeAttribution lấy tên của thuộc tính được yêu cầu làm đối số trên hàm tạo.

Ví dụ:

[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

Tôi thích câu trả lời của cocogza ngoại trừ việc gọi cơ sở.IsValid dẫn đến ngoại lệ tràn ngăn xếp vì nó sẽ nhập lại phương thức IsValid nhiều lần. Vì vậy, tôi đã sửa đổi nó thành một loại xác nhận cụ thể, trong trường hợp của tôi là địa chỉ e-mail.

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

Điều này làm việc tốt hơn nhiều! Nó không sụp đổ và tạo ra một thông báo lỗi tốt đẹp. Hy vọng điều này sẽ giúp được ai đó!


0

Điều tôi không thích ở iValidate là dường như chỉ chạy SAU tất cả các xác nhận khác.
Ngoài ra, ít nhất là trong trang web của chúng tôi, nó sẽ chạy lại trong nỗ lực lưu. Tôi sẽ đề nghị bạn chỉ cần tạo một hàm và đặt tất cả mã xác thực của bạn vào đó. Thay thế cho các trang web, bạn có thể có xác nhận "đặc biệt" của mình trong bộ điều khiển sau khi mô hình được tạo. Thí dụ:

 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
            {
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.