ASP.NET Conditional Validation

Posted by Vinicius de Melo Rocha on September 20th, 2017


Conditional validation can be very useful, and I’m glad that I found an elegant way of doing it. You can use the same View-Model on two different actions of your controller and have different validations for each of them.

Sample Scenario

To help illustrate the issue and the solution, let’s describe a simple scenario for user management. We will have a controller that adds and edits users. I will abstract the business layer so we can focus only on the user validation.

namespace Groups.Web.Admin.Controllers
{
    [RoutePrefix("users")]
    public class UsersController : BaseController
    {
        [HttpGet, Route("add")]
        public virtual ActionResult Add()
        {
            return View(new UserViewModel());
        }

        [HttpPost, Route("add")]
        [ValidateAntiForgeryToken]
        public virtual ActionResult Add(UserViewModel model)
        {
            if (!ModelState.IsValid)
            {
                return View(model);
            }

            // Business logic to add a new user
        }


        [HttpGet, Route("edit/{id}")]
        public ActionResult Edit(Guid id)
        {
            // Find and return User for editing
        }

        [HttpPost, Route("edit/{id}")]
        [ValidateAntiForgeryToken]
        public ActionResult Edit(UserViewModel model)
        {
            if (!ModelState.IsValid)
            {
                return View(model);
            }

            // Business logic to edit a user
        }
    }
}
using System.ComponentModel.DataAnnotations;
using Groups.Domain.Entities;

namespace Groups.Web.Admin.ViewModels
{
    public class UserViewModel : BaseViewModel
    {
        public UserViewModel() { }

        public UserViewModel(User user)
        {
            Id = user.Id;
            Name = user.Name;
            Email = user.Email;
        }

        public string Name { get; set; }

        public string Email { get; set; }

        [DataType(DataType.Password)]
        public string Password { get; set; }

        public override string ToString() => Name;
    }
}

Note that we don’t have any validation in our View-Model, we will add them in a moment.

Installing Fluent Validation

What we will be using for validation is a library called Fluent Validation. It is an attractive solution for validation in the .NET environment. To use it, the first thing that we need to do is install it via NuGet.

Install-Package FluentValidation.MVC5

The next step is to initialize the Fluent Validation library during the application start using the Configure method. To do that, just add the following line to your Application_Start method:

FluentValidationModelValidatorProvider.Configure();

If you are using Unity for Dependency Injection, also consider adding the following line in your registration method.

container
  .RegisterType<
        RuleSetForClientSideMessagesAttribute,
        RuleSetForClientSideMessagesAttribute>(
      new InjectionConstructor(typeof(string)));

Adding Validation

Now that we have the Fluent Validation library up and running in our ASP.NET MVC 5 project, let’s add validation to our View-Model classes. The way we will be doing this is by creating a validation class that we will be referencing from the View-Model.

using System.Web;
using FluentValidation;

namespace Groups.Web.Admin.ViewModels
{
    public class UserViewModelValidator : AbstractValidator<UserViewModel>
    {
        public UserViewModelValidator()
        {
            RuleFor(x => x.Name).NotEmpty();
            RuleFor(x => x.Email).NotEmpty().EmailAddress();

            RuleSet("AddOnly", () =>
            {
                RuleFor(x => x.Password).NotEmpty();
            });

            RuleSet("EditOnly", () =>
            {
                RuleFor(x => x.Id).NotEmpty();
            });
        }
    }
}

The RuleSet is one of the coolest things that Fluent Validation has to offer, and what makes it so flexible. We will be using those rules with the controller in the next section. Now, we can decorate our model class with the Validator attribute.

using System.ComponentModel.DataAnnotations;
using FluentValidation.Attributes;
using Groups.Domain.Entities;

namespace Groups.Web.Admin.ViewModels
{
    [Validator(typeof(UserViewModelValidator))]
    public class UserViewModel : BaseViewModel
    {
        public UserViewModel() { }

        public UserViewModel(User user)
        {
            Id = user.Id;
            Name = user.Name;
            Email = user.Email;
        }

        public string Name { get; set; }

        public string Email { get; set; }

        [DataType(DataType.Password)]
        public string Password { get; set; }

        public override string ToString() => Name;
    }
}

The only thing new here is line 7, where we define which validator we will be using for this View-Model class.

Setup the Conditional Validation

Now is when the magic happens. We can specify in our actions, which RuleSets we want to use for each one. We can use the RuleSetForClientSideMessages attribute for the HttpGet methods and CustomizeValidator for our model binding during Post events. And that is all we need to do.

namespace Groups.Web.Admin.Controllers
{
    [RoutePrefix("users")]
    public class UsersController : BaseController
    {
        [HttpGet, Route("add")]
        [RuleSetForClientSideMessages("default", "AddOnly")]
        public virtual ActionResult Add()
        {
            (...)
        }

        [HttpPost, Route("add")]
        [ValidateAntiForgeryToken]
        public virtual ActionResult Add(
          [CustomizeValidator(RuleSet = "default,AddOnly")] UserViewModel model)
        {
            if (!ModelState.IsValid)
            {
                return View(model);
            }

            (...)
        }


        [HttpGet, Route("edit/{id}")]
        [RuleSetForClientSideMessages("default", "EditOnly")]
        public ActionResult Edit(Guid id)
        {
            (...)
        }

        [HttpPost, Route("edit/{id}")]
        [ValidateAntiForgeryToken]
        public ActionResult Edit(
          [CustomizeValidator(RuleSet = "default,EditOnly")] UserViewModel model)
        {
            if (!ModelState.IsValid)
            {
                return View(model);
            }

            (...)
        }
    }
}

Conclusion

And really, that is all you need to do. No doubts about why this library is so popular. This approach is also good if you want to do validation that requires business logic, for example: check the database to see if the email already exists:

using System.Web;
using FluentValidation;
using Groups.Business.Services;
using Groups.Web.Admin.Configuration;
using Groups.Web.Admin.Extensions;
using Groups.Web.Admin.Resources;
using Microsoft.Practices.Unity;

namespace Groups.Web.Admin.ViewModels
{
    public class UserViewModelValidator : AbstractValidator<UserViewModel>
    {
        public UserViewModelValidator()
        {
            var userService = UnityConfig.GetConfiguredContainer().Resolve<IUserService>();
            var loggedUser = HttpContext.Current.Session.GetLoggedUser();

            RuleFor(x => x.Name).NotEmpty();
            RuleFor(x => x.Email).NotEmpty().EmailAddress();
            
            RuleFor(x => x.Email).Must((root, email) =>
                    !userService.AnyWithEmail(email, root.Id, loggedUser.Group.Id))
                .WithMessage(MainResource.ErrorEmailAlreadyRegistered);

            RuleSet("AddOnly", () =>
            {
                RuleFor(x => x.Password).NotEmpty();
            });

            RuleSet("EditOnly", () =>
            {
                RuleFor(x => x.Id).NotEmpty();
            });
        }
    }
}