Again, there is nothing wrong in data annotations as similar results can be accomplished by following that approach. But, too many annotations can make your model look quite ugly.
Here is the example of our current Product Model where we have all the properties defined:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using FluentValidation;
using FluentValidation.Attributes;
using FluentValidation.Results;
using System.ComponentModel.DataAnnotations;
using EFCodeFirstMVCApplication.Models;
namespace EFCodeFirstMVCApplication.Models
{
public class Product
{
public int ID { get; set; }
public string Prod_SKU { get; set; }
public string Prod_Name { get; set; }
public DateTime CreateDate { get; set; }
}
}
In order to implement FluentValidation, we need to add a line to the Global.asax file (in Application _Start method). This will hook up FluentValidation class library to the ASP.NET MVC 5 validation framework. We will call FluentValidationModelValidatorProvider.Configure() method inside the global.asax file like this:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;
using FluentValidation.Mvc;
namespace EFCodeFirstMVCApplication
{
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
// Adding FluentValidation
FluentValidationModelValidatorProvider.Configure();
}
}
}
Now, we can start setting up the actual data validation. There are two ways of creating custom validators. The first is to create a custom property validator, the second is to make use of the Custom method on AbstractValidator. Let’s create a new class called “FluentProductValidator” like this:
public class FluentProductValidator : AbstractValidator<
Product
>
{
public FluentProductValidator()
{
}
}
public class FluentProductValidator : AbstractValidator<
Product
>
{
public FluentProductValidator()
{
RuleFor(x => x.Prod_SKU).NotEmpty().WithMessage("SKU is required");
RuleFor(x => x.Prod_Name).NotEmpty().WithMessage("Name is required");
}
}
Our complete Product Model files looks like this:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using FluentValidation;
using FluentValidation.Attributes;
using FluentValidation.Results;
using System.ComponentModel.DataAnnotations;
using EFCodeFirstMVCApplication.Models;
namespace EFCodeFirstMVCApplication.Models
{
[Validator(typeof(FluentProductValidator))]
public class Product
{
public int ID { get; set; }
public string Prod_SKU { get; set; }
public string Prod_Name { get; set; }
public DateTime CreateDate { get; set; }
public virtual ICollection<
ProductManufacturerMapping
> ProductCategoryMappings { get; set; }
}
public class FluentProductValidator : AbstractValidator<
Product
>
{
public FluentProductValidator()
{
RuleFor(x => x.Prod_SKU).NotEmpty().WithMessage("SKU is required");
RuleFor(x => x.Prod_Name).NotEmpty().WithMessage("Name is required");
}
}
}
Now, if we run the application and go to Create OR Edit, we should see the validations like this:
We have successfully implemented the FluentValidation in our ASP.NET MVC web application.
Open-source .NET projects like nopCommerce are using FluentValidation for data validation
FluentValidation is a very powerful validation framework that allows many features that regular ComponentModel framework does not offer which is why project like nopCommerce is using it in the official software. Now the question is, Fluent Validation benefits projects like nopCommerce?
- Fluent Validation gives nopCommerce far more control over the validation rules
- nopCommerce project can make use of conditional validation which is quite easier as compared to data annotations
- It allows nopCommerce an option to separate all the validations from the view models
- Unit testing is far easier as compared to data annotations
- Fluent Validation offers a better client-side validation support
Let’s take a look at how nopCommerce is making use of FluentValidation in the project
One of the most important thing in an e-Commerce business is customer data. In order to maintain quality data (i.e. customer information) in the database, it is critical to have strong data validation rules on the public store. nopCommerce makes the best use of Fluent Validation in this area so let’s look into “AddressValidator.cs” that can be found in this location:
Nop.Web\Validators\Common\AddressValidator.cs
Here is the source code:using FluentValidation;
using FluentValidation.Results;
using Nop.Core.Domain.Common;
using Nop.Services.Directory;
using Nop.Services.Localization;
using Nop.Web.Framework.Validators;
using Nop.Web.Models.Common;
namespace Nop.Web.Validators.Common
{
public class AddressValidator : BaseNopValidator<
AddressModel
>
{
public AddressValidator(ILocalizationService localizationService,
IStateProvinceService stateProvinceService,
AddressSettings addressSettings)
{
RuleFor(x => x.FirstName)
.NotEmpty()
.WithMessage(localizationService.GetResource("Address.Fields.FirstName.Required"));
RuleFor(x => x.LastName)
.NotEmpty()
.WithMessage(localizationService.GetResource("Address.Fields.LastName.Required"));
RuleFor(x => x.Email)
.NotEmpty()
.WithMessage(localizationService.GetResource("Address.Fields.Email.Required"));
RuleFor(x => x.Email)
.EmailAddress()
.WithMessage(localizationService.GetResource("Common.WrongEmail"));
if (addressSettings.CountryEnabled)
{
RuleFor(x => x.CountryId)
.NotNull()
.WithMessage(localizationService.GetResource("Address.Fields.Country.Required"));
RuleFor(x => x.CountryId)
.NotEqual(0)
.WithMessage(localizationService.GetResource("Address.Fields.Country.Required"));
}
if (addressSettings.CountryEnabled && addressSettings.StateProvinceEnabled)
{
Custom(x =>
{
//does selected country has states?
var countryId = x.CountryId.HasValue ? x.CountryId.Value : 0;
var hasStates = stateProvinceService.GetStateProvincesByCountryId(countryId).Count > 0;
if (hasStates)
{
//if yes, then ensure that state is selected
if (!x.StateProvinceId.HasValue || x.StateProvinceId.Value == 0)
{
return new ValidationFailure("StateProvinceId", localizationService.GetResource("Address.Fields.StateProvince.Required"));
}
}
return null;
});
}
if (addressSettings.CompanyRequired && addressSettings.CompanyEnabled)
{
RuleFor(x => x.Company).NotEmpty().WithMessage(localizationService.GetResource("Account.Fields.Company.Required"));
}
if (addressSettings.StreetAddressRequired && addressSettings.StreetAddressEnabled)
{
RuleFor(x => x.Address1).NotEmpty().WithMessage(localizationService.GetResource("Account.Fields.StreetAddress.Required"));
}
if (addressSettings.StreetAddress2Required && addressSettings.StreetAddress2Enabled)
{
RuleFor(x => x.Address2).NotEmpty().WithMessage(localizationService.GetResource("Account.Fields.StreetAddress2.Required"));
}
if (addressSettings.ZipPostalCodeRequired && addressSettings.ZipPostalCodeEnabled)
{
RuleFor(x => x.ZipPostalCode).NotEmpty().WithMessage(localizationService.GetResource("Account.Fields.ZipPostalCode.Required"));
}
if (addressSettings.CityRequired && addressSettings.CityEnabled)
{
RuleFor(x => x.City).NotEmpty().WithMessage(localizationService.GetResource("Account.Fields.City.Required"));
}
if (addressSettings.PhoneRequired && addressSettings.PhoneEnabled)
{
RuleFor(x => x.PhoneNumber).NotEmpty().WithMessage(localizationService.GetResource("Account.Fields.Phone.Required"));
}
if (addressSettings.FaxRequired && addressSettings.FaxEnabled)
{
RuleFor(x => x.FaxNumber).NotEmpty().WithMessage(localizationService.GetResource("Account.Fields.Fax.Required"));
}
}
}
}
All the validation rules in nopCommerce are making use of localization resource values so that depending on the selected language for public store, the validation messages can be shown. If we look closely to the source code (above), we will find that nopCommerce is using “NotEmpty” validator (in this case) instead of NotNull validator. Whereas, in QueuedEmailValidator.cs, nopCommerce project is making use of “NotNull() validator”:
using FluentValidation;
using Nop.Admin.Models.Messages;
using Nop.Services.Localization;
using Nop.Web.Framework.Validators;
namespace Nop.Admin.Validators.Messages
{
public class QueuedEmailValidator : BaseNopValidator<
QueuedEmailModel
>
{
public QueuedEmailValidator(ILocalizationService localizationService)
{
RuleFor(x => x.From).NotEmpty().WithMessage(localizationService.GetResource("Admin.System.QueuedEmails.Fields.From.Required"));
RuleFor(x => x.To).NotEmpty().WithMessage(localizationService.GetResource("Admin.System.QueuedEmails.Fields.To.Required"));
RuleFor(x => x.SentTries).NotNull().WithMessage(localizationService.GetResource("Admin.System.QueuedEmails.Fields.SentTries.Required"))
.InclusiveBetween(0, 99999).WithMessage(localizationService.GetResource("Admin.System.QueuedEmails.Fields.SentTries.Range"));
}
}
}
Example:
RuleFor(x => x.FirstName)
.NotEmpty()
.WithMessage(localizationService.GetResource("Address.Fields.FirstName.Required"));
We should change/update it in the administration section (for each language): Configuration > Languages
Now, if we go to the nopCommerce public store (in account) and try to add the address without first name, we should see the updated message like this:
RuleFor(x => x.FirstName)
.NotEmpty()
.WithMessage(localizationService.GetResource("Address.Fields.FirstName.Required"));
New updated validation message in the code:
RuleFor(x => x.FirstName)
.NotEmpty()
.WithMessage("New Hardcoded Validation Message Here");
Now, if we go to the nopCommerce public store (in account) and try to add the address without first name, we should see the updated message like this:
AddressValidator
public AddressValidator(ILocalizationService localizationService,
IStateProvinceService stateProvinceService,
AddressSettings addressSettings)
?
Are you facing any issues?
The other user was eluding to an error that is generated
when you don't have a parameter-less constructor. How
did you get around that issue in your AddressValidator?
I would just like to clarify one thing - Are you talking about "Option 2" above in which I am updating the validation message in the source code?
Please clarify what you are referring to?
AddressValidator using Dependency Injection.
public AddressValidator(ILocalizationService localizationService,
IStateProvinceService stateProvinceService,
AddressSettings addressSettings)
When doing so with FluentValidation, this throws an error because
FluentValidation is looking for a ctor w/o parameters.
Thanks
I am not implementing AddressValidator in the above article. I am basically giving an overview of how FluentValidation is being used in nopCommerce. What you are referring to is not complete code. You can simply refer to "Nop.Web\Validators\Common\AddressValidator.cs" in your nopCommerce project and see the full source code.
You have to complete two steps in order to add a validation to some models in nopCommerce:
1. Create a class derived from AbstractValidator class and put all required logic there. See the image below to get an idea:
public class AddressValidator : AbstractValidator<AddressModel>
{
public AddressValidator(ILocalizationService localizationService)
{
RuleFor(x => x.FirstName)
.NotEmpty()
.WithMessage(localizationService.GetResource("Address.Fields.FirstName.Required"));
}
}
2. Annotate your model class with the ValidatorAttribute. Refer to the example below for guidance.
[Validator(typeof(AddressValidator))]
public class AddressModel : BaseNopEntityModel
{
ASP.NET will execute the appropriate validator when a view model is posted to a controller.
- Nop.Web\Validators\Common\AddressValidator.cs
- Nop.Web\Models\Common\AddressModel.cs
As you can see in the above code, GetResource method of Localization Service is used that retrieves a string resource which is then passed into the .WithMessage method.
Hope it helps!