Send your request Join Sii

Data validation is a key aspect of almost any system, incorrect or hacked data can cause a lot of damage, restoring can cost a lot of time, money and stress. Properly designed system operates on high-quality, correct, clean and checked data. In this post I will try to introduce the concept of validation using the Chain of Responsibility pattern (CoR), which I use successfull for years. More theory with examples of this pattern can be found here. Checking the input data may include simple user form or complex data received from an external system. Using this approach we are not afraid of any validation, so we can handle quite complex issues.

In this article, we’ll build a demo using MVC .NET web application with simple form (most of the times corrupted data comes from user input) and the purpose is to ensure that submitted data won’t kill the program. Presented approach can be used in any type of application. For the purposes of training, client side validation (based on data annotations) will be disabled and will focus only on the server-side validation. Anyway, it is worth being wary and always apply double check on submitted data to avoid hacking.

The following example illustrates how a chain of responsibility can handle validation issue elegant way.
Consider simple hotel system where we will be able to order one of the three apartments: one bedroom, one bedroom executive or two bedroom. Of course, date of reservation should be appropriate and chosen room should be avaliable. When the validation conditions are not met, the system should display an appropriate error message on the screen.

Error message OneBedroom Executive is currently not available

Business validation rules

  • DateFrom is not null
  • DateFrom is greater than now
  • DateTo is not null
  • DateTo is greater than DateFrom
  • One bedroom Exclusive is not avaliable

Demo – model

public class ApartmentBooking
{
public int Id { get; set; }
public ApartmentType ApartmentType { get; set; }
public DateTime DateFrom { get; set; }
public DateTime DateTo { get; set; }
}
public enum ApartmentType : byte
{
[Display(Name = "One Bedroom")]
OneBedroom,
[Display(Name = "One Bedroom Executive")]
OneBedroomExecutive,
[Display(Name = "Two Bedroom")]
TwoBedroom,
}

Demo – important components

  • ApartmentBookingValidationContext
  • DateFromRequiredValidator
  • DateFromRangeValidator
  • DateToRequiredValidator
  • DateToRangeValidator
  • ApartmentAvaliableValidator

One of the most important classes is ApartmentBookingValidationContext with one static method Validate(). This is the place where the magic starts, all validators instances are created here and hooked up in the right order.

As we see every validator contais important method SetSuccessor, it allows us to set the next item in the chain, so the validation gets passed along a chain of objects until one of them handles it. Notice that method Validate() returns collection of messages (errors), so we can use them in other layers of the application, for example pushing the UI notification.

