Conditional validation in MVC.NET Core (RequiredIf)

Another, cleaner and more versatile, approach would be to implement a more generic attribute, not a specific "requiredIf" attribute, as you would have to make multiple custom attributes for every type of validation you happen to use.

Luckily, since .NET Core 2, Microsoft provides the IPropertyValidationFilter interface, that you can implement on a custom attribute. This interface defines a function ShouldValidateEntry, that allows control over whether the current entry should be validated or not; so this runs before any validators are called.

There is one default implementation in the Framework already, the ValidateNeverAttribute, but it is trivial to implement your own that does a conditional check on another value:

using System;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;

namespace Foo {
    // Implementation makes use of the IPropertyValidationFilter interface that allows
    // control over whether the attribute (and its children, if relevant) need to be
    // validated.
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
    public class ConditionalValidationAttribute : Attribute, IPropertyValidationFilter {
        public string OtherProperty { get; set; }
        public object OtherValue { get; set; }

        public ConditionalValidationAttribute(string otherProperty, object otherValue) {
            OtherProperty = otherProperty;
            OtherValue = otherValue;
        }

        public bool ShouldValidateEntry(ValidationEntry entry, ValidationEntry parentEntry) {
            // Default behaviour if no other property is set: continue validation
            if (string.IsNullOrWhiteSpace(OtherProperty)) return true;

            // Get the property specified by the name. Might not properly work with
            // nested properties.
            var prop = parentEntry.Metadata.Properties[OtherProperty]?.PropertyGetter?.Invoke(parentEntry.Model);

            return prop == OtherValue;
        }
    }
}

Just annotate the relevant properties with this attribute and any validators, also custom validators you implemented yourself, will only be called when necessary!

Implementation example: here


Based on the original implementation I'd recommend extending RequiredAttribute rather than ValidationAttribute - then your default ErrorMessage and other defaults are set as per [Required]. Either way the "errormessage" property is redundant as you already have this as a property of ValidationAttribute and the original code generates a warning for the ErrorMessage property - you can also use nameof for the attribute decoration as well to keep things a lot tighter in your code:

My implementation is slightly more specific so that if a property is a bool I can indicate that a property is required (if say a checkbox is ticked):

[AttributeUsage(AttributeTargets.Property)]
public class RequiredIfTrueAttribute : RequiredAttribute
{
    private string PropertyName { get; set; }

    public RequiredIfTrueAttribute(string propertyName)
    {
        PropertyName = propertyName;
    }

    protected override ValidationResult IsValid(object value, ValidationContext context)
    {
        object instance = context.ObjectInstance;
        Type type = instance.GetType();

        bool.TryParse(type.GetProperty(PropertyName).GetValue(instance)?.ToString(), out bool propertyValue);

        if (propertyValue && string.IsNullOrWhiteSpace(value?.ToString()))
        {
            return new ValidationResult(ErrorMessage);
        }

        return ValidationResult.Success;
    }
}

Example Usage:

public bool IsBusinessProfile { get; set; }

[RequiredIfTrue(nameof(IsBusinessProfile), ErrorMessage = "ABN is required for Business Profiles")]
public string Abn { get; set; }

I built on the answer provided by Rob. This one is a generic validator instead of inheriting from Required, and also provides client-side validation. I am using .Net Core 3.0

using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using System;
using System.Collections.Generic;
using System.Text;

namespace System.ComponentModel.DataAnnotations
{

    [AttributeUsage(AttributeTargets.Property)]
    public class RequiredIfTrueAttribute : ValidationAttribute, IClientModelValidator
    {
        private string PropertyName { get; set; }

        public RequiredIfTrueAttribute(string propertyName)
        {
            PropertyName = propertyName;
            ErrorMessage = "The {0} field is required."; //used if error message is not set on attribute itself
        }

        protected override ValidationResult IsValid(object value, ValidationContext context)
        {
            object instance = context.ObjectInstance;
            Type type = instance.GetType();

            bool.TryParse(type.GetProperty(PropertyName).GetValue(instance)?.ToString(), out bool propertyValue);

            if (propertyValue && (value == null || string.IsNullOrWhiteSpace(value.ToString())))
            {
                return new ValidationResult(ErrorMessage);
            }

            return ValidationResult.Success;
        }

        public void AddValidation(ClientModelValidationContext context)
        {
            MergeAttribute(context.Attributes, "data-val", "true");
            var errorMessage = FormatErrorMessage(context.ModelMetadata.GetDisplayName());
            MergeAttribute(context.Attributes, "data-val-requirediftrue", errorMessage);
        }

        private bool MergeAttribute(IDictionary<string, string> attributes, string key, string value)
        {
            if (attributes.ContainsKey(key))
            {
                return false;
            }
            attributes.Add(key, value);
            return true;
        }
    }
}

Client-Side Javascript

//Custom validation script for the RequiredIfTrue validator
/*
 * Note that, jQuery validation registers its rules before the DOM is loaded. 
 * If you try to register your adapter after the DOM is loaded, your rules will
 * not be processed. So wrap it in a self-executing function.
 * */
(function ($) {

    var $jQval = $.validator;

   $jQval.addMethod("requirediftrue",
       function (value, element, parameters) {
            return value !== "" && value != null;
        }
    );

    var adapters = $jQval.unobtrusive.adapters;
    adapters.addBool('requirediftrue');

})(jQuery);

Usage

    public bool IsSpecialField { get; set; }

    [RequiredIfTrue(nameof(IsSpecialField), ErrorMessage="This is my custom error message")]
    [Display(Name = "Address 1")]
    public string Address1 { get; set; }

    [RequiredIfTrue(nameof(IsSpecialField))]
    public string City { get; set; }