Model Metadata and Validation Localization using Conventions

By default, ASP.NET MVC leverages Data Annotations to provide validation. The approach is easy to get started with and allows the validation applied on the server to “float” to the client without any extra work.

However, once you get localization involved, using Data Annotations can really clutter your models. For example, the following is a simple model class with two properties.

public class Character {
  public string FirstName { get; set; }
  public string LastName { get; set; }
}

Nothing to write home about, but it is nice, clean, and simple.  To make it more useful, I’ll add validation and format how the properties are displayed.

public class Character {
  [Display(Name="First Name")]
  [Required]
  [StringLength(50)]]
  public string FirstName { get; set; }
  
  [Display(Name="Last Name")]
  [Required]
  [StringLength(50)]]
  public string LastName { get; set; }
}

That’s busier, but not horrible. It sure is awful Anglo-centric though. I’ll fix that by making sure the property labels and error messages are pulled from a resource file.

public class Character {
  [Display(Name="Character_FirstName", ResourceType=typeof(ClassLib1.Resources))]
  [Required(ErrorMessageResourceType=typeof(ClassLib1.Resources), 
    ErrorMessageResourceName="Character_FirstName_Required")]
  [StringLength(50, ErrorMessageResourceType = typeof(ClassLib1.Resources),
    ErrorMessageResourceName = "Character_FirstName_StringLength")]
  public string FirstName { get; set; }

  [Display(Name="Character_LastName", ResourceType=typeof(ClassLib1.Resources))]
  [Required(ErrorMessageResourceType=typeof(ClassLib1.Resources), 
    ErrorMessageResourceName="Character_LastName_Required")]
  [StringLength(50, ErrorMessageResourceType = typeof(ClassLib1.Resources),
    ErrorMessageResourceName = "Character_LastName_StringLength")]
  public string LastName { get; set; }
}

Wow! I don’t know about you, but I feel a little bit dirty typing all that in. Allow me a moment as I go wash up.

So what can I do to get rid of all that noise? Conventions to the rescue! By employing a simple set of conventions, I should be able to look up error messages in resource files as well as property labels without having to specify all that information. In fact, by convention I shouldn’t even need to use the DisplayAttribute.

I wrote a custom PROOF OF CONCEPT ModelMetadataProvider that supports this approach. More specifically, mine is derived from the DataAnnotationsModelMetadataProvider.

What Conventions Does It Apply?

The nice thing about this convention based model metadata provider is it allows you to specify as little or as much of the metadata you need and it fills in the rest.

Providing minimal metadata

For example, the following is a class with one simple property.

public class Character {
  [Required]
  [StringLength(50)]
  public string FirstName {get; set;}
}

When displayed as a label, the custom metadata provider looks up the resource key, {ClassName}_{PropertyName}, and uses the resource value as the label. For example, for the FirstName property, the provider uses the key Character_FirstName to look up the label in the resource file. I’ll cover how resource type is specified later.

If a value for that resource is not found, the code falls back to using the property name as the label, but splits it using Pascal/Camel casing as a guide. Therefore in this case, the label is “First Name”.

The error message for a validation attribute uses a resource key of {ClassName}_{PropertyName}_{AttributeName}. For example, to locate the error message for a RequiredAttribute, the provider finds the resource key Character_FirstName_Required.

Partial Metadata

There may be cases where you can provide some metadata, but not all of it. Ideally, the metadata that you don’t supply is inferred based on the conventions. Going back to previous example again:

public class Character {
  [Required(ErrorMessageResourceType=typeof(MyResources))]
  [StringLength(50, ErrorMessageResourceName="StringLength_Error")]
  [Display(Name="First Name")]
  public string FirstName {get; set;}
}

Notice that the first attribute only specifies the error message resource type. In this case, the specified resource type will override the default resource type. But the resource key is still inferred by convention (aka Character_FirstName_Required).

In contrast, notice that the second StringLengthAttribute, only specifies the resource name, and doesn’t specify a resource type. In this case, the specified resource name is used to look up the error message using the default resource type. As you might expect, if the ErrorMessage property is specified, that takes precedence over the conventions.

The DisplayAttribute works slightly differently. By default, the Name property is used as a resource key if a resource type is also specified. If no resource type is specified, the Name property is used directly. In the case of this convention based provider, an attempt to lookup a resource value using the Name property as a resource always occurs before falling back to the default behavior.

Configuration

One detail I haven’t covered yet is what resource type is used to find these messages? Is that determined by convention?

Deteriming this by convention would be tricky so it’s the one bit of information that must be explicitly specified when configuring the provider itself. The following code in Global.asax.cs shows how to configure this.

