Sunday, 9 August 2009

Validation Framework with MVC support

The most recent project I've started to work on is going to require a number of web front-ends for a business. There will be a single database, from which information will be drawn and written to in various ways on the different websites. This situation suggests a robust means of applying and enforcing business rules and validation within a common business logic layer; promoting code re-use across the sites and avoiding the maintenance headache of having rules in the UI layer.

Due to a delayed start of the project, I've had a bit of time to investigate some different options to solve this - and to have a hard look at some of the techniques that have been used in the past where, if I'm honest, this wasn't always best achieved.

CSLA.Net

Firstly, I spent a bit of time with CSLA.Net. This is a robust, tried-and-tested framework for implementing your business objects. It's very powerful and I'm sure would prove a good solution. However in the end I decided against it - for two reasons.

The first was that it was proving tricky to use in a testable manner. It could well be that with perseverance I could have got around this, but it seems that the architecture employed (static methods, encapsulated business and data access) made separation of concerns and mocking/unit testing difficult.

Second was really just a nagging feeling that a lot of the features I would never be likely to use - particular those geared to Windows Forms. The mechanism supports n-level undo, remoting objects and the retrieval and saving of large object graphs - none of which I could see a likely scenario that I would require in the medium term.

Validation Framework

One great thing CSLA.Net would have provided though was a consistent means of handling business object validation - and so this is what I looked to replicate in the solution.

Much of the credit here really needs to go to the author of the recently released ASP.Net MVC Framework book from APress. I can certainly recommend this book highly - it does a great job of explaining not only the MVC framework, but also the patterns techniques deployed in the domain model layer, namely repositories and inversion of control/dependency injection.

The crux of the validation framework I've employed on this project, and no doubt on an ongoing basis, is that a particular business object or entity (e.g. a Customer object) may be validated agaist three groups of criteria:
  1. Self-validation. This would be anything that the simply entity class can validate about itself (e.g. required fields, ranges of valid values, internal field comparisons)
  2. External validation. This would be business rules enforced based on factors outside of the immediate properties and methods of the class (e.g. user authorisations)
  3. Database validation. These are really a special class of external validation that is enforced in the database (e.g. dependent records for deletions, unique fields).
Code wise my domain model layer is implemented as a single C# class library (though some might choose to split into three projects, one for each component).