public static class ApartmentBookingValidationContext
{
public static Dictionary<string, string> Validate(Models.ApartmentBooking model)
{
/* Hook up validation chain
DateFrom not null
DateTo not null
DateFrom greater than now
DateTo greater than DateFrom
Dummy apartment validation base on type
*/
 
DateFromRequiredValidator dateFromRequiredValidator = new DateFromRequiredValidator();
DateFromRangeValidator dateFromRangeValidator = new DateFromRangeValidator();
dateFromRequiredValidator.SetSuccessor( dateFromRangeValidator);
DateToRequiredValidator dateTimeToRequiredValidator = new DateToRequiredValidator();
dateFromRangeValidator.SetSuccessor( dateTimeToRequiredValidator);
DateToRangeValidator dateToRangeValidator = new DateToRangeValidator();
dateTimeToRequiredValidator.SetSuccessor( dateToRangeValidator);
ApartmentAvaliableValidator apartmentAvaliableValidator = new ApartmentAvaliableValidator();
dateToRangeValidator.SetSuccessor( apartmentAvaliableValidator);
 
return dateFromRequiredValidator.HandleValidation( model);
}
}
public abstract class ValidatorBase
{
protected ValidatorBase Successor { get; private set; }
protected Dictionary<string, string> ErrorsResult { get; set; }
 
protected ValidatorBase()
{
ErrorsResult = new Dictionary<string, string>();
}
 
public abstract Dictionary<string, string> HandleValidation( Models.ApartmentBooking model);
 
/// <summary>
/// Set next validation
/// </summary>
/// <param name="successor"></param>
public void SetSuccessor( ValidatorBase successor)
{
this.Successor = successor;
}
}
public class DateFromRequiredValidator : ValidatorBase
{
public override Dictionary<string, string> HandleValidation( Models.ApartmentBooking model)
{
if (model.DateFrom == DateTime.MinValue)
{
ErrorsResult.Add("DateFrom", "Date from field is required");
return ErrorsResult;
}
 
if (Successor != null)
return Successor.HandleValidation(model);
 
return ErrorsResult;
}
}
public class DateFromRangeValidator : ValidatorBase
{
public override Dictionary<string, string> HandleValidation( Models.ApartmentBooking model)
{
if (model.DateFrom < new DateTime(DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day))
{
ErrorsResult.Add("DateFrom", "Date from should be grater than now");
return ErrorsResult;
}
 
if (Successor != null)
return Successor.HandleValidation(model);
 
return ErrorsResult;
}
}
public class DateToRequiredValidator : ValidatorBase
{
public override Dictionary<string, string> HandleValidation(Models.ApartmentBooking model)
{
if (model.DateTo == DateTime.MinValue)
{
ErrorsResult.Add("DateTo", "Date to 2 field is required");
return ErrorsResult;
}
 
if (Successor != null)
return Successor.HandleValidation(model);
 
return ErrorsResult;
}
}
public class DateToRangeValidator : ValidatorBase
{
public override Dictionary<string, string> HandleValidation( Models.ApartmentBooking model)
{
if (model.DateTo <= model.DateFrom)
{
ErrorsResult.Add("DateTo", "Date to should be grater than Date from");
return ErrorsResult;
}
 
if (Successor != null)
return Successor.HandleValidation(model);
 
return ErrorsResult;
}
}
public class ApartmentAvaliableValidator : ValidatorBase
{
public override Dictionary<string, string> HandleValidation(Models.ApartmentBooking model)
{
/* Dummy validation */
if (model.ApartmentType == ApartmentType.OneBedroomExecutive)
{
ErrorsResult.Add("ApartmentType",
string.Format("{0} is currently not avaliable", model.ApartmentType));
return ErrorsResult;
}
 
if (Successor != null)
return Successor.HandleValidation(model);
 
return ErrorsResult;
}
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Create(ApartmentBooking model)
{
//if (ModelState.IsValid)
{
var errorsResult = ApartmentBookingValidationContext.Validate(model);
if (errorsResult.Any())
{
ModelState.Clear();
 
foreach (var error in errorsResult)
{
ModelState.AddModelError(error.Key, error.Value);
}
TempData["ValidationErrors"] = true;
 
return View(model);
}
else
{
db.ApartmentBookings.Add(model);
await db.SaveChangesAsync();
}
 
return RedirectToAction("Index");
}
 
//return View(model);
}

Conclusion

Validation based on Chain of the Responsibility pattern is very handy and gives a lot of benefits:

  • Improves readability (as you see controller is flat – no spaghetti code)
  • Handling complex validation by several objects in chain structure, which encapsulates the logic
  • Flexibility in assigning responsibilities to objects
  • Easy development and maintenance. So when business requirements change, just modify concrete validator or create a new one and hook it to the chain
  • Objects are independent, they dont’t know nothing about the chain structure

I encourage you to use this pattern in daily work, the source code can be found here.

4.5/5 ( votes: 2)
Rating:
4.5/5 ( votes: 2)
Author
Avatar
Piotr Bach

Senior .NET Developer and Umbraco Master.

Leave a comment

Cancel reply

Your email address will not be published. Required fields are marked *

You might also like

More articles

Don't miss out

Subscribe to our blog and receive information about the latest posts.

Get an offer

If you have any questions or would like to learn more about our offer, feel free to contact us.

Send your request Send your request

Natalia Competency Center Director

Get an offer

Join Sii

Find the job that's right for you. Check out open positions and apply.

Apply Apply

Paweł Process Owner

Join Sii

SUBMIT

Ta treść jest dostępna tylko w jednej wersji językowej.
Nastąpi przekierowanie do strony głównej.

Czy chcesz opuścić tę stronę?