使用数据注释实现有条件的必填属性

99

我有一个像这样的类:

public class Document
{
   public int DocumentType{get;set;}

   [Required]
   public string Name{get;set;}

   [Required]
   public string Name2{get;set;}
}

如果我在NameName2属性上加入[Required]数据注释,那么一切都很好,如果NameName2为空,验证将抛出一个错误。

但是我希望只有当DocumentType等于1时,Name字段才是必填的,当DocumentType等于2时,只有Name2是必填的。

public class Document
{
   public int DocumentType{get;set;}

   [Required(Expression<Func<object, bool>>)]
   public string Name{get;set;}

   [Required(Expression<Func<object, bool>>)]
   public string Name2{get;set;}
}

但我知道我不能这样做,那会导致错误。针对这个需求,我该怎么办?


1
在您的模型上实现这个 IValidatableObject 接口,并运行该模型的自定义代码 - http://msdn.microsoft.com/zh-cn/library/system.componentmodel.dataannotations.ivalidatableobject.aspx - Chris McKelt
2
对于未来的观众 - https://dev59.com/tGs05IYBdhLWcg3wLfAj - Chris McKelt
12个回答

127

必填验证属性

我编写了一个RequiredIfAttribute,它要求在另一个属性具有特定值(您需要的值)或者另一个属性不具有特定值时填写特定属性值。

以下是可能有帮助的代码:

/// <summary>
/// Provides conditional validation based on related property value.
/// </summary>
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public sealed class RequiredIfAttribute : ValidationAttribute
{
    #region Properties

    /// <summary>
    /// Gets or sets the other property name that will be used during validation.
    /// </summary>
    /// <value>
    /// The other property name.
    /// </value>
    public string OtherProperty { get; private set; }

    /// <summary>
    /// Gets or sets the display name of the other property.
    /// </summary>
    /// <value>
    /// The display name of the other property.
    /// </value>
    public string OtherPropertyDisplayName { get; set; }

    /// <summary>
    /// Gets or sets the other property value that will be relevant for validation.
    /// </summary>
    /// <value>
    /// The other property value.
    /// </value>
    public object OtherPropertyValue { get; private set; }

    /// <summary>
    /// Gets or sets a value indicating whether other property's value should match or differ from provided other property's value (default is <c>false</c>).
    /// </summary>
    /// <value>
    ///   <c>true</c> if other property's value validation should be inverted; otherwise, <c>false</c>.
    /// </value>
    /// <remarks>
    /// How this works
    /// - true: validated property is required when other property doesn't equal provided value
    /// - false: validated property is required when other property matches provided value
    /// </remarks>
    public bool IsInverted { get; set; }

    /// <summary>
    /// Gets a value that indicates whether the attribute requires validation context.
    /// </summary>
    /// <returns><c>true</c> if the attribute requires validation context; otherwise, <c>false</c>.</returns>
    public override bool RequiresValidationContext
    {
        get { return true; }
    }

    #endregion

    #region Constructor

    /// <summary>
    /// Initializes a new instance of the <see cref="RequiredIfAttribute"/> class.
    /// </summary>
    /// <param name="otherProperty">The other property.</param>
    /// <param name="otherPropertyValue">The other property value.</param>
    public RequiredIfAttribute(string otherProperty, object otherPropertyValue)
        : base("'{0}' is required because '{1}' has a value {3}'{2}'.")
    {
        this.OtherProperty = otherProperty;
        this.OtherPropertyValue = otherPropertyValue;
        this.IsInverted = false;
    }

    #endregion

    /// <summary>
    /// Applies formatting to an error message, based on the data field where the error occurred.
    /// </summary>
    /// <param name="name">The name to include in the formatted message.</param>
    /// <returns>
    /// An instance of the formatted error message.
    /// </returns>
    public override string FormatErrorMessage(string name)
    {
        return string.Format(
            CultureInfo.CurrentCulture,
            base.ErrorMessageString,
            name,
            this.OtherPropertyDisplayName ?? this.OtherProperty,
            this.OtherPropertyValue,
            this.IsInverted ? "other than " : "of ");
    }

    /// <summary>
    /// Validates the specified value with respect to the current validation attribute.
    /// </summary>
    /// <param name="value">The value to validate.</param>
    /// <param name="validationContext">The context information about the validation operation.</param>
    /// <returns>
    /// An instance of the <see cref="T:System.ComponentModel.DataAnnotations.ValidationResult" /> class.
    /// </returns>
    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        if (validationContext == null)
        {
            throw new ArgumentNullException("validationContext");
        }

        PropertyInfo otherProperty = validationContext.ObjectType.GetProperty(this.OtherProperty);
        if (otherProperty == null)
        {
            return new ValidationResult(
                string.Format(CultureInfo.CurrentCulture, "Could not find a property named '{0}'.", this.OtherProperty));
        }

        object otherValue = otherProperty.GetValue(validationContext.ObjectInstance);

        // check if this value is actually required and validate it
        if (!this.IsInverted && object.Equals(otherValue, this.OtherPropertyValue) ||
            this.IsInverted && !object.Equals(otherValue, this.OtherPropertyValue))
        {
            if (value == null)
            {
                return new ValidationResult(this.FormatErrorMessage(validationContext.DisplayName));
            }

            // additional check for strings so they're not empty
            string val = value as string;
            if (val != null && val.Trim().Length == 0)
            {
                return new ValidationResult(this.FormatErrorMessage(validationContext.DisplayName));
            }
        }

        return ValidationResult.Success;
    }
}