The business objects or entities are deliberately implemented as simply as possible. They only contain properties, methods that can act on internal fields only (e.g. an Order class that calculated it's own total would be a good candidate), and methods to implement an interface I've set up to enforce consistent self-validation:


    1 using System;


    2 using System.Collections.Specialized;


    3 


    4 namespace DomainModel.Interfaces


    5 {


    6     /// <summary>


    7     /// Interface for all entity classes that perform self-validation


    8     /// </summary>


    9     interface IValidatingEntity


   10     {


   11         NameValueCollection GetRuleViolations();


   12     }


   13 }




    1 using System;


    2 using System.Collections.Specialized;


    3 using DomainModel.Interfaces;


    4 


    5 namespace DomainModel.Entities


    6 {


    7     /// <summary>


    8     /// Class representiting a Title entity and it's self-validation rules


    9     /// </summary>


   10     public class Customer : IValidatingEntity


   11     {


   12         public int Id { get; set; }


   13         public string Name { get; set; }


   14 


   15         /// <summary>


   16         /// Checks class internal members are valid


   17         /// </summary>


   18         /// <returns>Dictionary of any valuations (property/message)</returns>


   19         public NameValueCollection GetRuleViolations()


   20         {


   21             NameValueCollection errors = new NameValueCollection();


   22 


   23             if (string.IsNullOrEmpty(Name))


   24                 errors.Add("Name", "Name is a required field.");


   25             else


   26                 if (Name.Length > 100)


   27                     errors.Add("Name", "The Name field must be 100 characters or less.");


   28 


   29             return errors;


   30         }


   31     }


   32 }




My data access layer is implemented using repositories for each area of the application - in this case a Customer repository that implements an interface:


    1 using System;


    2 using DomainModel.Entities;


    3 


    4 namespace DomainModel.Repository.Abstract


    5 {


    6     /// <summary>


    7     /// Interface for Customers repository


    8     /// </summary>


    9     public interface ICustomersRepository


   10     {


   11         IList<Customer> GetCustomerList();


   12         Customer GetCustomer(int id);


   13         void UpdateCustomer(Customer title);


   14     }


   15 }




    1 using System;


    2 using System.Data;


    3 using System.Data.SqlClient;


    4 using DomainModel.Entities;


    5 using DomainModel.Repository.Abstract;


    6 


    7 namespace DomainModel.Repository.Concrete


    8 {


    9     /// <summary>


   10     /// Customers repository implemented using SQL ADO.Net data access methods


   11     /// </summary>


   12     public class SqlCustomersRepository : ICustomersRepository


   13     {


   14         private string connectionString;


   15 


   16         public SqlCustomersRepository(string connectionString)


   17         {


   18             this.connectionString = connectionString;


   19         }       


   20 


   21         /// <summary>


   22         /// Persists a Customer object to the database


   23         /// </summary>


   24         /// <param name="customer">Customer object</param>


   25         public void UpdateCustomer(Customer customer)


   26         {


   27             using (SqlConnection cn = new SqlConnection(connectionString))


   28             {


   29                 SqlCommand cm = cn.CreateCommand();


   30                 cm.CommandType = CommandType.StoredProcedure;


   31                 cm.CommandText = "updateCusomter";


   32                 cm.Parameters.AddWithValue("@id", customer.Id);


   33                 cm.Parameters.AddWithValue("@name", customer.Name);


   34                 cn.Open();


   35                 cm.ExecuteNonQuery();


   36                 cn.Close();


   37             }


   38         }


   39     }


   40 }




Finally, acting as the bridge between the repository and the UI layer sits a service layer:


    1 using System;


    2 using DomainModel.Entities;


    3 


    4 namespace DomainModel.Service.Abstract


    5 {


    6     /// <summary>


    7     /// Interface for Customers service


    8     /// </summary>


    9     public interface ITitlesService


   10     {


   11         IList<Customer> GetCustomerList();


   12         Customer GetCustomer(int id);


   13         void UpdateCustomer(Customer title);


   14     }


   15 }




    1 using System;


    2 using System.Collections.Generic;


    3 using System.Linq;


    4 using System.Text;


    5 using System.Collections.Specialized;


    6 using DomainModel.Entities;


    7 using DomainModel.Service.Abstract;


    8 using DomainModel.Repository.Abstract;


    9 using DomainModel.Exceptions;


   10 using DomainModel.Interfaces;


   11 


   12 namespace DomainModel.Service.Concrete


   13 {


   14     /// <summary>


   15     /// Customers service layer


   16     /// </summary>


   17     public class CustomersService : ICustomersService


   18     {


   19         private ICustomersRepository repository;


   20 


   21         public CustomersService(ICustomersRepository repository)


   22         {


   23             this.repository = repository;


   24         }


   25 


   47         /// <summary>


   48         /// Validates (at entity, service and database level) and saves a Customer object


   49         /// </summary>


   50         /// <param name="customer">Customer object</param>


   51         /// <param name="user">User making the update</param>


   52         public void UpdateCustomer(Customer customer, User user)


   53         {


   54             NameValueCollection errors = customer.GetRuleViolations();


   55             GetServiceViolations(ref errors, user);


   56             if (errors.Count > 0)


   57                 throw new RuleException(errors);


   58             try


   59             {


   60                 repository.UpdateCustomer(customer);


   61             }


   62             catch (Exception ex)


   63             {


   64                 if (GetDatabaseViolations(ex, ref errors))


   65                     throw new RuleException(errors);


   66                 else


   67                     throw (ex);     // Throw original error if not handled


   68             }           


   69         }


   70 


   71         /// <summary>


   72         /// Checks any service level business rules


   73         /// </summary>


   74         /// <param name="errors">Existing collection of errors</param>


   75         private void GetServiceViolations(ref NameValueCollection errors, User user)


   76         {


   77             if(!user.IsInRole("Edit"))


   78                 errors.Add("Authorisation", "Your account is not authorised to perform this action.");


   79         }


   80 


   81         /// <summary>


   82         /// Handles a database exception triggered after a save.


   83         /// </summary>


   84         /// <param name="ex">Exception triggered by database save</param>


   85         /// <param name="errors">Existing collection of errors</param>


   86         /// <returns>Flag for if any handled database exceptions found</returns>


   87         private bool GetDatabaseViolations(Exception ex, ref NameValueCollection errors)


   88         {


   89             bool handledError = false;


   90             if (ex.Message.IndexOf("UIX_Customers_Name") >= 0)


   91             {


   92                 errors.Add("Database Constraint", "Names of Customers must be unique.");


   93                 handledError = true;


   94             }


   95             return handledError;


   96         }


   97     }




Which can be then brought into the UI layer and represented in the by copying them to the ModelStateDictionary, and hence hooking up with the MVC Framework's standard means of displaying validation errors in views. This last step is best implemented as an extension method in the UI layer, to avoid fixing a dependency on MVC in the domain model.


    1 using System;


    2 using System.Collections.Generic;


    3 using System.Web;


    4 using System.Web.Mvc;


    5 using DomainModel.Exceptions;


    6 


    7 namespace WebUI.ExtensionMethods


    8 {


    9     /// <summary>


   10     /// MVC project extension methods


   11     /// </summary>


   12     public static class ExtensionMethods


   13     {


   14         /// <summary>


   15         /// Extends RuleException (defined in domain model) with a method to copy it's contents to the MVC


   16         /// ModelStateDictionary


   17         /// </summary>


   18         /// <param name="ex">Rule exception object to apply extension to</param>


   19         /// <param name="modelState">MVC model state dictionary</param>


   20         /// <remarks>


   21         /// Implemented in web project to avoid MVC dependency in domain model layer.


   22         /// </remarks>


   23         public static void CopyToModelState(this RuleException ex, ModelStateDictionary modelState)


   24         {


   25             foreach (string key in ex.Errors)


   26                 foreach (string value in ex.Errors.GetValues(key))


   27                     modelState.AddModelError(key, value);


   28         }


   29     }


   30 }




The resulting controller action, that calls the validation and save of the Customer object looks like this:


   42         [AcceptVerbs(HttpVerbs.Post)]


   43         public ActionResult Edit(Customer title)


   44         {


   45             if (ModelState.IsValid)


   46             {


   47                 try


   48                 {


   49                     titlesService.UpdateCustomer(title);


   50                 }


   51                 catch (RuleException ex)


   52                 {


   53                     ex.CopyToModelState(ModelState);


   54                 }


   55             }


   56             if (ModelState.IsValid)


   57                 return RedirectToAction("List");


   58             else


   59                 return View("Edit", title);


   60         }




In summary, I think this going to prove a simple but easily adhered to and and clear pattern for centralised business rules. Will post further once it has been put through its paces in a real application.

No comments:

Post a Comment