96

I have a class like this:

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

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

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

Now if I put a [Required] data annotation on the Name and Name2 properties, then everything is ok and if Name or Name2 are empty, validation will throw an error.

But I want Name field only to be required if DocumentType is equal to 1 and Name2 only required if DocumentType is equal to 2 .

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

but I know I can't, it causes an error. What should I do for this requirement?

DrCopyPaste
  • 4,023
  • 1
  • 22
  • 57
brtb
  • 2,201
  • 6
  • 34
  • 53
  • 1
    Implement this ivalidatableobject on your models and run your custom code for that model - http://msdn.microsoft.com/en-us/library/system.componentmodel.dataannotations.ivalidatableobject.aspx – Chris McKelt Oct 14 '14 at 15:35
  • 2
    for future viewers - http://stackoverflow.com/questions/7390902/requiredif-conditional-validation-attribute – Chris McKelt Nov 15 '14 at 08:16

12 Answers12

125

RequiredIf validation attribute

I've written a RequiredIfAttribute that requires a particular property value when a different property has a certain value (what you require) or when a different property has anything but a specific value.

This is the code that may help:

/// <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;
    }
}
Robert Koritnik
  • 103,639
  • 52
  • 277
  • 404
  • 2
    well done, love it, will be adding a not parameter, checking only if its not something rather that only if it is ;) - thanks – Pakk Nov 10 '15 at 20:51
  • 1
    Could you provide how to use this attribute? I can't figure out in my ViewModel how to get the data and pass it to the attribute. – Philippe Nov 17 '16 at 18:56
  • 2
    @Pakk That's actually already part of this code. See the property `IsInverted` which acts as the inverted if... – Robert Koritnik Nov 18 '16 at 07:29
  • 1
    @Philippe: This is an attribute that is usually run directly by the frameworks, so you don't really need to pass it any data. You just declaratively set it on your data model POCO and that's it. A classic example would be web shop. During invoice creation users are asked whether this is personal or company purchase. If they check *company* a bunch of other fields become required. Such data model properties (related to those fields) would have this attribute on them `[RequiredIf('IsCompany', true)]` where `IsCompany: bool` is usually bound to a checkbox. Hope this helps. – Robert Koritnik Nov 18 '16 at 08:59
  • 5
    well done, one question: Is it possible to add unobtrusive validation to this? – Sirwan Afifi Aug 26 '17 at 17:50
  • 1
    @RobertKoritnik How would you say RequiredIf('Something', Not Null ????) – Pomster Nov 15 '18 at 12:20
  • This is great...but could i use it against a drop down and validate if 2 out of the 5 items are selected? – Stephen Apr 25 '19 at 16:35
  • How do you get the `OtherPropertyDisplayName` without having to pass it in as yet another string parameter to the attribute? – Vasily Hall Feb 28 '20 at 07:54
52

Conditionally required property using data annotations

 [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));
        }
    }
}
StevieTimes
  • 389
  • 3
  • 9
Ghebrehiywet
  • 884
  • 3
  • 12
  • 20
  • Hi, I used it in combination with radio button, it works but, after validation fires, when I change the dependent value, it didn't clear validation message. Any Idea how can I achieve that? – Bambam Deo Jun 27 '22 at 07:26
10

Out of the box I think this is still not possible.

But I found this promising article about Mvc.ValidationToolkit (also here, unfortunately this is only alpha, but you probably could also just extract the method(s) you need from this code and integrate it on your own), it contains the nice sounding attribute RequiredIf which seems to match exactly your cause:

  • you download the project from the linked zip and build it
  • get the built dll from your build folder and reference it in the project you are using
  • unfortunately this seems to require reference to MVC, too (easiest way to have that is starting an MVC-Project in VS or install-package Microsoft.AspNet.Mvc)
  • in the files where you want to use it, you add using Mvc.ValidationToolkit;
  • then you are able to write things like [RequiredIf("DocumentType", 2)] or [RequiredIf("DocumentType", 1)], so objects are valid if neither name or name2 are supplied as long as DocumentType is not equal to 1 or 2
DrCopyPaste
  • 4,023
  • 1
  • 22
  • 57
10

Check out Fluent Validation

https://www.nuget.org/packages/FluentValidation/

Project Description A small validation library for .NET that uses a fluent interface and lambda expressions for building validation rules for your business objects.

https://github.com/JeremySkinner/FluentValidation

Shimmy Weitzhandler
  • 101,809
  • 122
  • 424
  • 632
Chris McKelt
  • 1,378
  • 2
  • 17
  • 38
5

I have always used implemented IValidatableObject from System.ComponentModel.DataAnnotations;

Example below

  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) });
                }
            }
chris castle
  • 144
  • 2
  • 5
  • Kudos. I always prefer to use what's included in the .NET Framework before adding external libraries or writing specific logic. – SteveB Jun 29 '21 at 17:01
  • Too bad this doesn't trigger in the front-end like the data annotations. – Enrico Jul 05 '22 at 12:12
  • This is the simplest answer, and it works for me in Blazor's (.NET 7) front end when I call `editContext.Validate()`. – Brian Pursley May 08 '23 at 13:12
2

check out the ExpressiveAnnotations .net library Git reference

It has 'RequiredIf' and 'AssertThat' validation attributes

DiTap
  • 361
  • 2
  • 5
1

