Introduction
In the previous article, I described how Jeneva.Net helps to solve common problems in implementing WebAPI-based web applications. This article shows how Jeneva.Net can also greatly simplify your validation routines.
*Note, this article assumes that you have gone through my previous article.
Background
If you apply the techniques from my previous article in your web application, it can greatly reduce the amount of custom coding. But there is still one very important and useful feature in Jeneva.Net - validation.
The implementation is pretty simple. First of all, you have to identify classes that require validation, usually these are domain classes. Secondly, you have to identify validation groups for each class - for example: class Client
has two groups - validation before save and validation before update. This means that the same class - Client
can be validated two different ways - for save and for update. Sometimes, you may require different validation groups - for example, Assign or Merge or whatever. And the last step is implementing a validator class. For example, the Client
class will have validator - ClientValidator
.
Implementing
Let's create these validation methods for the Client
class. In order to follow all the SOLID principles, I will create a separate class - ClientValidator
which will contain these validation methods. The main idea of the Jeneva-based validation is the following - You must create a new instance of the JenevaValidationContext
class everytime you need to validate something. Everytime you find an error, you must register it in the context instance using its methods. Using the context instance ensures that each error message is tied with the corresponding property path. By the way, the context class already contains several simple validation routines, for example, it can check whether a particular field is null
, or if it is present in JSON, it can check text length or collection's size, it can check regular expressions, etc. As you must know, Jeneva manages data deserialization, and it stores information about each JSON field, therefore later you can validate if a field was present in JSON, if it was null
or if it was correctly parsed during deserialization. This information is accessible through the JenevaValidationContext
class methods.
One of the main goals of the JenevaValidationContext
class is to keep track of the property path. For example, when you validate an object and then validate its nested children, the context class ensures that all error messages are connected with corresponding property paths. The result of validation is a list of failures, where a failure is a property path text and a message. This failure structure is serializaed to JSON and sent back to browser, where it is parsed and displayed correctly in correct places in HTML.
The best practice would be deriving from theJenevaValidationContext
class, extending some additional application-specific validation routines there and then using the subclass in the validation methods. Below is the example of how to extend the context class:
public class ValidationContext : JenevaValidationContext
{
public ValidationContext(IJenevaContext jenevaContext)
: base(jenevaContext)
{
}
public void Required()
{
this.Assigned("is required");
if (this.IsFieldValid && this.IsValidFormat())
{
this.NotNull("is required");
}
}
public void Missing()
{
this.NotAssigned("must be empty");
}
public void Number()
{
this.ValidFormat("must be valid number");
}
public void Date()
{
this.ValidFormat("must be valid date");
}
public void Float()
{
this.ValidFormat("must be valid floating point number");
}
public void Email()
{
const string expr = @"\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}\b";
this.Regex(expr, "must be valid Email");
}
public void Text()
{
this.StringLengthBetween(3, 20, "must be between 3 and 20 characters");
}
public void TrueFalse()
{
this.ValidFormat("must be 'yes' or 'no'");
}
}
The logic above is extremely helpful and will make the validation methods extremely simple and readable. For example, I will no longer have to write redundant error messages for string
s between 3 and 20 characters, I will use Text()
. The same is for dates, doubles, numbers, required fields, etc.
In the following piece of code, you can see how validation method for Save
is implemented ValidateForSave()
:
public class ClientValidator : IClientValidator
{
public ILoginValidator LoginValidator { get; set; }
public void ValidateForSave(Client target, IValidationContext context)
{
context.SetField("id", target.Id); context.Missing();
context.SetField("name", target.Name); context.Required(); context.Text();
context.SetField("lastname", target.Lastname);
context.Required();
context.Text();
context.SetField("age", target.Age);
context.Required();
context.Number();
context.MustBeGreaterThan(0, "must be greater than zero");
context.SetField("logins", target.Logins);
context.Required();
context.MustHaveCountBetween(1, 5, "must be at least one login");
context.AddNested(); int index = 0;
foreach (Login login in target.Logins)
{
context.SetIndex(index++); context.SetTarget(login); this.LoginValidator.ValidateForSave(login, context);
}
context.RemoveNested(); }
}
The SetField()
method tells the context what field is currently validated, i.e., if you register a failure in the context, it will have property path of the current field. Validation routines must go after the SetFiled()
method call. TheJenevaValidationContext
class contains numerous validation routines. These routines usually do two actions: first - they check a condition (for example, check if field value is null
), and second - they register a failure if the condition is not true (using the Fail()
method). For example, Assigned()
- checks if field is assigned (is present in JSON) and if false
- it registers a failure using the current property path; NotNull()
, Null()
are selfdescriptive; ValidFormat()
- registers a failure if a field value is not correctly parsed (integer
or double
) from JSON.
As you have noticed, the JenevaValidationContext
class, besides the validation routines methods, also contains other important methods: SetTarget()
sets up a current validated object, this method is important and should always be called before any validation routine; AddNested()
- this method propagates current property in the property path as nested object, all subsequent calls to SetField()
will result in concatenating property name with the nested object name; the RemoveNested()
method does the inverse; the SetIndex()
method also adds indexing to the current property path - "[" + index + "]
".
Here you can see validation methods for the Login
class.
public class LoginValidator : ILoginValidator
{
public void ValidateForSave(Login target, IValidationContext context)
{
context.SetField("id", target.Id);
context.Missing();
context.SetField("name", target.Name);
context.Required();
context.Text();
context.SetField("password", target.Password);
context.Required();
context.Text();
context.SetField("enabled", target.Enabled);
context.TrueFalse();
context.SetField("client", target.Client);
context.Missing();
}
}
When the validators for all the domain and DTO classes are done, we are free to define a facade class which will be injected into our services or controllers and which will be used to fire validation. Here is an example of the validation facade:
public class ValidationFacade : IValidationFacade
{
public IValidationContextFactory ContextFactory { get; set; }
public IClientValidator ClientValidator { get; set; }
public void AssertClientForSave(Client target)
{
IValidationContext context = this.ContextFactory.GetNew();
context.SetTarget(target);
this.ClientValidator.ValidateForSave(target, context);
context.Assert();
}
public void AssertClientForUpdate(Client target)
{
IValidationContext context = this.ContextFactory.GetNew();
context.SetTarget(target);
this.ClientValidator.ValidateForUpdate(target, context);
context.Assert();
}
}
The class looks pretty simple, here I use simple factory method to get a new instance of the validation context. The most important is the last line of each method - Assert()
- this method throws ValidationException
if at least one error is registered in the context, otherwise it does nothing.
Here you can see how the facade is injected and used in the service layer:
public class ClientService : IClientService
{
public IMapper Mapper { get; set; }
public IValidationFacade Validator { get; set; }
public IClientDao ClientDao { get; set; }
public Client GetById(int id)
{
Client item = this.ClientDao.GetById(id);
return this.Mapper.Filter(item, Levels.DEEP);
}
public IList<client> GetAll()
{
IList<client> items = this.ClientDao.GetAll();
return this.Mapper.FilterList(items, Levels.GRID);
}
public void Save(Client item)
{
this.Validator.AssertClientForSave(item);
using (ITransaction tx = this.ClientDao.BeginTransaction())
{
this.Mapper.Map(item);
this.ClientDao.Save(item);
tx.Commit();
}
}
public void Update(Client item)
{
this.Validator.AssertClientForUpdate(item);
using (ITransaction tx = this.ClientDao.BeginTransaction())
{
Client existing = this.ClientDao.GetById(item.Id.Value);
this.Mapper.MapTo(item, existing);
this.ClientDao.Merge(existing);
tx.Commit();
}
}
}
By the way, the validation context class is quite handy and easy to extend and use it differently. You can always play with its methods and get a different behavior. In the example code, you will see how it is used for validation in different circumstances - for example, validation before Delete
. Please, see the AssertClientExists()
and the AssertMoreThanOneClient()
methods in the ValidationFacade
class.
public void AssertClientExists(Client item)
{
if (item == null)
{
IValidationContext context = this.ContextFactory.GetNew();
context.Fail("Client does not exist");
context.Assert();
}
}
public void AssertMoreThanOneClient(long count)
{
if (count == 1)
{
IValidationContext context = this.ContextFactory.GetNew();
context.Fail("Cannot delete the only client");
context.Assert();
}
}
In these methods, you don't rely on a target validated object. You just have to assert that some condition is met. In this case, a ValidationException
is thrown with jsut on failure in it, and property path is empty.
And that is it. Now validation must work. If you call the AssertValid()
method from your business layer, it will identify which type validator class to use, based on the provided group. Then it will invoke implementation of the abstract Validate()
method. If validation succeeds, nothing happens. If validation fails, ValidationException
is thrown. ValidationException
will contain a list of name-value pairs - property path and error message. In order to process this exception properly in ASP.NET MVC, I will create and register a custom ExceptionFilterAttribute
. This technique is common in ASP.NET MVC, you can find how to do it in the internet. So, here is the implementation of the custom Exception filter.
public class ErrorFilterAttribute : ExceptionFilterAttribute
{
public override void OnException(HttpActionExecutedContext context)
{
FailResponse response;
if (context.Exception is ValidationException)
{
response = (context.Exception as ValidationException).BuildFailResponse();
}
else
{
response = new FailResponse(context.Exception.ToString());
}
context.Response =
context.Request.CreateResponse(HttpStatusCode.InternalServerError, response);
}
}
I replace the HTTP response content with my JSON array of (property path - error message) pairs - FailResponse
class.
Now I have to register ErrorFilterAttribute
. I have to add this line in the Global.asax Application_Start
event.
GlobalConfiguration.Configuration.Filters.Add(new ErrorFilterAttribute());
Now, every time the ValidationException
is thrown, it will be handled by ErrorHandlerAttribute
handler, and a list of property paths and error messages will be sent back as JSON in the HTTP response.
Front-end
The last step is displaying those messages in the correct place in HTML. You are free to write your custom JavaScript for AngularJS or KnockoutJS, or anything else. Jeneva.Net comes with a small JavaScript library for AngularJS validation. These libraries make it simpler displaying those errors. For example, if you use angular, your HTML must look like this:
<label>Name:</label>
<input name="name" type="text" ng-model="name" jv-path="name" />
<span class="error" ng-if="form.name.$error.jvpath" ng-repeat="msg in form.name.$jvlist">{{msg}}</span>
The jVpath
directive works the same way as any AngularJS validation directives i.e. it tells that the name textbox is responsible for the "name" property path. The span below will repeatedly display the validation messages assigned to the "name" property path.
You can find out how to create a Single-Page Application (SPA) using AngularJS in my next article: AngularJS single-page app and Upida/Jeneva.Net.
References