Try fast search NHibernate

28 October 2009

NHibernate.Validator : Customizing messages (Message Interpolator)

The message interpolator is the responsible of the messages translation/composition. In the coming soon version (NHV-1.2.0) we had increase its power (and refactorized its implementation).

Has default NHV has two implementations: DefaultMessageInterpolatorAggregator and DefaultMessageInterpolator.

What is the usual you have seen in NHibernate’s eco-system ? Yes, you are right: Injectability!!!

So far the injectability in NHV is not so extreme as in NHibernate but we are closer…very closer ;)

If you only need to override some behavior you can inherit from DefaultMessageInterpolator. If you want implement a completely different way to create/translate/composite messages you can implements your own IMessageInterpolator.

In this post I will write about another case: the composition of your own behavior with the default behavior.

The needs

As you saw, in the previous post, we have solved the problem of magic-strings so and so…but was only for yesterday. Today I would manage all messages through my convention, and composite it only in the strings-resource-file, for all cases where possible.

The possible convention
  • The key for a class name will be : friendly.class.<TypeName> (ex.: friendly.class.Employee)
  • The key for a property name will be : friendly.property.<PropertyName> (ex: friendly.property.Salary) ; note: two properties with the same name should have the same meaning no matter which is the owner class (IMO).
  • The key for the message of a entity-validator (validate the instance) will be: validator.<TypeName>
  • The key for the message of a property-validator will be: validator.<TypeName>.<PropertyName>
  • The key for the message of a validator will be: validator.<ValidatorName>

For <ValidatorName> I mean the class name of the validator without the post-fix “Validator” (that is the convention used inside NHV).

There is a special case for the <ValidatorName> (as any good convention); using lambdas the implementation of the validator is ever the same so, in this case, the <TypeName> will be the type where the validation was specified.

Note: all reusable validators, using the Satisfier or not, should define its key.

Messages redefinition

In NHV the default message for the constraint NotNullNotEmpty is:

key: validator.notNullNotEmpty

value: may not be null or empty

what I would like is:

The [here the friendly name of the property] of the [here the friendly name of the entity] is mandatory.

The custom IMessageInterpolator

First I must define the syntax to use in my interpolator; there are three possible variables: [EntityName], [PropertyName], [PropertyValue]. In practice in my strings-resource-file I will have this:

CustomInterResource1

and in case I need a sub-property I would have something like this

CustomInterResource2

The implementation

public class ConventionMessageInterpolator : IMessageInterpolator
{
private const string EntityValidatorConvention = "{{validator.{0}}}";
private const string EntityPropertyValidatorConvention = "{{validator.{0}.{1}}}";
private const string EntityNameConvention = "{{friendly.class.{0}}}";
private const string PropertyNameConvention = "{{friendly.property.{0}}}";
private const string PropertyValueTagSubstitutor = "${{{0}{1}}}";
private static readonly int PropertyValueTagLength = "PropertyValue".Length;

private readonly Regex substitutions =
new Regex(@"\[EntityName\]|\[PropertyName\]|(\[PropertyValue([.][A-Za-z_][A-Za-z_0-9]*)*\])"
, RegexOptions.Compiled);

#region IMessageInterpolator Members

public string Interpolate(InterpolationInfo info)
{
string result = info.Message;
if(string.IsNullOrEmpty(result))
{
result = CreateDefaultMessage(info);
}
do
{
info.Message = Replace(result, info.Entity, info.PropertyName);
result = info.DefaultInterpolator.Interpolate(info);
}
while (!Equals(result, info.Message));
return result;
}

#endregion

public string
CreateDefaultMessage(InterpolationInfo info)
{
return string.IsNullOrEmpty(info.PropertyName) ?
string.Format(EntityValidatorConvention, GetEntityValidatorName(info))
:
string.Format(EntityPropertyValidatorConvention, info.Entity.Name, info.PropertyName);
}

private string GetEntityValidatorName(InterpolationInfo info)
{
var entityValidatorName = info.Entity.Name;
var validatorType = info.Validator.GetType();
if (validatorType.IsGenericType)
{
entityValidatorName = validatorType.GetGenericArguments().First().Name;
}
entityValidatorName = CleanValidatorPostfix(entityValidatorName);
return entityValidatorName;
}

private string CleanValidatorPostfix(string entityValidatorName)
{
var i = entityValidatorName.LastIndexOf("Validator");
return i > 0 ? entityValidatorName.Substring(0, i) : entityValidatorName;
}

public string Replace(string originalMessage, Type entity, string propName)
{
return substitutions.Replace(originalMessage, match =>
{
if ("[EntityName]".Equals(match.Value))
{
return string.Format(EntityNameConvention, entity.Name);
}
else if (!string.IsNullOrEmpty(propName) && "[PropertyName]".Equals(match.Value))
{
return string.Format(PropertyNameConvention, propName);
}
else if (!string.IsNullOrEmpty(propName) && match.Value.StartsWith("[PropertyValue"))
{
return string.Format(PropertyValueTagSubstitutor, propName,
match.Value.Trim('[', ']').Substring(PropertyValueTagLength));
}
return match.Value;
});
}
}

The configuration

To use both, my custom interpolator and my custom strings-resource-file the Loquacious configuration is:

var configure = new FluentConfiguration();
configure
.SetMessageInterpolator<ConventionMessageInterpolator>()
.SetCustomResourceManager("YourProd.Properties.ValidationMessagesConv", Assembly.Load("YourProd"))
.SetDefaultValidatorMode(ValidatorMode.UseExternal);

Results

Having a chunk of strings-resource-file as this

CustomInterResource3

I can write a clean definition like

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

public class EntreCallesValidation : ValidationDef<IEntreCalles>
{
public EntreCallesValidation()
{
ValidateInstance.By(IsValid);
}

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.");
}
}

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()
.And
.NotEmpty()
.And
.GapLessThanOrEqualTo(salaryGap)
.And
.Include(avgSalary);
}
}

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

Define(m => m.Lapse)
.IsValid()
.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("{validator.StandUpMeeting.Lapse.WorkingTime}")
.And
.GapLessThanOrEqualTo(meetingTime);
}
}

public class EmployeeValidation : ValidationDef<Employee>
{
public EmployeeValidation()
{
Define(e => e.Name).NotNullableAndNotEmpty()
.And
.LengthBetween(2, 30);

Define(e => e.Salary).GreaterThanOrEqualTo(1000m);
}
}

to have invalid messages as:

Must specify both streets or none.
No es un codigo postal Argentino.
The street of the address is mandatory.
The number must be greater than or equal to 1

The description of the employee position is mandatory.
The rage salary should include:4000

The meeting should happen in working time.
The duration of standup-meeting was too long. Expected less than :00:20:00

with all advantages a strings-resource-file, give me.

3 comments:

  1. Thank you for this post.

    One question regarding the resource files. How you suggest to switch 2 or 3 resource files. For example if you have EN,DE,IT languages that can be switched from the options menu in GUI, how can say to NHV simpy use EN resource file, now use the IT, switch back to EN?

    Each resource file will be translated for the specific language.

    ReplyDelete
  2. Is there a way to get through the api the final message of a validation? I would like to reuse the validation messages from NHV to show them when validating on client side.

    ReplyDelete
  3. If the validation message is static (without prop-name nor prop-value) you can do it but, if the validator message has some dynamic value you will need to implements the MessageInterpolator in Java script.

    ReplyDelete