2
干得好,喜欢它,将添加一个not参数,仅检查它是否不是某个东西而不是仅仅是它本身;) - 谢谢 - Pakk
1
你能提供一下如何使用这个属性吗?我在我的ViewModel中无法弄清楚如何获取数据并将其传递给属性。 - Philippe
2
@Pakk,这实际上已经是此代码的一部分了。请查看属性IsInverted,它充当了反向if... - Robert Koritnik
1
@Philippe:这是一个通常由框架直接运行的属性,因此您实际上不需要向其传递任何数据。您只需在数据模型POCO上声明性地设置它即可。一个经典的例子是网店。在发票创建期间,用户被问及是否为个人或公司购买。如果他们选择“公司”,则需要填写一些其他字段。这些数据模型属性(与这些字段相关)将在其上具有此属性[RequiredIf('IsCompany', true)],其中IsCompany: bool通常绑定到复选框。希望这可以帮助到您。 - Robert Koritnik
5
做得好,有一个问题:是否可以为此添加不显眼的验证? - Sirwan Afifi
显示剩余3条评论

52

使用数据注释实现有条件必需属性

 [RequiredIf(dependent Property name, dependent Property value)]

e.g. 


 [RequiredIf("Country", "Ethiopia")]
 public string POBox{get;set;}
 // POBox is required in Ethiopia
 public string Country{get;set;}

 [RequiredIf("destination", "US")]
 public string State{get;set;}
 // State is required in US

 public string destination{get;set;}



public class RequiredIfAttribute : ValidationAttribute
{
    RequiredAttribute _innerAttribute = new RequiredAttribute();
    public string _dependentProperty { get; set; }
    public object _targetValue { get; set; }

    public RequiredIfAttribute(string dependentProperty, object targetValue)
    {
        this._dependentProperty = dependentProperty;
        this._targetValue = targetValue;
    }
    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        var field = validationContext.ObjectType.GetProperty(_dependentProperty);
        if (field != null)
        {
            var dependentValue = field.GetValue(validationContext.ObjectInstance, null);
            if ((dependentValue == null && _targetValue == null) || (dependentValue.Equals(_targetValue)))
            {
                if (!_innerAttribute.IsValid(value))
                {
                    string name = validationContext.DisplayName;
                    string specificErrorMessage = ErrorMessage;
                    if (specificErrorMessage.Length < 1)
                        specificErrorMessage = $"{name} is required.";

                    return new ValidationResult(specificErrorMessage, new[] { validationContext.MemberName });
                }
            }
            return ValidationResult.Success;
        }
        else
        {
            return new ValidationResult(FormatErrorMessage(_dependentProperty));
        }
    }
}