Check out MVC Foolproof validation. It has data annotation in model like RequiredIf (dependent Property, dependent value) if I remember correctly. You can download Foolproof from:
Visual Studio(2017) -> Tools -> Nuget Package Manager -> Manage Nuget Packages for Solution. Reference mvcfoolproof.unobtrusive.min.js in addition to the jquery files.

Jaggan_j
  • 488
  • 1
  • 8
  • 9
1

I solved this by extending the RequiredAttribute class, borrowing some logic from the CompareAttribute and Robert's excellent solution:

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

Example usage:

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

This way, you get all the standard Required validation features.

N.B.: I am using .NET 5, but I tried to remove language features added in c# 9.0 for wider compatibility.

Connor Low
  • 5,900
  • 3
  • 31
  • 52
1

I wrote a simple custom validation attribute that it's very readable.

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() != "";
            }
        }
    }
}

You can use it like this

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;}
}
Franco Dipre
  • 371
  • 3
  • 9
0

I know this question is from a long time ago but someone asked in the comments section of Robert's answer how to use unobtrusive as part of the solution.

I wanted client side validation as well so I'm sharing my revised code to Robert's original code. It's essentially the same code except it implements IClientModelValidator and has an additional AddValidation method. The client validation still respects the IsInverted property.

Implement IClientModelValidator

public sealed class RequiredIfAttribute : ValidationAttribute, IClientModelValidator

New AddValidation method

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

Full Code

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

This should just work, provided you have included jquery.js, jquery.validate.js and jquery.validate.unobtrusive.js script files (in that order) to your layout or razor view.

Neil
  • 601
  • 4
  • 20
  • `Parameter count mismatch error at PropertyInfo.GetValue()` (at line 85 in my case) at the `var model` variable. I couldn't fix it, I even tried this one: https://stackoverflow.com/questions/31835823/parameter-count-mismatch-in-property-getvalue?noredirect=1&lq=1 but other errors started showing up, can you please revise your code. It would be so useful. – bzmind Aug 29 '22 at 15:47
  • An alternative solution that worked fine for me https://stackoverflow.com/q/63680820/11982162, although I had to do some tweaks on it, and I mentioned that in its comments as well. – bzmind Aug 30 '22 at 10:11
0

Here's my personal favorite RequriedIfAny. I've added the ability to add multiple property conditions.

public class RequiredIfAnyAttribute : ValidationAttribute
{
    private readonly RequiredAttribute _innerAttribute = new RequiredAttribute();
    public string[] _dependentProperties { get; set; }
    public List<List<object>> _targetValues { get; set; }

    public RequiredIfAnyAttribute(params object[] dependentPropertiesAndValues)
    {
        if (dependentPropertiesAndValues.Length % 2 != 0)
        {
            throw new ArgumentException("The number of dependent properties and values should be even.");
        }

        _dependentProperties = new string[dependentPropertiesAndValues.Length / 2];
        _targetValues = new List<List<object>>();

        for (int i = 0; i < dependentPropertiesAndValues.Length; i += 2)
        {
            string dependentProperty = (string)dependentPropertiesAndValues[i];
            IEnumerable targetValues = (IEnumerable)dependentPropertiesAndValues[i + 1];
            _dependentProperties[i / 2] = dependentProperty;
            _targetValues.Add(targetValues.Cast<object>().ToList());
        }
    }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        if (_dependentProperties.Length != _targetValues.Count)
        {
            throw new ArgumentException("Number of dependent properties should match the number of target values lists.");
        }

        bool anyConditionMet = false;

        for (int i = 0; i < _dependentProperties.Length; i++)
        {
            var field = validationContext.ObjectType.GetProperty(_dependentProperties[i]);
            if (field != null)
            {
                var dependentValue = field.GetValue(validationContext.ObjectInstance, null);
                if (_targetValues[i].Contains(dependentValue))
                {
                    anyConditionMet = true;
                    break;
                }
            }
            else
            {
                return new ValidationResult(FormatErrorMessage(_dependentProperties[i]));
            }
        }

        if (!anyConditionMet)
        {
            return ValidationResult.Success;
        }

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

You can use it just like some of the RequiredIf examples here except you can describe multiple property conditions. For example; If I only want to require the Age property when the Country property is "Canada" or "US" or the ItemType property is equal to "Tobacco" or "Alcohol" then it would look like this;

public string Country { get; set; }
public string ItemType { get; set; }

[RequiredIfAny("Country", new[] { "Canada", "US" }, "ItemType", new[] { "Tobacco", "Alcohol" }, ErrorMessage = "Age is required.")]
public int Age { get; set; }
clamchoda
  • 4,411
  • 2
  • 36
  • 74
-4

I can't give you exactly what you're asking for, but have you considered something like the following?

public abstract class Document // or interface, whichever is appropriate for you
{
    //some non-validted common properties
}

public class ValidatedDocument : Document
{
    [Required]
    public string Name {get;set;}
}

public class AnotherValidatedDocument : Document
{
    [Required]
    public string Name {get;set;}

    //I would suggest finding a descriptive name for this instead of Name2, 
    //Name2 doesn't make it clear what it's for
    public string Name2 {get;set;}
}

public class NonValidatedDocument : Document
{
    public string Name {get;set;}
}

//Etc...

Justification being the int DocumentType variable. You could replace this with using concrete subclass types for each "type" of document you need to deal with. Doing this gives you much better control of your property annotations.

It also appears that only some of your properties are needed in different situations, which could be a sign that your document class is trying to do too much, and supports the suggestion above.

Patrick Allwood
  • 1,822
  • 17
  • 21