ModelMetadataProviders.Current = new ConventionalModelMetadataProvider(
  requireConventionAttribute: false,
  defaultResourceType: typeof(MyResources.Resource)
);

The model metadata provider’s constructor has two arguments used to configure it.

Some developers will want the conventions to apply to every model, while others will want to be explicit and have models opt in to this behavior. The first argument, requireConventionAttribute, determines whether the conventions only apply to classes with the MetadataConventionsAttribute applied.

The explicit folks will want to set this value to true so that only classes with the MetadataConventionsAttribute applied to them (or classes in an assembly where the attribute is applied to the assembly) will use these conventions.

The attribute can also be used to specify the resource type for resource strings.

The second property specifies the default resource type to use for resource strings. Note that this can be overridden by any attribute that specifies its own resource type.

Caveats, Issues, Potholes

This code is something I hacked together and there are a few issues to consider that I could not easily work around. First of all, the implementation has to mutate properties of attributes. In general, this is not a good thing to do because attributes tend to be global. If other code relies on the attributes having their original values, this could cause issues.

I think for most ASP.NET MVC applications (in fact most web applications period) this will not be an issue.

Another issue is that the conventions don’t work for implied validation. For example, if you have a property of a simple value type (such as int), the DataAnnotationsValidatorProvider supplies a RequiredValidator to validate the value. Since this validator didn’t come from an attribute, it won’t use my convention based lookup for its error messages.

I thought about making this work, but it the hooks I need to do this without a large amount of code don’t appear to be there. I’d have to write my own validator provider (as far as I can tell) or register my own validator adapters in place of the default ones. I wasn’t up to the task just yet.

Try it out

  • Live Demo: To see a demo of it in action, check out the live demo site.
  • NuGet Package: To try it in your application, install it using NuGet: Install-Package ModelMetadataExtensions
  • Source Code: The source code is up on BitBucket.

What others have said

Requesting Gravatar... Matt Jul 14, 2011 5:36 AM
# re: Model Metadata and Validation Localization using Conventions
The attribute noise problem is something I had a big problem with on a recent project when using EF Code First. Do you know of any way to enable something similar for EF validation? The documentation is quite light.

Thanks!
Requesting Gravatar... PadovaBoy Jul 14, 2011 7:26 AM
# re: Model Metadata and Validation Localization using Conventions
Really interesting!
I agree with the conventional way also in this kind of stuffs!
I personally implement a set of override methods for LabelFor/EditorFor to take localizated text by conventions without make a mess with attribute in the model.

I made in this way:
1) check if there is a localization for classname_propertyname
2) if not check for propertyname
bucause we can tell that FirstName is the same for Contact and Employer (et example)...

What about error messages with the point of view of the attribute?
Because i can have something like 'The field {0} is required' (by attribute: the common) but i can have something more personlized like 'The field 'bill' is required..if u want to be paied you should compile it' (by the field: the special).

So: the localization system should find by convention before by the field (for personalized message) and aftere by the attribute.

tnx for sharing!

Requesting Gravatar... Jonathan Jul 14, 2011 8:41 AM
# re: Model Metadata and Validation Localization using Conventions
Very cool Phil. Couple requests:

- It would be nice if it looked for "Required" in the resource file and just used it it when a more specific translation wasn't provided. As it stands now you can end up with a message that is partially translated "The prénom field is required"

-The Pascal/Camel casing feature you mentioned should probably not make changes to all uppercase words (ie. SSN, ID, etc).
Requesting Gravatar... alexidsa Jul 14, 2011 2:13 PM
# re: Model Metadata and Validation Localization using Conventions
In my opinion, Fluent Validation is much more convenient. Don't like attribute-driven approach
Requesting Gravatar... ZippyV Jul 14, 2011 4:37 PM
# re: Model Metadata and Validation Localization using Conventions
Good idea. Hope to see this fully implemented in MVC4.
Requesting Gravatar... Andrew Gunn Jul 14, 2011 4:55 PM
# re: Model Metadata and Validation Localization using Conventions
The language form on the demo redirects to the root of http://demo.haacked.com, instead of http://demo.haacked.com/ModelMetadataDemo.

