【问题标题】:Custom Validation Attributes: Comparing two properties in the same model自定义验证属性:比较同一模型中的两个属性
【发布时间】:2017-01-27 18:16:21
【问题描述】:

有没有一种方法可以在 ASP.NET Core 中创建自定义属性,以使用 ValidationAttribute 验证模型中的一个日期属性是否小于其他日期属性。

假设我有这个:

public class MyViewModel 
{
    [Required]
    [CompareDates]
    public DateTime StartDate { get; set; }

    [Required]
    public DateTime EndDate { get; set; } = DateTime.Parse("3000-01-01");
}

我正在尝试使用这样的东西:

    public class CompareDates : ValidationAttribute
{
    public CompareDates()
        : base("") { }

    public override bool IsValid(object value)
    {
        return base.IsValid(value);
    }

}

我发现了其他建议使用另一个库的 SO 帖子,但如果可行的话,我更喜欢坚持使用 ValidationAttribute。

【问题讨论】:

    标签: c# validation asp.net-core


    【解决方案1】:

    您可以创建自定义验证属性来比较两个属性。这是一个服务器端验证:

    public class MyViewModel
    {
        [DateLessThan("End", ErrorMessage = "Not valid")]
        public DateTime Begin { get; set; }
    
        public DateTime End { get; set; }
    }
    
    public class DateLessThanAttribute : ValidationAttribute
    {
        private readonly string _comparisonProperty;
    
        public DateLessThanAttribute(string comparisonProperty)
        {
             _comparisonProperty = comparisonProperty;
        }
    
        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            ErrorMessage = ErrorMessageString;
            var currentValue = (DateTime)value;
    
            var property = validationContext.ObjectType.GetProperty(_comparisonProperty);
    
            if (property == null)
                throw new ArgumentException("Property with this name not found");
    
            var comparisonValue = (DateTime)property.GetValue(validationContext.ObjectInstance);
    
            if (currentValue > comparisonValue)
                return new ValidationResult(ErrorMessage);
    
            return ValidationResult.Success;
        }
    }
    

    更新: 如果您需要对该属性进行客户端验证,则需要实现一个IClientModelValidator 接口:

    public class DateLessThanAttribute : ValidationAttribute, IClientModelValidator
    {
        ...
        public void AddValidation(ClientModelValidationContext context)
        {
            var error = FormatErrorMessage(context.ModelMetadata.GetDisplayName());
            context.Attributes.Add("data-val", "true");
            context.Attributes.Add("data-val-error", error);
        }
    }
    

    AddValidation 方法将为来自context.Attributes 的输入添加属性。

    您可以在这里阅读更多内容IClientModelValidator

    【讨论】:

    • 我已经这样做了,服务器端验证了,但客户端什么也没做?
    • IClientModelValidator 适用于 Asp.net Core。 IClientValidatable 或 RemoteAttribute (see SO here) 可能有助于完整的框架客户端验证。
    • 我在 asp.net 核心中创建了一个包含最常见自定义验证的库。该库具有所有服务器端自定义验证的客户端验证。该库还解决了 OP 的单一属性问题,如下所示:[CompareTo(nameof(EndDate), ComparisionType.SmallerThan)] public DateTime StartDate { get; set; }。这是图书馆的链接:github.com/TanvirArjel/AspNetCore.CustomValidation
    • 如何本地化错误字符串? FormatErrorMessage(context.ModelMetadata.GetDisplayName()) 本地化?
    • 从CompareAttribute派生会不会很淘气,只调用基础构造函数并使用“OtherProperty”而不是“_comparisonProperty”?
    【解决方案2】:

    作为一种可能的选择自我验证

    您只需要使用方法Validate() 实现一个接口IValidatableObject,您可以在其中放置您的验证码。

    public class MyViewModel : IValidatableObject
    {
        [Required]
        public DateTime StartDate { get; set; }
    
        [Required]
        public DateTime EndDate { get; set; } = DateTime.Parse("3000-01-01");
    
        public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
        {
            int result = DateTime.Compare(StartDate , EndDate);
            if (result < 0)
            {
                yield return new ValidationResult("start date must be less than the end date!", new [] { "ConfirmEmail" });
            }
        }
    }
    

    【讨论】:

    • 谢谢,但我需要将其作为属性来实现。
    • 这有效,但仅适用于服务器端验证(提交后)。
    • 这使得使用IStringlocalizer 非常痛苦,不推荐这种方法。
    【解决方案3】:

    我创建了一个库,其中包含 ASP.NET Core 中最常见的自定义验证。 该库还具有所有服务器端自定义验证的客户端验证。 该库通过单个属性解决了 OP 的问题,如下所示:

    // If you want the StartDate to be smaller than the EndDate:
    [CompareTo(nameof(EndDate), ComparisionType.SmallerThan)] 
    public DateTime StartDate { get; set; }
    

    这里是库的 GitHub 链接:AspNetCore.CustomValidation

    目前,该库包含以下验证属性:

    1. FileAttribute - 验证文件类型、文件最大大小、文件最小大小;

    2。 MaxAgeAttribute - 根据 DateTime 类型的出生日期值验证最大年龄;

    3. MinAgeAttribute - 根据 DateTime 类型的出生日期值验证所需的最低年龄;

    4. MaxDateAttribute - 为 DateTime 字段设置最大值验证;

    5. MinDateAttribute - 为 DateTime 字段设置最小值验证;

    6. CompareToAttibute – 将一个属性值与另一个属性值进行比较;

    7. TinyMceRequiredAttribute - 在 TinyMCE、CkEditor 等在线文本编辑器上强制执行所需的验证属性。

    【讨论】:

      【解决方案4】:

      根据 Alexander Gore 的回应,我建议进行更好的通用验证(并且它与 .Net 核心兼容)。当您想使用 GreatherThan 或 LessThan 逻辑(无论是什么类型)比较属性时,您可以验证它们是否实现了 IComparable 接口。如果两个属性都有效,您可以使用CompareTo 实现。此规则也适用于DateTime 和数字类型

      小于

      [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter)]
      public class LessThanAttribute : ValidationAttribute
      {
          private readonly string _comparisonProperty;
      
          public LessThanAttribute(string comparisonProperty)
          {
              _comparisonProperty = comparisonProperty;
          }
      
          protected override ValidationResult IsValid(object value, ValidationContext validationContext)
          {
              ErrorMessage = ErrorMessageString;
      
              if (value.GetType() == typeof(IComparable))
              {
                  throw new ArgumentException("value has not implemented IComparable interface");
              }
      
              var currentValue = (IComparable)value;
      
              var property = validationContext.ObjectType.GetProperty(_comparisonProperty);
      
              if (property == null)
              {
                  throw new ArgumentException("Comparison property with this name not found");
              }
      
              var comparisonValue = property.GetValue(validationContext.ObjectInstance);
      
              if (comparisonValue.GetType() == typeof(IComparable))
              {
                  throw new ArgumentException("Comparison property has not implemented IComparable interface");
              }
      
              if (!ReferenceEquals(value.GetType(), comparisonValue.GetType()))
              {
                  throw new ArgumentException("The properties types must be the same");
              }
      
              if (currentValue.CompareTo((IComparable)comparisonValue) >= 0)
              {
                  return new ValidationResult(ErrorMessage);
              }
      
              return ValidationResult.Success;
          }
      }
      

      大于

      [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter)]
      public class GreaterThanAttribute : ValidationAttribute
      {
          private readonly string _comparisonProperty;
      
          public GreaterThanAttribute(string comparisonProperty)
          {
              _comparisonProperty = comparisonProperty;
          }
      
          protected override ValidationResult IsValid(object value, ValidationContext validationContext)
          {
              ErrorMessage = ErrorMessageString;
      
              if (value.GetType() == typeof(IComparable))
              {
                  throw new ArgumentException("value has not implemented IComparable interface");
              }
      
              var currentValue = (IComparable)value;
      
              var property = validationContext.ObjectType.GetProperty(_comparisonProperty);
      
              if (property == null)
              {
                  throw new ArgumentException("Comparison property with this name not found");
              }
      
              var comparisonValue = property.GetValue(validationContext.ObjectInstance);
      
              if (comparisonValue.GetType() == typeof(IComparable))
              {
                  throw new ArgumentException("Comparison property has not implemented IComparable interface");
              }
      
              if (!ReferenceEquals(value.GetType(), comparisonValue.GetType()))
              {
                  throw new ArgumentException("The properties types must be the same");
              }
      
              if (currentValue.CompareTo((IComparable)comparisonValue) < 0)
              {
                  return new ValidationResult(ErrorMessage);
              }
      
              return ValidationResult.Success;
          }
      }
      

      在预订上下文中,示例如下:

      public DateTime CheckInDate { get; set; }
      
      [GreaterThan("CheckInDate", ErrorMessage = "CheckOutDate must be greater than CheckInDate")]
      public DateTime CheckOutDate { get; set; }
      

      【讨论】:

      • Jaime,我喜欢你对 Alexander Gore 回应的概括。我建议将检查 'if (comparisonValue.GetType() == typeof(IComparable))' 替换为检查 'if(!ReferenceEquals(value.GetType(), comparisonValue.GetType()))'。两种类型可以实现 IComparable 但它们本身不具有可比性,例如一个 int 和一个 DateTime。其次,如果我们可以为 GT、GE、EQ、LE 和 LT 设置一个比较属性会很好,但我不知道如何。
      • Jaime,你在上面的小于属性中有错字。代码if (currentValue.CompareTo((IComparable)comparisonValue) &gt;= 0) 应该是if (currentValue.CompareTo((IComparable)comparisonValue) &gt; 0)
      【解决方案5】:

      基于 Jaime 的回答和 Jeffrey 关于需要单个属性来表示小于、小于或等于、等于、大于、大于或等于的评论。

      以下代码将使用单个属性处理所有条件。

      public enum ComparisonType
      {
          LessThan,
          LessThanOrEqualTo,
          EqualTo,
          GreaterThan,
          GreaterThanOrEqualTo
      }
      
      [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter)]
      public class ComparisonAttribute : ValidationAttribute
      {
          private readonly string _comparisonProperty;
          private readonly ComparisonType _comparisonType;
      
          public ComparisonAttribute(string comparisonProperty, ComparisonType comparisonType)
          {
              _comparisonProperty = comparisonProperty;
              _comparisonType = comparisonType;
          }
      
          protected override ValidationResult IsValid(object value, ValidationContext validationContext)
          {
              ErrorMessage = ErrorMessageString;
      
              if (value.GetType() == typeof(IComparable))
              {
                  throw new ArgumentException("value has not implemented IComparable interface");
              }
      
              var currentValue = (IComparable) value;
      
              var property = validationContext.ObjectType.GetProperty(_comparisonProperty);
      
              if (property == null)
              {
                  throw new ArgumentException("Comparison property with this name not found");
              }
      
              var comparisonValue = property.GetValue(validationContext.ObjectInstance);
      
              if (comparisonValue.GetType() == typeof(IComparable))
              {
                  throw new ArgumentException("Comparison property has not implemented IComparable interface");
              }
      
              if (!ReferenceEquals(value.GetType(), comparisonValue.GetType()))
              {
                  throw new ArgumentException("The properties types must be the same");
              }
      
              bool compareToResult;
      
              switch (_comparisonType)
              {
                  case ComparisonType.LessThan:
                      compareToResult = currentValue.CompareTo((IComparable) comparisonValue) >= 0;
                      break;
                  case ComparisonType.LessThanOrEqualTo:
                      compareToResult = currentValue.CompareTo((IComparable) comparisonValue) > 0;
                      break;
                  case ComparisonType.EqualTo:
                      compareToResult = currentValue.CompareTo((IComparable) comparisonValue) != 0;
                      break;
                  case ComparisonType.GreaterThan:
                      compareToResult = currentValue.CompareTo((IComparable) comparisonValue) <= 0;
                      break;
                  case ComparisonType.GreaterThanOrEqualTo:
                      compareToResult = currentValue.CompareTo((IComparable) comparisonValue) < 0;
                      break;
                  default:
                      throw new ArgumentOutOfRangeException();
              }
      
              return compareToResult ? new ValidationResult(ErrorMessage) : ValidationResult.Success;
          }
      }
      

      在预订上下文中,示例如下:

      public DateTime CheckInDate { get; set; }
      
      [Comparison("CheckInDate", ComparisonType.EqualTo, ErrorMessage = "CheckOutDate must be equal to CheckInDate")]
      public DateTime CheckOutDate { get; set; }
      

      【讨论】:

      • 这适用于非空值,但如果在调用之前放置 if(null != value)... 和额外的 if(null != comparisonValue) 语句会更好。值上的 GetType() 以避免 Null 引用异常。
      【解决方案6】:

      您可以在IsValid() 方法中比较两个日期。

      public class CompareDates : ValidationAttribute
      {
          protected override ValidationResult IsValid(object value, ValidationContext validationContext)
          {
              // get your StartDate and EndDate from model and value
      
              // perform comparison
              if (StartDate < EndDate)
              {
                  return new ValidationResult("start date must be less than the end date");
              }
              else
              {
                  return ValidationResult.Success;
              }
          }
      }
      

      【讨论】:

        【解决方案7】:

        这是我对此的看法。我的版本忽略了 null(可选)的属性。它非常适合应用于 Web API。

        [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter)]
        public class ComparisonAttribute : ValidationAttribute
        {
            private readonly string _comparisonProperty;
            private readonly ComparisonType _comparisonType;
        
            public ComparisonAttribute(string comparisonProperty, ComparisonType comparisonType)
            {
                _comparisonProperty = comparisonProperty;
                _comparisonType = comparisonType;
            }
        
            protected override ValidationResult IsValid(object value, ValidationContext validationContext)
            {
                ErrorMessage = ErrorMessageString;
        
                var property = validationContext.ObjectType.GetProperty(_comparisonProperty);
                if (property == null)
                    throw new ArgumentException($"Property {_comparisonProperty} not found");
        
                var right = property.GetValue(validationContext.ObjectInstance);
                if (value is null || right is null)
                    return ValidationResult.Success;
        
                if (value.GetType() == typeof(IComparable))
                    throw new ArgumentException($"The property {validationContext.MemberName} does not implement {typeof(IComparable).Name} interface");
        
                if (right.GetType() == typeof(IComparable))
                    throw new ArgumentException($"The property {_comparisonProperty} does not implement {typeof(IComparable).Name} interface");
        
                if (!ReferenceEquals(value.GetType(), right.GetType()))
                    throw new ArgumentException("The property types must be the same");
        
                var left = (IComparable)value;
                bool isValid;
        
                switch (_comparisonType)
                {
                    case ComparisonType.LessThan:
                        isValid = left.CompareTo((IComparable)right) < 0;
                        break;
                    case ComparisonType.LessThanOrEqualTo:
                        isValid = left.CompareTo((IComparable)right) <= 0;
                        break;
                    case ComparisonType.EqualTo:
                        isValid = left.CompareTo((IComparable)right) != 0;
                        break;
                    case ComparisonType.GreaterThan:
                        isValid = left.CompareTo((IComparable)right) > 0;
                        break;
                    case ComparisonType.GreaterThanOrEqualTo:
                        isValid = left.CompareTo((IComparable)right) >= 0;
                        break;
                    default:
                        throw new ArgumentOutOfRangeException();
                }
        
                return isValid
                    ? ValidationResult.Success
                    : new ValidationResult(ErrorMessage);
            }
        
            public enum ComparisonType
            {
                LessThan,
                LessThanOrEqualTo,
                EqualTo,
                GreaterThan,
                GreaterThanOrEqualTo
            }
        }
        

        【讨论】:

          猜你喜欢
          • 2016-12-12
          • 2012-08-11
          • 1970-01-01
          • 1970-01-01
          • 2013-01-17
          • 1970-01-01
          • 1970-01-01
          • 2011-07-16
          • 1970-01-01
          相关资源
          最近更新 更多