Sunday, January 25, 2009

Custom Validation Step 2: Business Object Validation

The Plumbing

In the first post in this series, Custom Validation Step 1: The ValidationErrorClass, I created some of the plumbing for my custom validation design.  I created a ValidationError class and a BALBase that defined the interface for my entities (business objects) that will use the validation.  The two classes look like this:

ValidationDiagram 

The Person Class Design

Now, I’m going to put that plumbing to use and implement validation in my Person entity class.   Person is an entity in my domain, which is just another way of saying that it’s a business object.   I typically use DTOs (Data Transfer Objects) to move data between different layers in my architecture, so the design of the Person class is pretty simple.  All of the actual person data is stored in a PersonDTO class.  The Person class, my full fledged business object, has a property named “data” that holds a PersonDTO.  So I don’t have individual data members in the body of my Person class, the data is all held in the data property

That takes care of data.  The only other thing the Person class needs to worry about is validation logic.  For that, it implements the Validate() method required by BALBase, and it also implements individual validation methods for each piece of data that requires validation.  So I have a Val_Name() method, a Val_Email() method, etc.  The resulting classes end up looking like this.

PersonEntity

A Sample Val_Email() Method

Now I can finally get to the validation.  I’m going to start by writing a validation method named Val_Email() for the Email field.  This method will cover all validation rules for my email field.  There are three rules that the email data must pass. First, email is required data.  Second, email must be a valid email address.  Third, the email can’t be in use by another person.  If my data fails to pass any of the rules, the Val_Email() method creates a new ValidationError object, adds it to the ValidationErrors member of the Person class, and then returns false to signify that we failed validation.  If the email data passes all three rules, we return true. This method encapsulates all validation rules needed for the email field.  The result looks like this.

//

// Val_Email

//

public bool Val_Email()

{

  // Email required

  if (this.Data.Email == DTOBase.String_NullValue)

  {

    this.ValidationErrors.Add(new ValidationError("Person.Email", "<FieldName> is required"));

    return false;

  }

  // Email is valid email

  else if (!ValidationRules.IsValidEmail(this.Data.Email))

  {

    this.ValidationErrors.Add(new ValidationError("Person.Email", "<FieldName> must be a valid email address"));

    return false;

  }

  // Email is not already in use.

  else

  {

    Person checkPerson = PersonRepository.GetPersonByEmail(this.Data.Email);

    if((checkPerson != null) && (!checkPerson.Data.PersonGuid.Equals(this.Data.PersonGuid)))

    {

      this.ValidationErrors.Add(new ValidationError("Person.Email", "That <FieldName> is already in use"));

      return false;

    }                    

  }

  // If we reached this point then the email passed validation

  return true;

}

There are a couple of things I want to point out about the code above.  First, notice that when we create a ValidationError object, the constructor takes two string parameters, the FieldName and the ErrorMessage.  The FieldName that we pass in is “Person.Email” not just “Email”.  This “fully qualified” FieldName is a convention I’ve picked up to ensure that my FieldNames are unique in the situation where there are multiple entities on a page that may have members with the same name.  This way my page will be able to distinguish between validation errors for the Person.Email field and another entity’s field, perhaps Company.Email.

I also want to point out my use of  a ValidationRules class.  This class (or some equivalent mechanism) should be part of any BAL.  There is no reason to write the same regular expression to validate an email address over and over.  The ValidationRules class is just a static class that contains a bunch of static validation methods that are used throughout my BAL. 

Pulling it All Together: The Validate() Method

So I now have a validation method for the email member that runs all of my email validation checks and then adds a new ValidationError object to the Person.ValidationErrors list for any errors that it finds.  Now I want to complete my BALBase implementation by adding a call to Val_Email() to my Validate() method.  Person.Validate() is really just a collection of calls to all of the individual validation methods for my entity. This may seem a little simplistic and at first I was tempted to implement a more flashy solution using attributes like EntLib, and I came very close to implementing a solution using delegates like Rocky Lhotka.  But at the end of the day the pragmatist in me took over and I realized that this method works, it gives me the flexibility that I’m looking for, and it will be easy to understand and maintain for those who come after me. So, after writing validation methods for the other data fields, here is what the Person.Validate method looks like. 