Good stuff though!
Requesting Gravatar... Nick Schonning Jul 14, 2011 4:57 PM
# re: Model Metadata and Validation Localization using Conventions
What about using the namespace prepended to the property name to avoid conflicts. Something like {Namespace}_{ClassName}_{PropertyName}_{AttributeName}. This might get a little bit noisy if you have complex namespaces.
Requesting Gravatar... Filip Cornelissen Jul 14, 2011 6:04 PM
# re: Model Metadata and Validation Localization using Conventions
I still get English error messages @ demo.haacked.com/ModelMetadataDemo?culture=es
Requesting Gravatar... blowdart Jul 14, 2011 11:44 PM
# re: Model Metadata and Validation Localization using Conventions
Yea, I'm with having assembly/namespace optionally in there, especially as my models live in a separate assembly, along with their metadata. Keeping it within a single assembly doesn't appear flexible enough.
Requesting Gravatar... BlueCoder Jul 15, 2011 2:24 AM
# Live Demo is Buggy
Check the live demo Phil! It does't work right as @Andrew said...
Requesting Gravatar... John Teague Jul 15, 2011 6:18 AM
# re: Model Metadata and Validation Localization using Conventions
OR
You could just use FluentValidation which is far more appropriate for MVC apps than dataannotations anyway.
Requesting Gravatar... Matt Honeycutt Jul 15, 2011 10:02 AM
# re: Model Metadata and Validation Localization using Conventions
I like the idea behind this convention, but the implementation leaves a lot to be desired. That's because it's trying to layer behavior on top of the base model metadata provider using inheritance. I'd really like to see MVC 4 take a cue from Fubu and look at how to better support composition, period. With a model metadata provider implementation that actually enables composition and an IoC container, a convention to transform PascalCase to Pascal Case needs just a simple class with two lines of code. Here's the model metadata provider I implemented: github.com/.../SolidModelMetadataProvider.cs, and here's an example of using it to implement a simple convention: trycatchfail.com/....
Requesting Gravatar... haacked Jul 16, 2011 2:33 AM
# re: Model Metadata and Validation Localization using Conventions
If you're into the fluent validation approach, there's a package 'FluentValidation.MVC3'. I haven't used it much so I don't have a strong opinion on it yet.
Requesting Gravatar... Ignacio Fuentes Jul 20, 2011 6:19 AM
# re: Model Metadata and Validation Localization using Conventions
Hey Phil, are you sure this is working correctly?? I cant get for the life of me a single spanish error message
Requesting Gravatar... Paul Jul 21, 2011 7:53 AM
# re: Model Metadata and Validation Localization using Conventions
Nice post. I really like the fluent validation that is in the MVC Extensions project. http://nuget.org/List/Packages/MvcExtensions Allows you to configure validation in a fluent manner while using Resource files the same way you are used to. Blog Post: weblogs.asp.net/...

It would be nice to see something like this in MVC4. The MVC Extensions project also lets you register action filters to controllers and actions in a fluent manner!
Requesting Gravatar... Brett Jul 23, 2011 8:24 PM
# re: Model Metadata and Validation Localization using Conventions
Thanks for that Phil, very cool implementation!

Unless I've missed something in my setup I think there might be a small change needed to the ApplyConventionsToValidationAttributes method to get the validation messages showing, change this:


if (!resourceType.PropertyExists(resourceKey)) {
resourceKey = "Error_" + attributeShortName;
if (!resourceType.PropertyExists(resourceKey)) {
continue;
}

validationAttribute.ErrorMessageResourceType = resourceType;
validationAttribute.ErrorMessageResourceName = resourceKey;
}

to this

if (!resourceType.PropertyExists(resourceKey)) {
resourceKey = "Error_" + attributeShortName;
if (!resourceType.PropertyExists(resourceKey)) {
continue;
}
}

validationAttribute.ErrorMessageResourceType = resourceType;
validationAttribute.ErrorMessageResourceName = resourceKey;

Requesting Gravatar... Andrej Slivko Jul 26, 2011 12:51 AM
# re: Model Metadata and Validation Localization using Conventions
Is there any reasons why extension method
public static DisplayAttribute Copy(this DisplayAttribute attribute) doesn't copy all DisplayAttribute?
Like AutoGenerateField, AutoGenerateFilter, Prompt, Order?
Or it is just because this is not real thing but just prof of concept?
Requesting Gravatar... kvic Jul 28, 2011 4:07 PM
# re: Model Metadata and Validation Localization using Conventions
I have been using data annotations and found you can group them to help with the noise they generate <Required(),StringLength(50),Display(autogeneratefield:=False)>
Requesting Gravatar... Rui Jarimba Jul 28, 2011 11:00 PM
# re: Model Metadata and Validation Localization using Conventions
Hi Phil,

