lunes, 5 de julio de 2010

Vale: Framework de validación de la comunidad Alt.Net Hispano

Hace algunos días nomás la comunidad de Alt.Net Hispano ha dado vida a un nuevo proyecto de código abierto llamado Vale.

A partir de la iniciativa de Fabio Maulo y el apoyo de Jorge Gamba, José Romaniello y otros se decidió hacer un framework de validación que sirva de puerta para que más gente se acerque al código abierto.

Metodología de desarrollo

Una de las grandes cosas que tiene este proyecto es que el desarrollo se hace en vivo…si si, cada tanto (por ahora semanalmente pero no hay un periodo fijo) a través de un Alt.Net Café alguien (Fabio generalmente) va programando en vivo el desarrollo, gracias a la magia de internet todos pueden opinar durante el desarrollo usando TDD.

Se se realizaron dos de estos encuentros y el framework está casi para usarlo, dejo el link del primero

Primer Alt.Net Café “Vale: desarrollando un framework desde cero con TDD”

Hablemos del framework, validación no - intrusiva

Algunos comentarios de Vale, en comparación con otros frameworks de validación como EntLib VAP, el primero que me viene a la mente es la validación no intrusiva, es decir en lugar de tener una clase Person así:

public class Person
{
    public string Name { get; set; }
    public string Surname { get; set; }
    public DateTime Birthday { get; set; }
}

y para validar agregar atributos en las propiedades así:

{
    [RequiredValidator]
    public string Name { get; set; }
    [StringLenghtValidator(20)]
    public string Surname    { get; set; }
    [TypeValidator(typeof(DateTime)]
    public DateTime Birthday { get; set; }
}

un método que deja “pegadas” las validaciones al modelo, con Vale hacemos esto:

IValidator validator = new Validator();
validator.Register<Person>(p => validator.StringIsRequired("Name", p.Name));
validator.Register<Person>(p => validator.StringIsRequired("Surname", p.Surname));
validator.Register<Person>(p => validator.DateIsInThePast("Birthday", p.Birthday));

La interface IValidator es tiene un método Register<T> que permite registrar validaciones sobre un tipo en particular sin que dicho tipo no se modifique, interesante.

Analizando la implementación:

Como dije antes el desarrollo se hizo on-line y se fueron tomando decisiones allí mismo, vamos a recorrer el código de esta versión y a contar un poco el por qué de cada parte.

La interface IValidator

public interface IValidator
{
    void Register<T>(Func<T, string> assert);
    IEnumerable<string> GetInvalidMessages(object entity);
}

Básicamente la idea de esta interface es permitir a través del método Register<T> registrar validaciones para un tipo en particular, y con IEnumerable<T> GetInvalidMessages  recuperar los errores, la utilización es la siguiente:

IValidator validator = new Validator();
validator.Register<Person>(p => validator.StringIsRequired("Name", p.Name));

Para todos las entidades del tipo Person que quisiéramos validar se aplicarán todas las reglas que hayamos registrado de este modo.

Actualmente no existe la posibilidad de tener diferentes políticas o reglas de validación para hacer algo así:

IValidator validator = new Validator();
validator.Register<Person>(p => validator.StringIsRequired("Name", p.Name), "default");

var invalidMessages = validator.GetInvalidMessages(person, "default");

Por el momento ya subí el Issue a codeplex, veremos si se implementa en alguna versión, o no, lo decidirá la comunidad.

El otro método que tiene por ahora la interface es IEnumerable<T> GetInvalidMessage, que básicamente aplica las validaciones y recupera los errores.

La implementación interna

Pongo es código de Validator en su primer versión (quité un par de líneas para simplificar) comentadas para su comprensión

public class Validator : IValidator
{
    /// <summary>
    /// Lista de (tipo, lista) de validaciones para el tipo, 
    /// el tipo es la clave del diccionario
    /// </summary>
    Dictionary<Type, List<Func<object, string>>> validatorsHolder = 
        new Dictionary<Type, List<Func<object, string>>>();

    /// <summary>
    /// Permite registrar validaciones para un tipo, si ya existen validaciones 
    /// para el tipo las agrega sino crea el grupo de validaciones.
    /// Al permitir que los validadores sean Func&lt;T,string&gt; se puede hacer
    /// validator.Register&lt;Person&gt;(p => validator.StringIsRequired("Name value")
    /// llamar al Func&lt;T,string&gt; y recuperar el string como resultado
    /// de la ejecución de la Func sobre T
    /// </summary>
    /// <typeparam name="T">El tipo al que se aplican las validaciones</typeparam>
    /// <param name="assert">La validación</param>
    public void Register<T>(Func<T, string> assert)
    {
        //recupera el tipo al que se aplicaran las validaciones
        var objType = typeof(T);

        //verifica si ya existe la lista de validaciones para T, sino la crea
        List<Func<object, string>> actions;
        if (!validatorsHolder.TryGetValue(objType, out actions))
        {
            actions = new List<Func<object, string>>();
        }

        //agrega la nueva validación a la lista de validaciones
        actions.Add(x => assert((T)x));

        //agrega la lista al holder de todas las validaciones
        validatorsHolder[objType] = actions;
    }

    /// <summary>
    /// Ejecuta las validaciones sobre la instancia y retorna los mensajes de error
    /// </summary>
    /// <param name="entity">La entidad sobre la que se aplicarán las validaciones</param>
    /// <returns>El listado de error, string[0] si no existen</returns>
    public IEnumerable<string> GetInvalidMessages(object entity)
    {

        //recupera el tipo del objeto a validar
        var objType = entity.GetType();

        
        List<Func<object, string>> actions;
        //si existe una lista de validaciones para T se ejecutan
        if (validatorsHolder.TryGetValue(objType, out actions))
        {
            //se llama a todas las validaciones dentro de actions 
            //(las validaciones para T dentro del holder)
            //ejecuta las action que no tienen mensaje vacio
            return actions.Select(action => action(entity))
                .Where(message => !string.Empty.Equals(message));
        }
        return new string[0];
    }

Como se puede ver es muy sencillo, un IDictionary que tiene como clave de cada valor el tipo (el tipo que queremos validar) y como valor una lista de validadores, que en este caso son Func<T,string> donde T es el mismo tipo que queremos validar y string el mensaje de error.

Entonces un validador no sería más que un simple método que acepte T y retorne un string en caso de error, la implementación más sencilla sería:

public class Validadores
{
    public string StringIsRequired(string value)
    {
        if (string.IsNullOrEmpty(value))
        {
            return string.Format("field is required.");
        }
        return string.Empty;
    }

}

Para usarlo así:

IValidator validator = new Validator();
validator.Register<Person>(p => new Validadores().StringIsRequired(p.Name));

var person = new Person { Name = null, Surname = null, Birthday = DateTime.Today };
var invalidMessages = validator.GetInvalidMessages(person);

Para mayor comodidad se implementaron como Extension Methods para IValidator en la clase estática CommonValidator y el uso es el siguiente:

IValidator validator = new Validator();
validator.Register<Person>(p => validator.StringIsRequired("Name", p.Name));
validator.Register<Person>(p => validator.StringIsRequired("Surname", p.Surname));
validator.Register<Person>(p => validator.DateIsInThePast("Birthday", p.Birthday));

var person = new Person { Name = null, Surname = null, Birthday = DateTime.Today };
var invalidMessages = validator.GetInvalidMessages(person);

Mágico por todos lados, nos vemos la próxima.

Links

Proyecto Vale en Codeplex

Twitter de la comunidad Alt.Net Hispano

Lista de correo de Alt.Net Hispano

3 comentarios:

Rodrigo Barboni dijo...

Hola Leo, muy interesante al explicacion del frame de validacion, necesito implementar algo asi en mi capa de negocio para validar los datos en negocio y estoy examinando esta opcion

te hago una consulta entiendo tus ejemplos de uso pero revise el codigo que base y en los test hacen:
test:

IValidator validator = new Validator();
//registra las validaciones que se haran sobre el tipo Person
validator.Register(p => validator.StringIsRequired("Name", p.Name));
validator.Register(p => validator.StringIsRequired("Surname", p.Surname));
validator.Register(p => validator.DateIsInThePast("Birthday", p.Birthday));

//carga los datos
var person = new Person { Name = null, Surname = null, Birthday = DateTime.Today };

//obtiene los mensajes de error si hay para las validaciones registradas
// GetInvalidMessages, aplica las validaciones y recupera los errores
var invalidMessages = validator.GetInvalidMessages(person);

//no entiendo esta parte: que hace aca?
invalidMessages.Should().Contain("Name is required.");
invalidMessages.Should().Contain("Surname is required.");
invalidMessages.Should().Contain("Birthday should be in the past.");

no entiendo que se hace en las ultimas lineas, me podes dar una mano

esta buena la idea de ir agregando metodos de validacion y luego poder registrar en el codigo esas validaciones

yo haria una validacion que controle el tipo de datos, si es requerdio o no, la longitud maxima, si permite null sobre un valor pasado, todo en una misma funcion, entonces para la mayoria de los campos regitro como validacion esa funcion, que haga todo en uno y no por separado, podria ser no

LgEaObNrAiReDlO dijo...

Hola Rodrigo,
Esas líneas son parte de la verificación de la prueba unitaria, el ejemplo es una prueba, al final para validar que el resultado es el esperado se hacen esas verificaciones (llamadas assert), en este caso haciendo uso de la libería SharpTestEx
http://sharptestex.codeplex.com/
Si, lo bueno es que podés crear tus propios validadores con poco esfuerzo entonces podrías hacer un validador que agrupe esas validaciones que vos decís, el único tema es que al hacerlas juntas el mensaje de error no te va a indicar exáctamente qué falló.

Rodrigo Barboni dijo...

Hola Leo, gracias por aclararme el tema, te hago una consulta mas
porque estoy confundido con la idea general, con el tema de registar validaciones al tipo
y ejecutar las validaciones a ese tipo
yo pensaba que las validaciones se acumulan en un diccionario y luego se aplicaban a un valor pasado como parametro

en este ejemplo:

IValidator validator = new Validator();
validator.Register(p => validator.StringIsRequired("Name", p.Name));

var person = new Person { Name = null, Surname = null, Birthday = DateTime.Today };
var invalidMessages = validator.GetInvalidMessages(person);



GetInvalidMessage, aplica las validaciones sobre el tipo T y recupera los errores


No entiendo, para que se pasa el tipo al metodo register
luego se tendria que aplicar esas validaciones sobre el valor pasado p.Name, en el ejemplo
no sobre un tipo, que pasa si quiero validar un campo que no pertenece
a ningun tipo entidad T, sino que es un tipo de valor por ejemplo una variable int32


validatorsHolder: un IDictionary que tiene como clave de cada valor el tipo (el tipo que queremos validar)
y como valor una lista de validadores, que en este caso son Func
donde T es el mismo tipo que queremos validar y string el mensaje de error

se supone que las validaciones serian todas para el mismo valor pasado como parametro
no entiendo porque se pasa el tipo, puedo pasar tipos distitntos en una misma instancia
de la clase validator

mi idea no es validar un tipo entidad, o sea aplicar las validaciones que quiero a un valor
que paso como parametro que puede o no pertenecer a una entidad pero podria ser
un campo de variable

disculpa mi ignorancia pero estoy confundido con el concepto del funciomiento que hace con el tipo al pasarlo como parametro y gracias de antemano