As @seagull and @cesar mentioned, the default class ASP.NET uses for form validation errors is input-validation-error. This shows up in both client-side validation (via the jQuery Unobtrusive Validation library) and server-side (via the HtmlHelper class).
Therefore, both client-side and server-side validation need to be addressed for a complete solution.
Client-side
Update the default _ValidationScriptsPartial.cshtml file like so:
<script src="~/lib/jquery-validate/jquery.validate.min.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>
<script>
// Change validation classes to work with Bootstrap
const settings = {
validClass: "is-valid",
errorClass: "is-invalid"
};
$.validator.setDefaults(settings);
$.validator.unobtrusive.options = settings;
</script>
This allows the validation JavaScript to run as normal, but swaps in the correct Bootstrap classes. (Depending on the template you used, the paths on the first two line might need to be changed.)
Server-side
Because the class on the server side is hardcoded and can't be changed, we need to update the classes after validation runs. This can be done using a custom Tag Helper. Create a file (named InvalidClassTagHelper.cs) in your project with the following code:
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.TagHelpers;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
using System.Text.Encodings.Web;
/// <summary>
/// This <see cref="ITagHelper"/> implementation adds the Bootstrap-compatible class "is-invalid" to form elements
/// <c>input</c>, <c>select</c>, and <c>textarea</c> if they have already been marked as invalid with the ASP.NET
/// default class "input-validation-error".
/// </summary>
[HtmlTargetElement("input", Attributes = ForAttributeName)]
[HtmlTargetElement("select", Attributes = ForAttributeName)]
[HtmlTargetElement("textarea", Attributes = ForAttributeName)]
public class InvalidClassTagHelper : TagHelper
{
private const string ForAttributeName = "asp-for";
private const string ClassAttributeName = "class";
private const string BootstrapInvalidElementClass = "is-invalid";
[HtmlAttributeName(ForAttributeName)]
public ModelExpression? For { get; set; }
// If the element contains the default ASP.NET class indicating a validation error,
// this will add the corresponding Bootstrap class.
public override void Process(TagHelperContext context, TagHelperOutput output)
{
base.Process(context, output);
if (output.Attributes.TryGetAttribute(ClassAttributeName, out var classAttribute) &&
ExtractClassValue(classAttribute).Contains(HtmlHelper.ValidationInputCssClassName))
{
output.AddClass(BootstrapInvalidElementClass, HtmlEncoder.Default);
}
}
// Gets the current value of the element's class attribute.
// Adapted from https://github.com/dotnet/aspnetcore/blob/ab9e2630e6144efc529bfa8c67caa68732c80086/src/Mvc/Mvc.TagHelpers/src/TagHelperOutputExtensions.cs#L257-L283
private static string ExtractClassValue(TagHelperAttribute classAttribute)
{
string? extractedClassValue;
switch (classAttribute.Value)
{
case string valueAsString:
extractedClassValue = HtmlEncoder.Default.Encode(valueAsString);
break;
case HtmlString valueAsHtmlString:
extractedClassValue = valueAsHtmlString.Value;
break;
case IHtmlContent valueAsHtmlContent:
using (var stringWriter = new StringWriter())
{
valueAsHtmlContent.WriteTo(stringWriter, HtmlEncoder.Default);
extractedClassValue = stringWriter.ToString();
}
break;
default:
extractedClassValue = null;
break;
}
return extractedClassValue ?? string.Empty;
}
}
The Tag Helper checks <input>, <select>, and <textarea> elements for the default validation error class, and if present, it adds the Bootstrap-compatible class as well.