Requirements
- Custom validation attribute that contains the server-side validation logic
AttributeAdapterwhich is responsible for renderingdata-val-*attributes on the HTML form control so that it can take part in client side validationAttributeAdapterProviderregisters the adapter with the DI/validation system. The framework itself registers multiple adapters with one adapter provider:ValidationAttributeAdapterProvider
Custom Validation Attribute
using System.ComponentModel.DataAnnotations;
namespace IntranetCore.Web.ValidationAttributes;
/// <summary>
/// Requires all properties to be set if any of the properties are set
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter)]
public class RequireAllIfAnyAttribute(params string[] propertyNames) : ValidationAttribute
{
public string[] PropertyNames { get; } = propertyNames;
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (PropertyNames.Any(prop => !string.IsNullOrWhiteSpace(validationContext.ObjectType.GetProperty(prop).GetValue(validationContext.ObjectInstance)?.ToString())))
{
bool allFilled = PropertyNames.All(prop => !string.IsNullOrWhiteSpace(validationContext.ObjectType.GetProperty(prop).GetValue(validationContext.ObjectInstance)?.ToString()));
if (!allFilled)
{
return new ValidationResult(ErrorMessage ?? "All Proof Email fields (Template, From Address, Subject) are required if any one is filled.");
}
}
return ValidationResult.Success;
}
}
This code defines a custom validation attribute named RequireAllIfAnyAttribute in C#. The purpose of this attribute is to enforce a rule where if any of the specified properties are filled in (i.e., not empty or whitespace), then all of the specified properties must be filled in.
Here's a breakdown of how it works:
Constructor:
- The constructor takes an array of property names (
params string[] propertyNames) that need to be validated together. - The
propertyNamesarray is stored in the private fieldpropertyNames.
- The constructor takes an array of property names (
IsValidMethod:- This is the main validation logic. It overrides the
IsValidmethod from the baseValidationAttributeclass.
- This is the main validation logic. It overrides the
Check if any properties are filled:
- The code uses LINQ's
Anymethod to check if any of the specified properties (propertyNames) are filled (i.e., not null, empty, or whitespace). - If none of the properties are filled, the validation passes (returning
ValidationResult.Success).
- The code uses LINQ's
Check if all properties are filled:
- If at least one property is filled, it then checks if all specified properties are filled using LINQ's
Allmethod. - If not all properties are filled, it returns a
ValidationResultwith an error message indicating that all specified fields are required if any one is filled.
- If at least one property is filled, it then checks if all specified properties are filled using LINQ's
AttributeAdapter
This code defines a class RequireAllIfAnyAttributeAdapter, which acts as an adapter for the RequireAllIfAnyAttribute custom validation attribute. The adapter class is used to integrate the server-side validation attribute with client-side validation in ASP.NET Core.
using Microsoft.AspNetCore.Mvc.DataAnnotations;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.Extensions.Localization;
namespace IntranetCore.Web.ValidationAttributes;
public class RequireAllIfAnyAttributeAdapter(RequireAllIfAnyAttribute attribute, IStringLocalizer stringLocalizer) :
AttributeAdapterBase<RequireAllIfAnyAttribute>(attribute, stringLocalizer)
{
public override void AddValidation(ClientModelValidationContext context)
{
MergeAttribute(context.Attributes, "data-val", "true");
MergeAttribute(context.Attributes, "data-val-require-all-if-any", GetErrorMessage(context));
var attribute = context.ModelMetadata.ValidatorMetadata.OfType<RequireAllIfAnyAttribute>().FirstOrDefault();
if (attribute != null)
{
MergeAttribute(context.Attributes, "data-val-require-all-if-any-properties", string.Join(",", attribute.PropertyNames));
}
}
public override string GetErrorMessage(ModelValidationContextBase validationContext) =>
GetErrorMessage(validationContext.ModelMetadata, validationContext.ModelMetadata.GetDisplayName());
}
Here's how it works:
Constructor:
- The constructor takes an instance of
RequireAllIfAnyAttributeand anIStringLocalizer. - It passes these parameters to the base class
AttributeAdapterBase, which handles the localization of validation messages.
- The constructor takes an instance of
AddValidationMethod:- This method is overridden to add the necessary HTML5 data attributes for client-side validation.
MergeAttributeis a helper method that adds or updates the specified attribute in thecontext.Attributesdictionary.- It adds the
data-valattribute with the value "true" to indicate that client-side validation is enabled. - It adds the
data-val-require-all-if-anyattribute with the value returned by theGetErrorMessagemethod. This attribute holds the error message to be displayed if the validation fails.
- It adds the
GetErrorMessageMethod:- This method is overridden to provide the error message for the client-side validation.
- It calls the base class's
GetErrorMessagemethod, passing the model metadata and display name of the property being validated.
Attribute Adapter Provider
The RequireAllIfAnyAdapterProvider class implements the IValidationAttributeAdapterProvider interface. Its purpose is to provide a custom validation adapter for a specific attribute (RequireAllIfAnyAttribute). If the attribute is not RequireAllIfAnyAttribute, it falls back to a base provider for other attributes.
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc.DataAnnotations;
using Microsoft.Extensions.Localization;
public class RequireAllIfAnyAdapterProvider : IValidationAttributeAdapterProvider
{
private readonly IValidationAttributeAdapterProvider baseProvider = new ValidationAttributeAdapterProvider();
public IAttributeAdapter GetAttributeAdapter(ValidationAttribute attribute, IStringLocalizer stringLocalizer)
{
if (attribute is RequireAllIfAnyAttribute)
{
return new RequireAllIfAnyAttributeAdapter(attribute as RequireAllIfAnyAttribute, stringLocalizer);
}
else
{
return baseProvider.GetAttributeAdapter(attribute, stringLocalizer);
}
}
}
Register the AdapterProvider
builder.Services.AddSingleton<IValidationAttributeAdapterProvider, RequireAllIfAnyAdapterProvider>();
Extend jQuery Validation
Add a validation method and an adapter
$.validator.addMethod('require-all-if-any', function (value, element, params) {
var properties = params.properties.split(',');
var allFilled = true;
var anyFilled = false;
for (var i = 0; i < properties.length; i++) {
var propertyValue = $('#' + properties[i]).val();
if (propertyValue) {
anyFilled = true;
} else {
allFilled = false;
}
} if (anyFilled && !allFilled) {
return false;
}
return true;
});
$.validator.unobtrusive.adapters.add('require-all-if-any', ['properties'], function (options) {
options.rules['require-all-if-any'] = { properties: options.params.properties };
options.messages['require-all-if-any'] = options.message;
});
$.validator.unobtrusive.adapters.add
The add method is used to create a custom adapter for an unobtrusive validation attribute. It allows you to define how your custom validation attribute should be interpreted and handled by the jQuery Unobtrusive Validation framework.
The add method takes three parameters:
- Adapter Name: 'require-all-if-any' - This is the name of the custom validation method.
- Dependent Parameters:
['properties']- This array lists the names of the additional parameters that should be passed to the custom validation method. In this case, it's the properties parameter. - Adapter Function: A function that defines how the validation rules and messages should be set up.
Adapter Function
The adapter function receives an options object, which contains various properties related to the validation, including parameters and messages.
options.rules:
options.rulesis an object that holds the validation rules for the element.options.rules['require-all-if-any']adds a new validation rule named require-all-if-any.{ properties: options.params.properties }sets the properties parameter for this rule. Theoptions.params.propertiesvalue comes from thedata-val-require-all-if-any-propertiesattribute that you added to the HTML element.
options.messages:
options.messagesis an object that holds the validation messages for the element.options.messages['require-all-if-any']sets the validation message for the require-all-if-any rule.options.messagecontains the error message specified in the attribute (e.g., "All fields (Proof Email Template, Proof From Email Address, Proof Email Subject) are required if a value is provided for any one of them").
IClientModelValidator
As an alternative to creating an AttributeAdapter and AttributeAdapterProvider, you can implement IClientModelValidator in the ValidationAttribute, which requires you to include the AddValidation method there:
public void AddValidation(ClientModelValidationContext context)
{
context.Attributes.TryAdd("data-val", "true");
context.Attributes.TryAdd("data-val-required-if", ErrorMessage ?? $"This field is required");
context.Attributes.TryAdd("data-val-required-if-property-name", PropertyName);
context.Attributes.TryAdd("data-val-required-if-expected-val", ExpectedValue?.ToString());
}
The AttributeAdapter approach is generally recommended as it separates server side validation (in the attribute) with client validation concerns (rendering data-val-* attributes).