Try fast search NHibernate

23 October 2009

NHibernate.Validator : Fine grained and polymorphic validation

If you know that you can define constraints for classes, properties and even for field, what mean “Fine grained validation” ?

In NHV, when you define a validator you are defining a validator for a System.Type… well… for real you can define a validator for various types but let me simplify the concept a little bit (parafrasando lo psico nano: “mi consenta”).

In OOP the Type can be polymorphic and we know it.

A classic example

AnimalsDiagramIn this schema you can define validators for each Type. The Animal may have its validation, Mammal may have its validation and so on.

A Cat is a Mammal and is a Animal but is not a Reptile… clear, no?

Each time you will register a Type in the ValidatorEngine NHV will investigate the Type graph and will register the validator of each Type in the graph as part of the validation of the Type you are register.

For the above diagram this mean:

Because a Cat is a Mammal it must satisfy the same rules defined for Mammal and because a Mammal is a Animal it must satisfy the same rules defined for Animal.

Because a Reptile is a Animal it must satisfy the same rules defined for Animal.

Because a Dog is a Mammal it must satisfy the same rules defined for Mammal and because a Mammal is a Animal it must satisfy the same rules defined for Animal. Because a Dog is not a Cat it shouldn't satisfy the rules defined for a Cat.

Composition through interfaces

The domain:

public interface IAdress
{
string Street { get; }
int Number { get; }
}

public interface IEntreCalles
{
string CalleA { get; }
string CalleB { get; }
}

public class DireccionArgentina: IAdress, IEntreCalles
{
public string Street { get; set; }
public int Number { get; set; }
public string CalleA { get; set; }
public string CalleB { get; set; }
public string CodigoPostal { get; set; }
}

IAddress is a generic interface to define an address. IEntreCalle (that mean “between streets”) is another generic interface for some south-America countries. DireccionArgentina define an address for Argentina with its Postal-Code and, as you can see, it implements the others two interfaces.

As I done with domain classes, I would define my generic constraints for each Type.

public class AddressValidation: ValidationDef<IAdress>
{
public AddressValidation()
{
Define(a => a.Street).NotNullableAndNotEmpty();
Define(a => a.Number).GreaterThanOrEqualTo(1);
}
}

public class EntreCallesValidation : ValidationDef<IEntreCalles>
{
public EntreCallesValidation()
{
ValidateInstance.By(IsValid)
.WithMessage("Debe especificar ambas calles o ninguna.");
}

public bool IsValid(IEntreCalles subject, IConstraintValidatorContext context)
{
if(subject == null)
{
return true;
}
var calleA = subject.CalleA == null ? string.Empty : subject.CalleA.Trim();
var calleB = subject.CalleB == null ? string.Empty : subject.CalleB.Trim();
return !(string.Empty.Equals(calleA) ^ string.Empty.Equals(calleB));
}
}

public class DireccionArgentinaValidation: ValidationDef<DireccionArgentina>
{
public DireccionArgentinaValidation()
{
Define(da => da.CodigoPostal)
.MatchWith("[A-Z][0-9]{4}[A-Z]{3}")
.WithMessage("No es un codigo postal Argentino.");
}
}

Each Type has its own specific constraints and the follow test has green-field:

var configure = new FluentConfiguration();
configure.Register(new[]
{
typeof(AddressValidation),
typeof(EntreCallesValidation),
typeof(DireccionArgentinaValidation)
})
.SetDefaultValidatorMode(ValidatorMode.UseExternal);
var ve = new ValidatorEngine();

ve.Configure(configure);
var valid = new DireccionArgentina
{
Street = "Cabildo",
Number = 2450,
CalleA = "Monroe",
CalleB = "Blanco Encalada",
CodigoPostal = "C1428AAT"
};
ve.IsValid(valid).Should().Be.True();

var notValidBecauseStreetNull = new DireccionArgentina
{
Number = 2450,
CalleA = "Monroe",
CalleB = "Blanco Encalada",
CodigoPostal = "C1428AAT"
};
ve.IsValid(notValidBecauseStreetNull).Should().Be.False();

var notValidBecauseCalleAEmpty = new DireccionArgentina
{
Street = "Cabildo",
Number = 2450,
CalleA = " ",
CalleB = "Blanco Encalada",
CodigoPostal = "C1428AAT"
};
ve.IsValid(notValidBecauseCalleAEmpty).Should().Be.False();

var notValidBecauseWrongCodigoPostal = new DireccionArgentina
{
Street = "Cabildo",
Number = 2450,
CalleA = "Monroe",
CalleB = "Blanco Encalada",
CodigoPostal = "1428"
};
ve.IsValid(notValidBecauseWrongCodigoPostal).Should().Be.False();

Which is the trick ?

A DireccionArgentina "is a" IAddress and "is a" IEntreCalle.

The Rule

As you can see the rule to follow is simple and is: "is a" no matter if the Type is a concrete class, an abstract class or an interface.

3 comments:

  1. What IF you don't have a ValidationDef for DireccionArgentina. The other rules that were defined on IEntreCalles and IAddres will be applicable to DireccionArgentina?
    Well, I know the answer with the trunk version, but I want to know your opinion.

    ReplyDelete
  2. Is this polimorphic validation valid for ddl(NHib export schema). I tried http://groups.google.com/group/nhusers/browse_thread/thread/2bd3bb0fe3d467d4/ffc183334cdfa4b7?lnk=raot and couple other variants and I am still in dead end.

    ReplyDelete
  3. the spanish code i kinda confusing. you know? ;p

    ReplyDelete