使用数据注释的依赖属性的自定义模型验证

Posted

技术标签:

【中文标题】使用数据注释的依赖属性的自定义模型验证【英文标题】:Custom model validation of dependent properties using Data Annotations 【发布时间】:2010-02-17 12:30:50 【问题描述】:

从现在开始我就使用了出色的FluentValidation 库来验证我的模型类。在 Web 应用程序中,我也将它与 jquery.validate 插件结合使用来执行客户端验证。 一个缺点是大部分验证逻辑在客户端重复,不再集中在一个地方。

出于这个原因,我正在寻找替代方案。 there 中的 many 示例显示了使用数据注释来执行模型验证。它看起来很有希望。 我不知道的一件事是如何验证依赖于另一个属性值的属性。

我们以下面的模型为例:

public class Event

    [Required]
    public DateTime? StartDate  get; set; 
    [Required]
    public DateTime? EndDate  get; set; 

我想确保EndDate 大于StartDate。我可以写一个自定义 验证属性扩展 ValidationAttribute 以执行自定义验证逻辑。不幸的是,我找不到获得 模型实例:

public class CustomValidationAttribute : ValidationAttribute

    public override bool IsValid(object value)
    
        // value represents the property value on which this attribute is applied
        // but how to obtain the object instance to which this property belongs?
        return true;
    

我发现CustomValidationAttribute 似乎可以完成这项工作,因为它具有包含正在验证的对象实例的ValidationContext 属性。不幸的是,此属性仅在 .NET 4.0 中添加。所以我的问题是:我可以在 .NET 3.5 SP1 中实现相同的功能吗?


更新:

似乎FluentValidation already supports ASP.NET MVC 2 中的客户端验证和元数据。

尽管数据注释是否可用于验证依赖属性,但还是很高兴知道。

【问题讨论】:

您或是否有人想出一种方法让 dataannotations 和 FluentValidation 在同一个类/模型上一起工作(用于验证)?如果是这样那就太棒了,我有一个关于这个与 FV 作者 Jeremy 讨论的帖子,你可以在这里查看:fluentvalidation.codeplex.com/Thread/View.aspx?ThreadId=212371 【参考方案1】:

MVC2 附带一个示例“PropertiesMustMatchAttribute”,它展示了如何让 DataAnnotations 为您工作,它应该在 .NET 3.5 和 .NET 4.0 中都可以工作。该示例代码如下所示:

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
public sealed class PropertiesMustMatchAttribute : ValidationAttribute

    private const string _defaultErrorMessage = "'0' and '1' do not match.";

    private readonly object _typeId = new object();

    public PropertiesMustMatchAttribute(string originalProperty, string confirmProperty)
        : base(_defaultErrorMessage)
    
        OriginalProperty = originalProperty;
        ConfirmProperty = confirmProperty;
    

    public string ConfirmProperty
    
        get;
        private set;
    

    public string OriginalProperty
    
        get;
        private set;
    

    public override object TypeId
    
        get
        
            return _typeId;
        
    

    public override string FormatErrorMessage(string name)
    
        return String.Format(CultureInfo.CurrentUICulture, ErrorMessageString,
            OriginalProperty, ConfirmProperty);
    

    public override bool IsValid(object value)
    
        PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(value);
        object originalValue = properties.Find(OriginalProperty, true /* ignoreCase */).GetValue(value);
        object confirmValue = properties.Find(ConfirmProperty, true /* ignoreCase */).GetValue(value);
        return Object.Equals(originalValue, confirmValue);
    

当您使用该属性时,而不是将其放在模型类的属性上,而是将其放在类本身上:

[PropertiesMustMatch("NewPassword", "ConfirmPassword", ErrorMessage = "The new password and confirmation password do not match.")]
public class ChangePasswordModel

    public string NewPassword  get; set; 
    public string ConfirmPassword  get; set; 

当在您的自定义属性上调用“IsValid”时,整个模型实例都会传递给它,因此您可以通过这种方式获取相关属性值。您可以轻松地按照此模式创建日期比较属性,甚至是更通用的比较属性。

Brad Wilson has a good example on his blog 还展示了如何添加验证的客户端部分,但我不确定该示例是否适用于 .NET 3.5 和 .NET 4.0。

【讨论】:

