'How to make RequiredIf Client-side and server-side validation in ASP.NET Core 3.1 MVC?
I have to make client-side and server-side validation in ASP.NET Core 3.1. I have gone through many blogs but didn't get enough idea in ASP.NET Core MVC.
I have to make a RequiredIf kind of validation. When BusinessType is Business, the VATNumber field is required, but this field is optional if BusinessType is Personal. Please help.
public enum BusinessType
{
Personal,
Business
}
public class Profile
{
[Required(ErrorMessage = "Please enter name")]
public string Name { get; set; }
[Required(ErrorMessage = "Please enter email")]
[EmailAddress]
public string Email { get; set; }
[Required(ErrorMessage = "Please choose business type")]
[EnumDataType(typeof(BusinessType))]
public BusinessType BusinessType { get; set; }
[Display(Name="VAT Number")]
public string VATNumber { get; set; }
}
Solution 1:[1]
Finally, I have implemented both client-side and server-side validation to achieve my requirement. Here is the complete code:
Enum
public enum BusinessType
{
Personal,
Business
}
Profile Model
public class Profile
{
[Required(ErrorMessage = "Please enter name")]
public string Name { get; set; }
[Required(ErrorMessage = "Please enter email")]
[EmailAddress]
public string Email { get; set; }
[Required(ErrorMessage = "Please choose business type")]
[EnumDataType(typeof(BusinessType))]
public BusinessType BusinessType { get; set; }
[RequiredIf("BusinessType", BusinessType.Business, ErrorMessage = "VAT number is required")]
[Display(Name = "VAT Number")]
public string VATNumber { get; set; }
}
RequiredIfAttribute class
public class RequiredIfAttribute : ValidationAttribute, IClientModelValidator
{
public string PropertyName { get; set; }
public object Value { get; set; }
public RequiredIfAttribute(string propertyName, object value, string errorMessage = "")
{
PropertyName = propertyName;
ErrorMessage = errorMessage;
Value = value;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var instance = validationContext.ObjectInstance;
var type = instance.GetType();
var proprtyvalue = type.GetProperty(PropertyName).GetValue(instance, null);
if (proprtyvalue != null)
{
if (proprtyvalue.ToString() == Value.ToString() && value == null)
{
return new ValidationResult(ErrorMessage);
}
}
return ValidationResult.Success;
}
public void AddValidation(ClientModelValidationContext context)
{
context.Attributes.Add("data-val", "true");
context.Attributes.Add("data-val-vatNumber", ErrorMessage);
context.Attributes.Add("data-val-vatNumber-businessType", Value.ToString());
}
}
requiredIfBusinessTypeValidate.js
$.validator.addMethod('vatNumber', function (value, element, params) {
var genre = $(params[0]).val(), businessType = params[1], vat = value;
var selectedBusinessType = $("#BusinessType option:selected").text();
if (selectedBusinessType === businessType) {
if (value.length === 0) {
return false;
} else {
return true;
}
} else {
return true;
}
});
$.validator.unobtrusive.adapters.add('vatNumber', ['businessType'], function (options) {
var element = $(options.form).find('select#BusinessType')[0];
options.rules['vatNumber'] = [element, options.params['businessType']];
options.messages['vatNumber'] = options.message;
});
View
<h4>Profile</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form asp-action="Index">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Name" class="control-label"></label>
<input asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Email" class="control-label"></label>
<input asp-for="Email" class="form-control" />
<span asp-validation-for="Email" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="BusinessType" class="control-label"></label>
<select asp-for="BusinessType" class="form-control" asp-items="Html.GetEnumSelectList<BusinessType>()">
<option value="">Choose</option>
</select>
<span asp-validation-for="BusinessType" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="VATNumber" class="control-label"></label>
<input asp-for="VATNumber" class="form-control" />
<span asp-validation-for="VATNumber" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
</div>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
<script src="~/js/requiredIfBusinessTypeValidate.js"></script>
}
Controller
public class ProfileController : Controller
{
public ActionResult Index()
{
return View();
}
[HttpPost]
public ActionResult Index(Profile profile)
{
if (ModelState.IsValid)
{
// Write business logic here ...
}
return View(profile);
}
}
Solution 2:[2]
You could custom validation like below:
public class RequieredIfAttribute : ValidationAttribute
{
private readonly string _otherProperty;
public RequieredIfAttribute(string otherProperty)
{
_otherProperty = otherProperty;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
string data = (string)value;
BusinessType selectedType = (BusinessType)validationContext.ObjectType.GetProperty(_otherProperty).GetValue(validationContext.ObjectInstance, null);
if (string.IsNullOrEmpty(data) && selectedType == BusinessType.Business)
{
return new ValidationResult("VATNumber is requiered.");
}
return ValidationResult.Success;
}
}
Model:
public class Profile
{
[Required(ErrorMessage = "Please enter name")]
public string Name { get; set; }
[Required(ErrorMessage = "Please enter email")]
[EmailAddress]
public string Email { get; set; }
[Required(ErrorMessage = "Please choose business type")]
[EnumDataType(typeof(BusinessType))]
public BusinessType BusinessType { get; set; }
[RequieredIf("BusinessType")] //add this...
[Display(Name = "VAT Number")]
public string VATNumber { get; set; }
}
View(Index.cshtml):
@model Profile
<form asp-action="Create">
<select asp-for="BusinessType" asp-items="Html.GetEnumSelectList<BusinessType>()"></select>
<div class="form-group">
<label asp-for="VATNumber" class="control-label"></label>
<input asp-for="VATNumber" class="form-control" />
<span asp-validation-for="VATNumber" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Email" class="control-label"></label>
<input asp-for="Email" class="form-control" />
<span asp-validation-for="Email" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Name" class="control-label"></label>
<input asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
Controller:
[HttpPost]
public IActionResult Create(Profile profile)
{
if(!ModelState.IsValid)
{
//do your stuff...
return View("Index");
}
//do your stuff...
return View("Privacy");
}
Solution 3:[3]
Intead of using hardcoded names we can make it dynamic based on the anotation values. Here is the server side
/// <summary>
/// Provides conditional validation based on related property value.
/// </summary>
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public sealed class RequiredIfAttribute : RequiredAttribute, 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()
{
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;
}
public void AddValidation(ClientModelValidationContext context)
{
context.Attributes.Add("data-val", "true");
context.Attributes.Add("data-val-required-if", ErrorMessage);
context.Attributes.Add("data-val-required-if-other-name", OtherProperty);
context.Attributes.Add("data-val-required-if-other-value", OtherPropertyValue.ToString());
}
}
And here is the client side required-if.js
$.validator.addMethod('required-if', function (value, element, params) {
var targetValue = params['other-value'];
var otherElement = $("#" + params['other-name']);
var otherCurrentValue = otherElement.val();
if (targetValue != otherCurrentValue) {
return true;
}
if (value.length === 0) {
return false;
} else {
return true;
}
});
$.validator.unobtrusive.adapters.add('required-if', ['other-name', 'other-value'], function (options) {
options.rules['required-if'] = [options.element, options.params];
options.messages['required-if'] = options.message;
options.messages['required'] = options.message;
});
and here is the usage
[RequiredIf("OtherPropertyName", otherPropertyValue: true, AllowEmptyStrings = false, ErrorMessage = "This field is required")]
public string RequiredPropertyTest { get; set; }
Now when the OtherPropertyName has a value true and RequiredPropertyTest is empty the validation error will popup
Solution 4:[4]
Adding to Sahib's answer, which is really working well for me, you obviously need to add:
using System;
using System.ComponentModel.DataAnnotations;
using System.Reflection;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using System.Globalization;
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | Simant |
| Solution 2 | |
| Solution 3 | Sahib Khan |
| Solution 4 | Nico |