嗨,我将它与单选按钮结合使用,它可以工作,但是在验证触发之后,当我更改相关值时,它无法清除验证消息。您有任何想法如何实现吗? - Bambam Deo

10

我认为,这在开箱即用的情况下仍然不可能。

但是我找到了这篇有关Mvc.ValidationToolkit的有希望的文章(此处也有链接,不幸地是仅为alpha版,但您可能只需从此代码中提取所需的方法并将其集成到自己的项目中即可),其中包含听起来不错的属性RequiredIf,似乎正好符合您的目的:

  • 您可以从链接的zip文件中下载该项目并构建它
  • 从生成的文件夹中获取已构建的dll文件,并在您使用的项目中引用它
  • 不幸的是,这似乎还需要引用MVC(最简单的方法是在VS中启动MVC项目或install-package Microsoft.AspNet.Mvc
  • 在您想要使用它的文件中,添加using Mvc.ValidationToolkit;
  • 然后,您可以编写像这样的代码:[RequiredIf("DocumentType", 2)][RequiredIf("DocumentType", 1)],因此只有在DocumentType不等于1或2时未提供namename2时对象才有效。

10

1
感谢推荐FluentValidation。很喜欢它。 - Stephen McDowell

5

我一直使用来自 System.ComponentModel.DataAnnotations 的 IValidatableObject 接口;

以下是一个示例:

  public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
        {
            if (this.SendInAppNotification)
            {
                if (string.IsNullOrEmpty(this.NotificationTitle) || string.IsNullOrWhiteSpace(this.NotificationTitle))
                {
                    yield return new ValidationResult(
                        $"Notification Title is required",
                        new[] { nameof(this.NotificationTitle) });
                }
            }

赞。我总是更喜欢使用.NET Framework中包含的内容,而不是添加外部库或编写特定逻辑。 - SteveB
很遗憾,这不像数据注释那样在前端触发。 - Enrico
这是最简单的答案,在 Blazor(.NET 7)前端调用 editContext.Validate() 时对我有效。 - Brian Pursley

2

请查看ExpressiveAnnotations .net库 Git参考资料

它有'RequiredIf'和'AssertThat'验证属性


1

我编写了一个简单的自定义验证属性,非常易读。

using System;
using System.ComponentModel.DataAnnotations;

namespace some.namespace
{
    public class RequiredIfAttribute : ValidationAttribute
    {
        public string PropertyName { get; set; }
        public object Value { get; set; }

        public RequiredIfAttribute(string propertyName, object value = null, string errorMessage = "")
        {
            PropertyName = propertyName;
            Value = value;
            ErrorMessage = errorMessage;
        }

        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            if (PropertyName == null || PropertyName.ToString() == "")
            {
                throw new Exception("RequiredIf: you have to indicate the name of the property to use in the validation");
            }

            var propertyValue = GetPropertyValue(validationContext);

            if (HasPropertyValue(propertyValue) && (value == null || value.ToString() == ""))
            {
                return new ValidationResult(ErrorMessage);
            }
            else
            {
                return ValidationResult.Success;
            }
        }

        private object GetPropertyValue(ValidationContext validationContext)
        {
            var instance = validationContext.ObjectInstance;
            var type = instance.GetType();
            return type.GetProperty(PropertyName).GetValue(instance);
        }

        private bool HasPropertyValue(object propertyValue)
        {
            if (Value != null)
            {
                return propertyValue != null && propertyValue.ToString() == Value.ToString();
            }
            else
            {
                return propertyValue != null && propertyValue.ToString() != "";
            }
        }
    }
}

你可以这样使用它。
public class Document
{
   public int DocumentType{get;set;}

   [RequiredIf("DocumentType", "1", ErrorMessage = "The field is required.")]
   public string Name{get;set;}

   [RequiredIf("DocumentType", "2", ErrorMessage = "The field is required.")]
   public string Name2{get;set;}
}

1
我通过扩展RequiredAttribute类并借鉴CompareAttributeRobert的优秀解决方案中的一些逻辑来解决这个问题。
/// <summary>
/// Provides conditional <see cref="RequiredAttribute"/> 
/// validation based on related property value.
/// </summary>
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public sealed class RequiredIfAttribute : RequiredAttribute
{
    /// <summary>
    /// Gets or sets a value indicating whether other property's value should
    /// match or differ from provided other property's value (default is <c>false</c>).
    /// </summary>
    public bool IsInverted { get; set; } = false;

    /// <summary>
    /// Gets or sets the other property name that will be used during validation.
    /// </summary>
    /// <value>
    /// The other property name.
    /// </value>
    public string OtherProperty { get; private set; }

    /// <summary>
    /// Gets or sets the other property value that will be relevant for validation.
    /// </summary>
    /// <value>
    /// The other property value.
    /// </value>
    public object OtherPropertyValue { get; private set; }

    /// <summary>
    /// Initializes a new instance of the <see cref="RequiredIfAttribute"/> class.
    /// </summary>
    /// <param name="otherProperty">The other property.</param>
    /// <param name="otherPropertyValue">The other property value.</param>
    public RequiredIfAttribute(string otherProperty, object otherPropertyValue)
        : base()
    {
        OtherProperty = otherProperty;
        OtherPropertyValue = otherPropertyValue;
    }

    protected override ValidationResult IsValid(
        object value, 
        ValidationContext validationContext)
    {
        PropertyInfo otherPropertyInfo = validationContext
            .ObjectType.GetProperty(OtherProperty);
        if (otherPropertyInfo == null)
        {
            return new ValidationResult(
                string.Format(
                    CultureInfo.CurrentCulture, 
                    "Could not find a property named {0}.", 
                validationContext.ObjectType, OtherProperty));
        }

        // Determine whether to run [Required] validation
        object actualOtherPropertyValue = otherPropertyInfo
            .GetValue(validationContext.ObjectInstance, null);
        if (!IsInverted && Equals(actualOtherPropertyValue, OtherPropertyValue) ||
            IsInverted && !Equals(actualOtherPropertyValue, OtherPropertyValue))
        {
            return base.IsValid(value, validationContext);
        }
        return default;
    }
}

示例用法:

public class Model {
    public bool Subscribe { get; set; }
    
    [RequiredIf(nameof(Subscribe), true)]
    [DataType(DataType.EmailAddress)]
    public string Email { get; set; }
}

这样,您就可以获得所有标准的Required验证功能。
注:我正在使用.NET 5,但我尝试删除c# 9.0中添加的语言功能以实现更广泛的兼容性。

1
请查看MVC Foolproof验证。它在模型中具有数据注释,例如RequiredIf (dependent Property, dependent value),如果我记得正确的话。您可以从以下位置下载Foolproof:
Visual Studio(2017) -> 工具 -> Nuget包管理器 -> 管理解决方案的Nuget包。除了jquery文件之外,还要引用mvcfoolproof.unobtrusive.min.js。

0

我知道这个问题很久以前就有了,但是在罗伯特的答案评论区有人问如何将unobtrusive用作解决方案的一部分。

我也想要客户端验证,所以我分享了我的修订代码到罗伯特原始代码中。它本质上与原始代码相同,只是实现了IClientModelValidator并添加了一个额外的AddValidation方法。客户端验证仍然尊重IsInverted属性。

实现IClientModelValidator

public sealed class RequiredIfAttribute : ValidationAttribute, IClientModelValidator

新的AddValidation方法

 public void AddValidation(ClientModelValidationContext context)
        {
            var viewContext = context.ActionContext as ViewContext;
            var modelType = context.ModelMetadata.ContainerType;
            var instance = viewContext?.ViewData.Model;
            var model = instance?.GetType().Name == modelType.Name
                ? instance
                : instance?.GetType()?.GetProperties().First(x => x.PropertyType.Name == modelType.Name)
                    .GetValue(instance, null);
            object otherValue = modelType.GetProperty(this.OtherProperty)?.GetValue(model, null);
            object value = modelType.GetProperty(context.ModelMetadata.Name)?.GetValue(model, null);
            string displayName = context.ModelMetadata.DisplayName ?? context.ModelMetadata.Name;
            string errorMessage = null;

            // check if this value is actually required and validate it
            if (!this.IsInverted && object.Equals(otherValue, this.OtherPropertyValue) ||
                this.IsInverted && !object.Equals(otherValue, this.OtherPropertyValue))
            {
                if (value == null)
                {
                    errorMessage = this.FormatErrorMessage(displayName);
                }

                // additional check for strings so they're not empty
                string val = value as string;
                if (val != null && val.Trim().Length == 0)
                {
                    errorMessage = this.FormatErrorMessage(displayName);
                }
            }

            if (!string.IsNullOrWhiteSpace(errorMessage))
            {
                context.Attributes.Add("data-val", "true");
                context.Attributes.Add("data-val-required", errorMessage);
            }
            
        }

完整代码

    /// <summary>
    /// Provides conditional validation based on related property value.
    /// </summary>
    [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
    public sealed class RequiredIfAttribute : ValidationAttribute, IClientModelValidator
    {
        #region Properties

        /// <summary>
        /// Gets or sets the other property name that will be used during validation.
        /// </summary>
        /// <value>
        /// The other property name.
        /// </value>
        public string OtherProperty { get; private set; }

        /// <summary>
        /// Gets or sets the display name of the other property.
        /// </summary>
        /// <value>
        /// The display name of the other property.
        /// </value>
        public string OtherPropertyDisplayName { get; set; }

        /// <summary>
        /// Gets or sets the other property value that will be relevant for validation.
        /// </summary>
        /// <value>
        /// The other property value.
        /// </value>
        public object OtherPropertyValue { get; private set; }

        /// <summary>
        /// Gets or sets a value indicating whether other property's value should match or differ from provided other property's value (default is <c>false</c>).
        /// </summary>
        /// <value>
        ///   <c>true</c> if other property's value validation should be inverted; otherwise, <c>false</c>.
        /// </value>
        /// <remarks>
        /// How this works
        /// - true: validated property is required when other property doesn't equal provided value
        /// - false: validated property is required when other property matches provided value
        /// </remarks>
        public bool IsInverted { get; set; }

        /// <summary>
        /// Gets a value that indicates whether the attribute requires validation context.
        /// </summary>
        /// <returns><c>true</c> if the attribute requires validation context; otherwise, <c>false</c>.</returns>
        public override bool RequiresValidationContext
        {
            get { return true; }
        }
        #endregion

        #region Constructor

        /// <summary>
        /// Initializes a new instance of the <see cref="RequiredIfAttribute"/> class.
        /// </summary>
        /// <param name="otherProperty">The other property.</param>
        /// <param name="otherPropertyValue">The other property value.</param>
        public RequiredIfAttribute(string otherProperty, object otherPropertyValue)
            : base("'{0}' is required because '{1}' has a value {3}'{2}'.")
        {
            this.OtherProperty = otherProperty;
            this.OtherPropertyValue = otherPropertyValue;
            this.IsInverted = false;
        }

        #endregion

        public void AddValidation(ClientModelValidationContext context)
        {
            var viewContext = context.ActionContext as ViewContext;
            var modelType = context.ModelMetadata.ContainerType;
            var instance = viewContext?.ViewData.Model;
            var model = instance?.GetType().Name == modelType.Name
                ? instance
                : instance?.GetType()?.GetProperties().First(x => x.PropertyType.Name == modelType.Name)
                    .GetValue(instance, null);
            object otherValue = modelType.GetProperty(this.OtherProperty)?.GetValue(model, null);
            object value = modelType.GetProperty(context.ModelMetadata.Name)?.GetValue(model, null);
            string displayName = context.ModelMetadata.DisplayName ?? context.ModelMetadata.Name;
            string errorMessage = null;

            // check if this value is actually required and validate it
            if (!this.IsInverted && object.Equals(otherValue, this.OtherPropertyValue) ||
                this.IsInverted && !object.Equals(otherValue, this.OtherPropertyValue))
            {
                if (value == null)
                {
                    errorMessage = this.FormatErrorMessage(displayName);
                }

                // additional check for strings so they're not empty
                string val = value as string;
                if (val != null && val.Trim().Length == 0)
                {
                    errorMessage = this.FormatErrorMessage(displayName);
                }
            }

            if (!string.IsNullOrWhiteSpace(errorMessage))
            {
                context.Attributes.Add("data-val", "true");
                context.Attributes.Add("data-val-required", errorMessage);
            }
            
        }

        /// <summary>
        /// Applies formatting to an error message, based on the data field where the error occurred.
        /// </summary>
        /// <param name="name">The name to include in the formatted message.</param>
        /// <returns>
        /// An instance of the formatted error message.
        /// </returns>
        public override string FormatErrorMessage(string name)
        {
            return string.Format(
                CultureInfo.CurrentCulture,
                base.ErrorMessageString,
                name,
                this.OtherPropertyDisplayName ?? this.OtherProperty,
                this.OtherPropertyValue,
                this.IsInverted ? "other than " : "of ");
        }

        /// <summary>
        /// Validates the specified value with respect to the current validation attribute.
        /// </summary>
        /// <param name="value">The value to validate.</param>
        /// <param name="validationContext">The context information about the validation operation.</param>
        /// <returns>
        /// An instance of the <see cref="T:System.ComponentModel.DataAnnotations.ValidationResult" /> class.
        /// </returns>
        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            if (validationContext == null)
            {
                throw new ArgumentNullException("validationContext");
            }

            PropertyInfo otherProperty = validationContext.ObjectType.GetProperty(this.OtherProperty);
            if (otherProperty == null)
            {
                return new ValidationResult(
                    string.Format(CultureInfo.CurrentCulture, "Could not find a property named '{0}'.", this.OtherProperty));
            }

            object otherValue = otherProperty.GetValue(validationContext.ObjectInstance);

            // check if this value is actually required and validate it
            if (!this.IsInverted && object.Equals(otherValue, this.OtherPropertyValue) ||
                this.IsInverted && !object.Equals(otherValue, this.OtherPropertyValue))
            {
                if (value == null)
                {
                    return new ValidationResult(this.FormatErrorMessage(validationContext.DisplayName));
                }

                // additional check for strings so they're not empty
                string val = value as string;
                if (val != null && val.Trim().Length == 0)
                {
                    return new ValidationResult(this.FormatErrorMessage(validationContext.DisplayName));
                }
            }

            return ValidationResult.Success;
        }
    }

只要您在布局或Razor视图中按顺序包含了jquery.js、jquery.validate.js和jquery.validate.unobtrusive.js脚本文件,这个应该就可以正常工作了。


Parameter count mismatch error at PropertyInfo.GetValue()(在我的代码中是第85行)出现在 var model 变量处。我无法修复它,我甚至尝试了这个:https://dev59.com/D1wZ5IYBdhLWcg3wG9J_?noredirect=1&lq=1 ,但是其他错误开始显示出来,你能否请检查一下你的代码。这将非常有用。 - bzmind
一个对我来说很好的替代方案是 https://dev59.com/8r3pa4cB1Zd3GeqPWAaf,虽然我需要对其进行一些调整,并在评论中提到了这一点。 - bzmind

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接