Try fast search NHibernate

26 October 2009

NHibernate.Validator : Extending ValidationDef

In various applications I’m using my implementation of Range.

public interface IRange<T> : IEquatable<IRange<T>> where T : IComparable<T>
{
T LowLimit { get; }
T HighLimit { get; }
bool IsEmpty { get; }
bool Includes(T value);
bool Includes(IRange<T> other);
bool Overlaps(IRange<T> other);
}

As you can imagine I’m using it to represent various kind of ranges and in each usage I need to validate the range in various ways. Two simply examples:

public class EmployeePosition
{
public string Description { get; set; }
public IRange<decimal> Salary { get; set; }
}

public class StandUpMeeting
{
public ICollection<Employee> Employees { get; set; }
public IRange<DateTime> Lapse { get; set; }
}

To validate IRange<T> properties, through NHV-loquacious-configuration, I can’t extend some NHV’s interfaces because NHV doesn’t know my type (IRange<T>) and a simple entity-validator (as showed here) is not enough because I need to validate various situation of the range; the way to go is create my own set of constraints.

Custom constraints (extending ValidationDef)

public interface IRangeConstraints<T> : ISatisfier<IRange<T>, IRangeConstraints<T>>
where T : IComparable<T>
{
IChainableConstraint<IRangeConstraints<T>> IsValid();
IChainableConstraint<IRangeConstraints<T>> NotEmpty();
IChainableConstraint<IRangeConstraints<T>> Include(T value);
IChainableConstraint<IRangeConstraints<T>> Include(IRange<T> range);
IChainableConstraint<IRangeConstraints<T>> Overlaps(IRange<T> range);
}

Defined the interface I need an entry point to integrate it with the Loquacious configuration. The “natural” extension-point seems the class ValidationDef<T>. There are various way to extend the ValidationDef<T> but in my opinion the most clear, the most useful and the most easy is a simple inheritance.

public class ValidationDefEx<T> : ValidationDef<T> where T : class
{
public IRangeConstraints<decimal> Define(Expression<Func<T, IRange<decimal>>> property)
{
return null;
}

public IRangeConstraints<DateTime> Define(Expression<Func<T, IRange<DateTime>>> property)
{
return null;
}
}

Now I’m ready to check the API.

public class EmployeePositionValidation : ValidationDefEx<EmployeePosition>
{
public EmployeePositionValidation()
{
const decimal avgSalary = 4000m;

Define(e => e.Description).NotNullableAndNotEmpty();
Define(ep => ep.Salary)
.IsValid()
.WithMessage("The {property.salary} should be valid but was ${Salary}.")
.And
.NotEmpty()
.And
.Include(avgSalary)
.WithMessage("The {property.salary} should be around " + avgSalary);
}
}

public class StandUpMeetingValidation : ValidationDefEx<StandUpMeeting>
{
public StandUpMeetingValidation()
{
Define(m => m.Lapse)
.IsValid()
.WithMessage("The {property.lapse} should be valid but was ${Lapse}.")
.And
.NotEmpty();
}
}

The base API work fine; I can go to tests and implementation.

public class RangeConstraints<TR> : BaseConstraints<IRangeConstraints<TR>>, IRangeConstraints<TR>
where TR : IComparable<TR>
{
#region Implementation of IRangeConstraints<TR>

public RangeConstraints(IConstraintAggregator parent, MemberInfo member) : base(parent, member) {}

public IChainableConstraint<IRangeConstraints<TR>> IsValid()
{
return Satisfy(r => r.LowLimit.CompareTo(r.HighLimit) <= 0)
.WithMessage("{validator.range.IsValid}");
}

public IChainableConstraint<IRangeConstraints<TR>> NotEmpty()
{
return Satisfy(r => !r.IsEmpty)
.WithMessage("{validator.range.NotEmpty}");
}

public IChainableConstraint<IRangeConstraints<TR>> Include(TR value)
{
return Satisfy(r => r.Includes(value))
.WithMessage("{validator.range.Include}" + value);
}

public IChainableConstraint<IRangeConstraints<TR>> Include(IRange<TR> range)
{
return Satisfy(r => r.Includes(range))
.WithMessage("{validator.range.Include}" + range);
}

public IChainableConstraint<IRangeConstraints<TR>> Overlaps(IRange<TR> range)
{
return Satisfy(r => r.Overlaps(range))
.WithMessage("{validator.range.Overlaps}" + range);
}

#endregion

#region
Implementation of ISatisfier<IRange<TR>,IRangeConstraints<TR>>

public IChainableConstraint<IRangeConstraints<TR>> Satisfy(Func<IRange<TR>, IConstraintValidatorContext, bool> isValidDelegate)
{
var attribute = new DelegatedValidatorAttribute(new DelegatedConstraint<IRange<TR>>(isValidDelegate));
return AddWithConstraintsChain(attribute);
}

public IChainableConstraint<IRangeConstraints<TR>> Satisfy(Func<IRange<TR>, bool> isValidDelegate)
{
var attribute = new DelegatedValidatorAttribute(new DelegatedSimpleConstraint<IRange<TR>>(isValidDelegate));
return AddWithConstraintsChain(attribute);
}

#endregion
}

