Centralized model validation both for MVC/WebApi and SPA client-side validation using FluentValidation
Validation is one of the crucial parts of any application. It has to validate on both client side and server side requests.
The above looks pretty simplified, and names say it all. The little twist is, it can hold validation rules for any addition, update or deletion of a model. An example can be seen from above through RuleSet of Insert through OperationRequest.Insert enum.
The above code includes Common rules for any CRUD operation which would be shared in all operations.
Here is code for OperationRequest enum and Base class from above.
Enum
Base Class
Above two items are simple enough, after this, we just need to register it in MVC service collection for dependency injection.
We are not going to register in-built fluent validation on assembly level instead register on DI and use it on our custom smart filter for validation of actions and generation of JSON for client-side validation.
Startup.cs
That completes FluentValidation part.
Codes of Action filter attribute
JqueryValidatorProvider
This class is responsible for generating JSON for general form validation. This is done by looking into FluentValidation models meta information and ultimately creating JSON which helps in form validation.
The implementation could be changed based on your choice of the client-side validator, and server-side codes would accordingly change.
From codes, remote validation would only enable when it did not fall in any category of validation.
That's an end of server-side codes. Client-side codes are relatively simple.
FormValidation.ts
Usage
Form.ts was explained in the earlier blog post, but please refer to Form.ts in case of any confusion. The function RegisterJqueryValidation would take care of everything.
What are target features or implementation from this article?
- Model validation for any given model.
- Centralized/One code for validation on both server-side and client-side.
- Automatic validation of model without writing any extra codes on/under actions for validation.
- NO EXTRA/ANY codes on client-side to validate any form.
- Compatible with SPA.
- Can be compatible with any client-side validation framework/library. Like Angular Reactive form validation or any jquery validation libraries.
Tools used in the implementation?
- FluentValidation: I feel DataAnnotation validation are excellent and simple to use, but in case of complex validation or writing any custom validations are always tricker and need to write a lot of codes to achieve whereas FluentValidations are simple even in case of complex validation. Generally, we need to validate incoming input against database values, which are straight-forward in FluentValidation. Also, it is not bound to be used only with MVC it can implement at any layers or any front-end, and on the entry point, it just need initialization. Also, the Fluent approach is similar to Entity Framework Fluent mapping so an identical pattern.
- jquery-validation: In case of MVC and as long as cshtml/Views are used it is effortless to implement client-side validation since it is automatically taken care by auto-generated unobtrusive validation under rendered HTML. No work at all there unless some remote validations are required. In case of SPA, it gets hugely affected since rendering engines are on client-side and Microsoft Unobtrusive validation won't play a role.
jquery-validation works based on JSON data, we would be getting rules and messages from server side and applying validation on the form through centralized code implementation. So, one code would take care of everything. - MVC and ActionFilterAttribute: The filter to generate validation rules based on JS validation library, validate model when posted on the server and remote validation for given field if it needs any check against database/server side codes.
Composing Fluent validation for the view model
Let's first directly begin with composing fluent validation and later look into Filter implementation. The validation is straightforward but a bit of enhancement through RuleSet and implementation of a little wrapper on base class BaseValidation.
/// <summary>
/// Fluent validation for <see cref="ContactViewModel"/> view model.
/// </summary>
/// <seealso cref="BaseValidation{ContactViewModel}" />
public class ContactViewModelValidation
: BaseValidation<ContactViewModel>
{
/// <summary>
/// Initializes a new instance of the <see cref="ContactViewModelValidation"/> class.
/// </summary>
public ContactViewModelValidation()
{
RuleFor(model => model.Name)
.NotEmpty()
.Length(3, 100);
RuleFor(model => model.Email)
.NotEmpty()
.EmailAddress();
RuleFor(model => model.Message)
.NotEmpty()
.Length(5, 2000);
RuleFor(model => model.Subject)
.NotEmpty();
RuleSet(OperationRequest.Insert, () =>
{
RuleFor(model => model.Message)
.Must((model, field, token) =>
{
// TODO: Check against database if Email and message already exists.
return false;
}).WithMessage("Your information is already submitted.")
;//.When(model => !String.IsNullOrEmpty(model.Message) && String.IsNullOrEmpty(model.Email));
});
}
}
The above looks pretty simplified, and names say it all. The little twist is, it can hold validation rules for any addition, update or deletion of a model. An example can be seen from above through RuleSet of Insert through OperationRequest.Insert enum.
The above code includes Common rules for any CRUD operation which would be shared in all operations.
Here is code for OperationRequest enum and Base class from above.
Enum
/// <summary>
/// Operation request
/// </summary>
public enum OperationRequest
{
/// <summary>
/// The insert request
/// </summary>
Insert,
/// <summary>
/// The update request
/// </summary>
Update,
/// <summary>
/// The delete request
/// </summary>
Delete
}
Base Class
/// <summary>
/// Base validation for all Fluent validations.
/// </summary>
/// <typeparam name="TModel">The type of the model.</typeparam>
/// <seealso cref="AbstractValidator{TModel}" />
public class BaseValidation<TModel>
: AbstractValidator<TModel>
{
/// <summary>
/// Defines a RuleSet that can be used to group together several validators.
/// </summary>
/// <param name="ruleSetName">The name of the ruleset.</param>
/// <param name="action">Action that encapsulates the rules in the ruleset.</param>
protected void RuleSet(OperationRequest ruleSetName, Action action)
{
RuleSet(ruleSetName.ToString(), action);
}
}
Above two items are simple enough, after this, we just need to register it in MVC service collection for dependency injection.
We are not going to register in-built fluent validation on assembly level instead register on DI and use it on our custom smart filter for validation of actions and generation of JSON for client-side validation.
/// <summary>
/// Extension to Register all Fluent validations.
/// </summary>
public static class ExtensionRegisterValidation
{
/// <summary>
/// Adds FluentValidation for the application.
/// </summary>
/// <param name="services">The services.</param>
/// <returns>The Service collection.</returns>
public static IServiceCollection AddFluentValidation(this IServiceCollection services)
{
services.AddScoped<IValidator<ContactViewModel>, ContactViewModelValidation>();
return services;
}
}
Startup.cs
services.AddFluentValidation();
That completes FluentValidation part.
Filter to auto-trigger FluentValidation and generation of client-side validation rules along with support of remote validation
I have already explained three responsibilities of same, let's look in detail:
- Model validation for action. We discussed four modes of FluentValidation including Common for all kind of CRUD operation using OperationRequest seen earlier. If you see GetValidationRule function it retrieves RuleSet for FluentValidation. Common RuleSet is named as Default under FluentValidation. While creating Fluent Validator, we can specify an array of RuleSets. The function generates RuleSet based on Action name in MVC/WebApi so that we do not have to put Validate filter by specifying RuleSets manually. Do modify based on your need as of current implementation it pulls from action name containing add, create, insert, delete, update. So, action name like CreateCustomer, AddCustomer, UpdateCustomerAsync would be automatically resolved.
- Remote validation: In this, an AJAX call would be made for a field to validate on the server side. The trick is, it would request the same URL with query string validate with property name to validate. From code level, you can see some condition with validate checks are done for the same reason. A JsonResult is sent from this. An example:
- Validation Rules and messages: This is a static data served when a form is rendered, this would contain necessary information about validations for fields used in the form. This, JSON generation is achieved through JqueryValidatorProvider class implementation, and the result is this:
Validation Rules for the entire form generated from JqueryValidatorProvider |
Codes of Action filter attribute
/// <summary>
/// Validation filter for model state validation.
/// </summary>
/// <seealso cref="ActionFilterAttribute" />
public sealed partial class ValidationResponseFilterAttribute
: ActionFilterAttribute
{
/// <summary>
/// Action execution on any controller.
/// </summary>
/// <param name="context">Action context.</param>
public override void OnActionExecuting(ActionExecutingContext context)
{
if (context == null)
{
return;
}
var model = GetTheActionArgument(context);
if (model == null && context.HttpContext.Request.Query.TryGetValue("validate", out var _))
{
context.Result = new JsonResult(string.Empty);
return;
}
if (model == null)
{
return;
}
var validator = context.HttpContext.RequestServices
.GetService(typeof(IValidator<>)
.MakeGenericType(model.ModelType)) as IValidator;
// In case of no validator and URL requested for validation rule
if (validator == null && context.HttpContext.Request.Query.Keys.Any(val => val == "validation"))
{
context.Result = new EmptyResult();
return;
}
if (validator == null)
{
return;
}
if (context.HttpContext.Request.Query.TryGetValue("validation", out var clientRuleRequest))
{
switch (clientRuleRequest)
{
default:
context.Result = new JqueryValidatorProvider(validator).GetValidationRules();
break;
}
return;
}
model.ExecuteOnModel((modelValue) =>
{
var validationResult =
validator.Validate(new ValidationContext(
modelValue,
new PropertyChain(),
new RulesetValidatorSelector(GetValidationRule(context))));
validationResult.AddToModelState(context.ModelState, null);
});
// If requested for Ajax validation.
if (context.HttpContext.Request.Query.TryGetValue("validate", out var modelProperty))
{
context.Result = new JsonResult(context.ModelState.ToDictionary(
kvp => kvp.Key,
kvp => kvp.Value.Errors.Select(e => e.ErrorMessage).Humanize()));
return;
}
if (!context.ModelState.IsValid)
{
context.Result = new BadRequestObjectResult(context.ModelState);
}
}
/// <summary>
/// Gets the action argument.
/// </summary>
/// <param name="actionContext">The action context.</param>
/// <returns>Object type and object.</returns>
private static ModelDefination GetTheActionArgument(ActionExecutingContext actionContext)
{
return (from arg in actionContext.ActionArguments.Values
let typ = arg?.GetType()
where typ != null && !typ.IsPrimitive && !typ.IsValueType && typ != typeof(string)
//&& typ != typeof(Kendo.Mvc.UI.DataSourceRequest) TODO: Note could exclude any other types
select new ModelDefination(arg)).FirstOrDefault();
}
/// <summary>
/// Gets the validation rule set names for Fluent validation.
/// </summary>
/// <param name="actionContext">The action context.</param>
/// <returns>Validation rules for Fluent mapping</returns>
private static string[] GetValidationRule(ActionExecutingContext actionContext)
{
var ruleSetNames = new List<string>(2) { "default" };
if (actionContext.ActionDescriptor is ControllerActionDescriptor descriptor)
{
if (descriptor.ActionName.Contains(nameof(OperationRequest.Delete)))
{
ruleSetNames.Add(OperationRequest.Delete.ToString());
}
else if (descriptor.ActionName.Contains("Create") ||
descriptor.ActionName.Contains("Add") ||
descriptor.ActionName.Contains(nameof(OperationRequest.Insert)))
{
ruleSetNames.Add(OperationRequest.Insert.ToString());
}
else if (descriptor.ActionName.Contains(nameof(OperationRequest.Update)))
{
ruleSetNames.Add(OperationRequest.Update.ToString());
}
}
return ruleSetNames.ToArray();
}
}
JqueryValidatorProvider
This class is responsible for generating JSON for general form validation. This is done by looking into FluentValidation models meta information and ultimately creating JSON which helps in form validation.
The implementation could be changed based on your choice of the client-side validator, and server-side codes would accordingly change.
/// <summary>
/// jQuery.Validator rules provider.
/// </summary>
public class JqueryValidatorProvider
{
/// <summary>
/// The validator
/// </summary>
private readonly IValidator Validator;
/// <summary>
/// Initializes a new instance of the <see cref="JqueryValidatorProvider"/> class.
/// </summary>
/// <param name="validator">The validator.</param>
public JqueryValidatorProvider(IValidator validator)
{
Validator = validator;
}
/// <summary>
/// Gets the validation rules based on jQuery.validator.
/// </summary>
/// <returns>Result along with rules.</returns>
public IActionResult GetValidationRules()
{
if (Validator == null)
{
return new EmptyResult();
}
var validationObject = new Dictionary<string, object>();
var errorObject = new Dictionary<string, Dictionary<string, string>>();
var validatorDescriptor = Validator.CreateDescriptor();
foreach (var valida in validatorDescriptor.GetMembersWithValidators())
{
var validations = new Dictionary<string, object>();
var errors = new Dictionary<string, string>();
foreach (var propertyValidator in valida)
{
var validatorAdded = false;
Action<string, object, Action<MessageFormatter, Action<Func<string, string>>>> add = (validationType, value, format) =>
{
validatorAdded = true;
var formatter = new MessageFormatter();
formatter.AppendPropertyName(valida.Key.Humanize());
var template = propertyValidator.ErrorMessageSource.GetString(null);
format?.Invoke(formatter, (d) => { template = d?.Invoke(template); });
validations.SafeAdd(validationType, value);
errors.SafeAdd(validationType, formatter.BuildMessage(template));
};
if (propertyValidator is NotNullValidator
|| propertyValidator is NotEmptyValidator)
{
add("required", true, null);
}
if (propertyValidator is EmailValidator)
{
add("email", true, null);
}
if (propertyValidator is LengthValidator lengthValidator)
{
if (lengthValidator.Max > 0)
{
add(
"maxlength",
lengthValidator.Max,
(formatter, template) =>
{
formatter.AppendArgument("MinLength", lengthValidator.Min);
formatter.AppendArgument("MaxLength", lengthValidator.Max);
template?.Invoke((tem) => !tem.Contains("TotalLength") ? tem :
tem.Split(".")?.FirstOrDefault());
});
}
add(
"minlength",
lengthValidator.Min,
(formatter, template) =>
{
formatter.AppendArgument("MinLength", lengthValidator.Min);
formatter.AppendArgument("MaxLength", lengthValidator.Max);
template?.Invoke((tem) => !tem.Contains("TotalLength") ? tem :
tem.Split(".")?.FirstOrDefault());
});
}
if (propertyValidator is RegularExpressionValidator expressionValidator)
{
add("regex", expressionValidator.Expression, null);
}
if (!validatorAdded)
{
add("remote", valida.Key, null);
}
}
validations.IfNotEmpty(() => validationObject.Add(valida.Key, validations));
errors.IfNotEmpty(() => errorObject.Add(valida.Key, errors));
}
return new JsonResult(new
{
rules = validationObject,
messages = errorObject
});
}
}
From codes, remote validation would only enable when it did not fall in any category of validation.
That's an end of server-side codes. Client-side codes are relatively simple.
Client-side codes for validation
The client-side validation is entirely dependent on jquery-validation plugin since it is our choice for client-side validation.FormValidation.ts
import { injectable } from "inversify";
import { HttpRequestResponse } from './../RequestResponse/HttpRequestResponse';
@injectable()
class FormValidation {
private AppendError(form: JQuery<HTMLFormElement> | JQuery<HTMLElement>,
error: JQuery<HTMLElement>, element: JQuery<HTMLElement>) {
let errorSelector = $(`#${$(element).attr("id")}Error`);
errorSelector = errorSelector.length ? errorSelector : $(`#${$(element).attr("id")}-error`);
if (errorSelector.length) {
errorSelector.removeClass('field-validation-valid').addClass('field-validation-error');
errorSelector.text(error.text());
}
else {
$(element)
.parent().after(
`<div id='${$(element).attr('name')}Holder' class="field-validation-error"></div>`);
$(`#${(element).attr('name')}Holder`).append(error);
}
}
ParseModelStateError(data: JQuery.jqXHR<any>, eachErrorCallback: (message: string, propName: string) => void) {
if (data == undefined || data.responseJSON == undefined) {
return;
}
var message = '';
var propStrings = Object.keys(data.responseJSON);
$.each(propStrings, (errIndex, propString) => {
var propErrors = data.responseJSON[propString];
$.each(propErrors, (errMsgIndex, propError) => {
message += propError;
});
message += '\n';
eachErrorCallback(message, propString);
message = '';
});
}
private RemoteValidation(elementName: string,
form: JQuery<HTMLFormElement> | JQuery<HTMLElement>) {
let getData = () => {
let serializedData = {};
$(form).serializeArray()
.map((key) => { serializedData[key.name] = key.value; });
return JSON.stringify(serializedData);
}
let remote: JQueryAjaxSettings = {
url: `${form.attr('action')}?validate=${elementName}`,
type: "post",
async: true,
contentType: 'application/json',
beforeSend: (xhr, setting) => {
setting.data = getData();
},
dataFilter: (response) => {
if ($.parseJSON(response) == true) {
return response;
}
var data = $.parseJSON(response);
var validator: any = $(form).validate();
validator.invalid[elementName] = data[elementName] != undefined;
return JSON.stringify(data[elementName] != undefined ? data[elementName] : true);
}
};
return remote;
}
RegisterJqueryValidation(formSelector: string) {
let form = $(formSelector);
$.ajax({
url: form.attr('action') + '?validation=jquery',
data: '{}',
contentType: 'application/json',
method: 'post',
success: (validationRule: JQueryValidation.ValidationOptions) => {
if (validationRule != undefined && validationRule.rules != undefined) {
validationRule.errorPlacement = (error, element) =>
this.AppendError(form, error, element);
validationRule.success = (label, input) =>
label.empty();
$.each(validationRule.rules, (prop: any) => {
if (validationRule.rules[prop].hasOwnProperty('remote')) {
validationRule.rules[prop].remote = () => this.RemoteValidation(prop, form);
}
});
$.validator.setDefaults({ ignore: '' });
var validator = form.validate(validationRule);
}
}
});
}
}
export { FormValidation }
Usage
Form.ts was explained in the earlier blog post, but please refer to Form.ts in case of any confusion. The function RegisterJqueryValidation would take care of everything.
this.Validation.RegisterJqueryValidation(this.ContactViewOption.FormSelector);
this.EventHelper.RegisterClickEvent(
this.ContactViewOption.SubmitButtonSelector,
(evt, selector) => {
this.FormHelper.SubmitForm({
Source: {
ButtonEvent: evt
},
OnPostSuccessResult: (data) => {
console.log('Submitted successfully.');
}
});
});
Great Content. It will useful for knowledge seekers. Keep sharing your knowledge through this kind of article.
ReplyDeleteMean Stack Training in Chennai
Go Lang Training in Chennai
Mean Stack Course in Chennai
Go Lang Course in Chennai
Mean stack Classes in Chennai
Go Lang Training Course in Chennai