我已经尝试过了,但我永远无法在我的 aspx 页面/视图上显示验证错误。我尝试使用空字符串调用validationmessagefor,也尝试使用验证摘要,但它没有显示在那里(就像在propertiesmustmatch示例中那样) 我浪费了好几个小时试图让它工作并认为我的代码是错误的,直到我终于意识到当我看到这篇文章时我根本没有正确测试它:***.com/questions/3586324/…(基本上字段/属性级别验证首先触发,因此您需要在它触发您的类级别属性 isvalid() 方法之前对其进行完全验证。 我后来在这里找到了关于字段验证问题后的类验证的更好的帖子:***.com/questions/3099397/…【参考方案2】:

我遇到了这个问题,最近开源了我的解决方案: http://foolproof.codeplex.com/

上面例子的万无一失的解决方案是:

public class Event

    [Required]
    public DateTime? StartDate  get; set; 

    [Required]
    [GreaterThan("StartDate")]
    public DateTime? EndDate  get; set; 

【讨论】:

我认为 GreaterThan 日期验证仅适用于美国格式日期【参考方案3】:

可以在 MVC3 中使用的 CompareAttribute 代替 PropertiesMustMatch。根据这个链接http://devtrends.co.uk/blog/the-complete-guide-to-validation-in-asp.net-mvc-3-part-1:

public class RegisterModel

    // skipped

    [Required]
    [ValidatePasswordLength]
    [DataType(DataType.Password)]
    [Display(Name = "Password")]
    public string Password  get; set;                        

    [DataType(DataType.Password)]
    [Display(Name = "Confirm password")]
    [Compare("Password", ErrorMessage = "The password and confirmation do not match.")]
    public string ConfirmPassword  get; set; 

CompareAttribute 是一个新的、非常有用的验证器,实际上并不是 部分 System.ComponentModel.DataAnnotations, 但已添加到 团队的 System.Web.Mvc DLL。同时 不是特别好命名(唯一的 它所做的比较是为了检查 平等,所以也许 EqualTo 是 更明显),很容易从 此验证器检查的用法 一个财产的价值等于 另一个财产的价值。你可以 从代码中可以看出,该属性 接受一个字符串属性,它是 其他属性的名称 你在比较。经典用法 这种类型的验证器就是我们 在这里使用它:密码 确认。

【讨论】:

【参考方案4】:

您的问题被问到已经过了一段时间,但如果您仍然喜欢元数据(至少有时),下面还有另一种替代解决方案,它允许您为属性提供各种逻辑表达式:

[Required]
public DateTime? StartDate  get; set;     
[Required]
[AssertThat("StartDate != null && EndDate > StartDate")]
public DateTime? EndDate  get; set; 

它适用于服务器端和客户端。更多详情can be found here.

【讨论】:

非常感谢这个库对大多数事情都非常有用。【参考方案5】:

由于 .NET 3.5 的 DataAnnotations 的方法不允许您提供已验证的实际对象或验证上下文,因此您必须采取一些技巧来完成此操作。我必须承认我不熟悉 ASP.NET MVC,所以我不能说如何与 MCV 完全结合使用,但您可以尝试使用线程静态值来传递参数本身。这是一个可能有用的例子。

首先创建某种“对象范围”,允许您传递对象而无需通过调用堆栈传递它们:

public sealed class ContextScope : IDisposable 

    [ThreadStatic]
    private static object currentContext;

    public ContextScope(object context)
    
        currentContext = context;
    

    public static object CurrentContext
    
        get  return context; 
    

    public void Dispose()
    
        currentContext = null;
    

接下来,创建您的验证器以使用 ContextScope:

public class CustomValidationAttribute : ValidationAttribute

    public override bool IsValid(object value)
    
         Event e = (Event)ObjectContext.CurrentContext;

         // validate event here.
    

最后但并非最不重要的一点是,确保对象通过 ContextScope:

Event eventToValidate = [....];
using (var scope new ContextScope(eventToValidate))

    DataAnnotations.Validator.Validate(eventToValidate);

这有用吗?

【讨论】:

史蒂文,这看起来不错。我唯一要修改的是将当前上下文存储到 HttpContext 中,而不是使用 ThreadStatic。我只是在 ASP.NET 应用程序中避免它。 您能解释一下为什么您认为我们应该在 ASP.NET 应用程序中避免这种情况。我在自己的生产应用程序中使用了这个结构,所以我很感兴趣为什么这是不好的。 互联网上有很多文章为什么这是不好的。这是一个:hanselman.com/blog/… ThreadStatic 的问题在于,在 ASP.NET 中,您无法控制线程的生命周期,并且在重用线程时,在某些情况下变量可能会被修改。如果您使用异步页面和控制器,事情会变得更加丑陋。例如,一个请求可能在一个线程上开始并在另一个线程上结束。因此,在 ASP.NET 中,唯一 拥有真正的按请求存储的方法是 HttpContext。 是的,我猜这就是你要去的地方。您对异步页面和控制器是绝对正确的。但是,由于使用它的“上下文”,我并不担心在这种情况下使用 ThreadStatic。 ContextScope 在单个方法调用中使用,可能在您的业务层的某个地方。在这种情况下,ASP.NET 无法将您的方法调用撕成碎片(在您执行该特定方法时切换线程)。当然,您不应该将 ContextScope 存储为 Page 的私有成员,这确实会带来麻烦。 ... 所以在这种情况下,是的,你必须小心滥用,我同意你不应该在你的 ASP.NET Web 应用程序项目中使用它,但是你可以愉快地在您的 Web 应用程序的业务层中使用它。但是当您仍然担心团队中的开发人员可能会滥用此构造时,只需将“currentContext =”的出现更改为“HttpContext.Current.Items[typeof(ContextScope)] =”即可。

以上是关于使用数据注释的依赖属性的自定义模型验证的主要内容,如果未能解决你的问题,请参考以下文章

具有客户端验证的自定义数据注释验证属性

.NET Core 3 自定义验证中的依赖注入 [重复]

Wpf:通用集合依赖属性

在 XAML 中调用时,如何使我的自定义依赖项属性排序到顶部?

如何为依赖于关系的自定义 Core Data 属性发布通知?

如何创建不依赖于 ASP.NET Core 声明的自定义 Authorize 属性?