Is it possible to use a ModelMetadataProvider with ASP.NET Web Forms?
Requesting Gravatar... silverlight development Aug 02, 2011 6:04 PM
# re: Model Metadata and Validation Localization using Conventions
Thanks for that Phil, very cool implementation!
Requesting Gravatar... BZ Aug 05, 2011 3:11 AM
# re: Model Metadata and Validation Localization using Conventions
The validation errors don't work. They always show English when submitting.
Requesting Gravatar... Kristoffer Aug 06, 2011 10:30 PM
# re: Model Metadata and Validation Localization using Conventions
Great idea. Works perfectly after the fix by Brett.
Requesting Gravatar... NachoF Aug 07, 2011 9:34 PM
# re: Model Metadata and Validation Localization using Conventions
I dont see any spanish error message at all... even after Brett's fix.... :s
Requesting Gravatar... Seitensprung Aug 10, 2011 3:37 AM
# re: Model Metadata and Validation Localization using Conventions
"The validation errors don't work. They always show English when submitting."

Same problem here. :-( Anyway thanks for your work!

Greetings
M. Seitensprung
Requesting Gravatar... Netzwelt Aug 24, 2011 6:30 AM
# re: Model Metadata and Validation Localization using Conventions
Is there a solution for the problem yet?
Requesting Gravatar... Neroken Aug 30, 2011 3:55 PM
# re: Model Metadata and Validation Localization using Conventions
I found the problem.

The way the demo site is setup makes it that the validation errors don't work.

The CurrentCulture is set after datamodelbinder is invoked.

If you add the next method to the controller it will work.

protected override void ExecuteCore()
{
string culture = Request["culture"];

if (!string.IsNullOrEmpty(culture))
{
Thread.CurrentThread.CurrentCulture =
CultureInfo.CreateSpecificCulture(culture);
Thread.CurrentThread.CurrentUICulture =
CultureInfo.CreateSpecificCulture(culture);
}

base.ExecuteCore();
}
Requesting Gravatar... Mustafa GULMEZ Sep 10, 2011 12:16 PM
# re: Model Metadata and Validation Localization using Conventions
great post! but few questions.
my model like this:

public class Application
{
public int ApplicationId { get; set; }
public string ApplicationName { get; set; }
public DateTime ApplicationCreateDate { get; set; }
public DateTime? ApplicationEndDate { get; set; }
public int ApplicationPackageId { get; set; }
}

ApplicationCreateDate and ApplicationPackageId has not Required attribute. But client side validation says "The ApplicationPackageId field is required." how do i change this message by default? if i add Required attribute by manually i can change this message. But default??
Requesting Gravatar... Immobilien Nachrichten Sep 11, 2011 6:51 PM
# re: Model Metadata and Validation Localization using Conventions
Thanks Neroken, your modeling works!
Requesting Gravatar... haacked Sep 18, 2011 12:56 PM
# re: Model Metadata and Validation Localization using Conventions
Hi all, the reason the validation message wasn't showing up in spanish was that I didn't set the culture early enough. I updated the site and code.
Requesting Gravatar... nacho10f Sep 21, 2011 9:35 PM
# re: Model Metadata and Validation Localization using Conventions
Hey Phil, the change proposed by Brett here and mgulmez on bitbucket is necessary otherwise errors with set up keys with the format Property_Name_Required never get picked.. I have made a comment with more specifics on the bug report on bibucket.
Requesting Gravatar... Luciano Sep 28, 2011 7:20 AM
# re: Model Metadata and Validation Localization using Conventions
Would be nice to have a interface to a kind of KeyProvider when we could implement custom key formats. Instead of ConventionalModelMetadataProvider.GetResourceKey(),

Got the idea ?

I'm creating it now, because I need a custom key format, including Area name to the Model name.
Requesting Gravatar... Scott Xu Oct 07, 2011 8:26 PM
# re: Model Metadata and Validation Localization using Conventions
Thanks for that Phil.
Just like Brett said, there might be a small change needed to the ApplyConventionsToValidationAttributes method to get the validation messages showing, change this:

if (!resourceType.PropertyExists(resourceKey)) {
resourceKey = "Error_" + attributeShortName;
if (!resourceType.PropertyExists(resourceKey)) {
continue;
}

validationAttribute.ErrorMessageResourceType = resourceType;
validationAttribute.ErrorMessageResourceName = resourceKey;
}

to this

if (!resourceType.PropertyExists(resourceKey)) {
resourceKey = "Error_" + attributeShortName;
if (!resourceType.PropertyExists(resourceKey)) {
continue;
}
}

validationAttribute.ErrorMessageResourceType = resourceType;
validationAttribute.ErrorMessageResourceName = resourceKey;
Requesting Gravatar... Hardy Wang Nov 23, 2011 11:41 AM
# re: Model Metadata and Validation Localization using Conventions
How about nested models? How do I compose resource key?

For example

class A {
[Required]
public string PropertyA1 { get; set; }
}

class B {
public A PropertyB_A { get; set; }
}

What do you have to say?

(will show your gravatar)
Please add 2 and 3 and type the answer here: