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.");
}
}
"My implementation of Rage" is an excellent band name.
ReplyDeleteTYPO fixed.
ReplyDeleteThanks.
Hello Fabio,
ReplyDeleteI 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,
var nhvConfiguration = new FluentConfiguration();
ReplyDeletenhvConfiguration
.SetDefaultValidatorMode(ValidatorMode.UseExternal)
.Register(Assembly.Load("Dll.Where.ValidationDefAre")
.ValidationDefinitions())
.IntegrateWithNHibernate
.ApplyingDDLConstraints()
.And
.RegisteringListeners();
You are forgetting this
.SetDefaultValidatorMode(ValidatorMode.UseExternal)
Can you answer this:
ReplyDeletehttp://stackoverflow.com/questions/3738570/nhibernate-validator-loquacious-fluent-api-and-schema-export-question
an int can't be null, for int? we will check btw the JIRA, for feature request or bugs is not in stackoverflow.
ReplyDeletehttp://216.121.112.228/browse/NHV#selectedTab=com.atlassian.jira.plugin.system.project%3Aissues-panel