//

// Validate

// Required for BALBase implementation

//

public override List<ValidationError> Validate()

{

  // Call all validation functions

  Val_Name();

  Val_Email();

  Val_Password();

  Val_TimeZone();

  Val_City();

  Val_State();

  Val_ZipCode();

  Val_ImType();

 

  // Return any errors

  return this.ValidationErrors;

}

One last thing I want to point out is that the Validate() method returns the ValidationErrors list when it is done.  I decided on this return type after initially returning a boolean then repeatedly having to check that boolean flag just to see if I needed to then go and get the ValidationErrors list and append it to my PageErrors list.  Now, by returning the ValidationErrors list in the Validate() method, I can write code in my UI that looks like this. 

this.PageErrors.AddList(person.Validate());

 

Much cleaner.  So that’s it.  I now have a consistent interface used by all of my entity classes to run validation for the object and then store any errors in a ValidationErrors list.  Next time I’ll look at how to implement this in my ASP.Net pages.  Below is the complete Person class for anyone who is interested.

public class Person : BALBase

{

    //

    // Data

    // This property exists for all BAL objects, and it is

    // set to the DTO type for this entity.  This is the

    // mechanism that we use to implement "has a" inheritance

    // instead of "is a" inheritance.

    //

    public PersonDTO Data { get; set; }

 

 

    //

    // Person - default constructor

    //

    public Person() { this.Data = new PersonDTO(); }

    //

    // Person - takes a DTO

    //

    public Person(PersonDTO dto) { this.Data = dto; }

 

 

    #region "Validation"

         //

    // Validate

    // Required for BALBase implementation

    //

    public override List<ValidationError> Validate()

    {

        // Call all validation functions

        Val_Name();

        Val_Email();

        Val_Password();

        Val_TimeZone();

        Val_City();

        Val_State();

        Val_ZipCode();

        Val_ImType();

 

             return this.ValidationErrors;

    }

 

 

    // Validation Methods:

    // There are only 2 requirements on validation methods.

    //  - They must handle adding a Validation Error to the

    //    ValidationErrors list if they find an error.

    //  - You must manually add a call to all validation methods

    //    to the Validate() function.

    //  When creating a new ValidationError object, remember

    //  that the first parameter is the exact name of the field

    //  that has the bad value, and the error message should

    //  not contain the field name, but instead the <FieldName>

    //  tag, which will be replaced by the UI or consuming app.

 

    //

    // Val_Name

    //

    public bool Val_Name()

    {

        // Name required

        if (this.Data.Name == DTOBase.String_NullValue)

        {

            this.ValidationErrors.Add(new ValidationError("Person.Name", "<FieldName> is required"));

            return false;

        }

        else

        {

            return true;

        }

    }

    //

    // Val_Email

    //

    public bool Val_Email()

    {

        // Email required

        if (this.Data.Email == DTOBase.String_NullValue)

        {

            this.ValidationErrors.Add(new ValidationError("Person.Email", "<FieldName> is required"));

            return false;

        }

        // Email is valid email

        else if (!ValidationRules.IsValidEmail(this.Data.Email))

        {

            this.ValidationErrors.Add(new ValidationError("Person.Email", "<FieldName> must be a valid email address"));

            return false;

        }

        // Email is not already in use.

        else

        {

            Person checkPerson = PersonRepository.GetPersonByEmail(this.Data.Email);

            if ((checkPerson != null) && (!checkPerson.Data.PersonGuid.Equals(this.Data.PersonGuid)))

            {

                this.ValidationErrors.Add(new ValidationError("Person.Email", "That <FieldName> is already in use"));

                return false;

            }

        }

        // If we reached this point then the email passed validation

        return true;

    }

    //

    // Val_Password

    //

    public bool Val_Password()

    {

        // Password required

        if (string.IsNullOrEmpty(this.Data.Password))

        {

            this.ValidationErrors.Add(new ValidationError("Person.Password", "<FieldName> is required"));

            return false;

        }

        // Password is valid password

        else if (!ValidationRules.IsValidPassword(this.Data.Password))

        {

            this.ValidationErrors.Add(new ValidationError("Person.Password", "<FieldName> must be at least 6 characters"));

            return false;

        }

        else

        {

            return true;

        }

    }

