Chain of Responsibility – an elegant way to handle Complex Validation

Introduction

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 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.
chains_of_responsibility_bedroom types

Piotr Luksza - Chains of Responsibility design patter form

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. Mirror version of the article was published at BlogerSii

Używane laptopy białystok

You may also like...

1 Response

  1. kaviyaa says:

    Everything is fine, am happy about your blog. Thanks admin for sharing the unique content, you have done a great job I appreciate your effort and I hope you will get more positive comments from the web users.

Leave a Reply

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