and the my validation definition extension look like

public class ValidationDefEx<T> : ValidationDef<T> where T : class
{
public IRangeConstraints<decimal> Define(Expression<Func<T, IRange<decimal>>> property)
{
return new RangeConstraints<decimal>(this, TypeUtils.DecodeMemberAccessExpression(property));
}

public IRangeConstraints<DateTime> Define(Expression<Func<T, IRange<DateTime>>> property)
{
return new RangeConstraints<DateTime>(this, TypeUtils.DecodeMemberAccessExpression(property));
}
}

Work done!! Now I have my own set of constraints for my IRange<T> and I can use it with NHV.

Ups!!! … new request: I must validate the gap of the Salary and the time of the standup-meeting.

Extending the Extension

public static class RangeConstraintsExtensions
{
public static IChainableConstraint<IRangeConstraints<decimal>>
GapLessThanOrEqualTo(this IRangeConstraints<decimal> definition, decimal value)
{
return
definition.Satisfy(r => r.HighLimit - r.LowLimit <= value)
.WithMessage("{validator.range.GapLessThanOrEqualTo}" + value);
}

public static IChainableConstraint<IRangeConstraints<decimal>>
GapGreaterThanOrEqualTo(this IRangeConstraints<decimal> definition, decimal value)
{
return
definition.Satisfy(r => r.HighLimit - r.LowLimit >= value)
.WithMessage("{validator.range.GapGreaterThanOrEqualTo}" + value);
}

public static IChainableConstraint<IRangeConstraints<DateTime>>
GapLessThanOrEqualTo(this IRangeConstraints<DateTime> definition, TimeSpan value)
{
return
definition.Satisfy(r => r.HighLimit - r.LowLimit <= value)
.WithMessage("{validator.TimeRange.GapLessThanOrEqualTo}" + value);
}

public static IChainableConstraint<IRangeConstraints<DateTime>>
GapGreaterThanOrEqualTo(this IRangeConstraints<DateTime> definition, TimeSpan value)
{
return
definition.Satisfy(r => r.HighLimit - r.LowLimit >= value)
.WithMessage("{validator.TimeRange.GapGreaterThanOrEqualTo}" + value);
}
}

And now my two definitions can look like

public class EmployeePositionValidation : ValidationDefEx<EmployeePosition>
{
public EmployeePositionValidation()
{
const decimal avgSalary = 4000m;
const decimal salaryGap = 1500m;

Define(e => e.Description).NotNullableAndNotEmpty();
Define(ep => ep.Salary)
.IsValid()
.WithMessage("The {property.salary} should be valid but was ${Salary}.")
.And
.NotEmpty()
.And
.GapLessThanOrEqualTo(salaryGap)
.WithMessage("The gap of {property.salary} should be " + salaryGap)
.And
.Include(avgSalary)
.WithMessage("The {property.salary} should be around " + avgSalary);
}
}

public class StandUpMeetingValidation : ValidationDefEx<StandUpMeeting>
{
public StandUpMeetingValidation()
{
TimeSpan meetingTime = TimeSpan.FromMinutes(20);

Define(m => m.Lapse)
.IsValid()
.WithMessage("The {property.lapse} should be valid but was ${Lapse}.")
.And
.NotEmpty()
.And
.Satisfy(
r =>(new Range<DateTime>
{
LowLimit = r.LowLimit.Date.AddHours(9),
HighLimit = r.LowLimit.Date.AddHours(17).AddMinutes(30)
}).Includes(r)
)
.WithMessage("The meeting should happen during working time")
.And
.GapLessThanOrEqualTo(meetingTime)
.WithMessage("The {entity.StandUpMeeting} was too long.");
}
}

Conclusion

Now you know one reason because NHibernate.Validator is part of my Gum-Architecture: it is compressible, extensible, ball-able, cube-able, flat-able… ;) and the story does not end here...

6 comments:

  1. "My implementation of Rage" is an excellent band name.

    ReplyDelete
  2. Hello Fabio,

    I just started to play around with "Loquacious", and it seems to be really good.

    However, for some reason, I can't put it to work together with hbm2ddl.SchemaExport.

    Do you know if there is any impediment while exporting the fluent validation to the database schema, as we are used to do through attributes?

    Thanks,

    ReplyDelete
  3. var nhvConfiguration = new FluentConfiguration();
    nhvConfiguration
    .SetDefaultValidatorMode(ValidatorMode.UseExternal)
    .Register(Assembly.Load("Dll.Where.ValidationDefAre")
    .ValidationDefinitions())
    .IntegrateWithNHibernate
    .ApplyingDDLConstraints()
    .And
    .RegisteringListeners();
    You are forgetting this
    .SetDefaultValidatorMode(ValidatorMode.UseExternal)

    ReplyDelete
  4. Can you answer this:

    http://stackoverflow.com/questions/3738570/nhibernate-validator-loquacious-fluent-api-and-schema-export-question

    ReplyDelete
  5. an int can't be null, for int? we will check btw the JIRA, for feature request or bugs is not in stackoverflow.

    http://216.121.112.228/browse/NHV#selectedTab=com.atlassian.jira.plugin.system.project%3Aissues-panel

    ReplyDelete