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:
and in case I need a sub-property I would have something like this
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
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 1The description of the employee position is mandatory.
The rage salary should include:4000The 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.
Thank you for this post.
ReplyDeleteOne 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.
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.
ReplyDeleteIf 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