    // TimeZone required

    public bool Val_TimeZone()

    {

        //  TimeZone required

        if (this.Data.TimeZoneId == CommonBase.Int_NullValue)

        {

            this.ValidationErrors.Add(new ValidationError("Person.TimeZoneId", "<FieldName> is required"));

            return false;

        }

        else

        {

            return true;

        }

    }

    // Val_City

    public bool Val_City()

    {

        // City required

        if (string.IsNullOrEmpty(this.Data.City))

        {

            this.ValidationErrors.Add(new ValidationError("Person.City", "<FieldName> is required"));

            return false;

        }

        // Valid City business rule

        else if (!ValidationRules.IsValidCity(this.Data.City))

        {

            this.ValidationErrors.Add(new ValidationError("Person.City", "<FieldName> must be a valid city"));

            return false;

        }

        else

        {

            return true;

        }

    }

 

    // Val_State

    public bool Val_State()

    {

        //  State required

        if (string.IsNullOrEmpty(this.Data.State))

        {

            this.ValidationErrors.Add(new ValidationError("Person.State", "<FieldName> is required"));

            return false;

        }

        // Valid StateCode business rule

        else if (!ValidationRules.IsValidStateCode(this.Data.State))

        {

            this.ValidationErrors.Add(new ValidationError("Person.State", "<FieldName> must be a valid state"));

            return false;

        }

        else

        {

            return true;

        }

    }

 

    // Val_ZipCode

    public bool Val_ZipCode()

    {

        // ZipCode Required

        if (this.Data.ZipCode == CommonBase.Int_NullValue)

        {

            this.ValidationErrors.Add(new ValidationError("Person.ZipCode", "<FieldName> is required"));

            return false;

        }

        // Valid ZipCode business rule

        else if (!ValidationRules.IsValidZipCode(this.Data.ZipCode))

        {

            this.ValidationErrors.Add(new ValidationError("Person.ZipCode", "A valid <FieldName> is required."));

            return false;

        }

        else

        {

            return true;

        }

    }

 

    // ValImType

    public bool Val_ImType()

    {

        //  If ImAddress exists, ImType is required

        if (!string.IsNullOrEmpty(this.Data.ImAddress) && (!ValidationRules.IsValidImTypeId(this.Data.ImType)))

        {

            this.ValidationErrors.Add(new ValidationError("Person.ImType", "<FieldName> is required for the IM Address"));

            return false;

        }

        else

        {

            return true;

        }

    }

    #endregion

}

 

6 comments:

  1. Hi Rudy,

    This is certainly a very useful article about validation. It gives me a direction about implementing a Validate() method in all my BO. I am interested to see what kind of code is inside the CommonBase,DTOBase. Could u please provide that?

    ReplyDelete
  2. This comment has been removed by the author.

    ReplyDelete
  3. Hi Rudy,
    Could you give me some more info about the "ValidationRules"? is it a built in class in Dotnet or your custom implementation?

    I am using LLBLGen for DAL layer. I am creating a cusotm BL class which contains fields from more than one entity. This class will have some complex observable collection as it's property. For these kind of fields what is your suggested approach for validation?

    ReplyDelete
  4. @saravana, the ValidationRules was my own custom class. The code you're looking at is a little old. You may want to take a look at some of the more recent Agile ADO.Net Persistence Layer posts.

    ReplyDelete
  5. whoa!!!, you had a passion in blogging, thumbs up for your work of love.. Hehe very inspiring ideas,


    anyway I'm william
    mind if I put a link back to you?


    see my works here ------> Suits

    ReplyDelete
  6. In a recent project, I created a layer separating the UI (ASP.NET MVC) from the business layer. This layer in between was responsible for translating web elements (form data, query strings, config. settings, session values, etc.) to business objects. Part of the transformation step was validation. This layer was also responsible for building view models used by the UI. This was a really nice break out because it allowed my business logic to be entirely focused on a single job (using a well-defined object model) and be reused by the web site and other services. The validation, in a sense, became its own set of independent business objects. It led to a lot of reuse. My MVC controllers had a very small job - they kicked off the task, got feedback and responded accordingly.

    ReplyDelete