Model Metadata and Validation Localization using Conventions

asp.net, code, asp.net mvc 0 comments suggest edit

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?

Determining 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

  • NuGet Package: To try it in your application, install it using NuGet: Install-Package ModelMetadataExtensions
  • Source Code:The source code is up on GitHub.
Found a typo or error? Suggest an edit! If accepted, your contribution is listed automatically here.

Comments

avatar

39 responses

  1. Avatar for Matt
    Matt July 14th, 2011

    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!

  2. Avatar for PadovaBoy
    PadovaBoy July 14th, 2011

    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!

  3. Avatar for Jonathan
    Jonathan July 14th, 2011

    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).

  4. Avatar for alexidsa
    alexidsa July 14th, 2011

    In my opinion, Fluent Validation is much more convenient. Don't like attribute-driven approach

  5. Avatar for ZippyV
    ZippyV July 14th, 2011

    Good idea. Hope to see this fully implemented in MVC4.

  6. Avatar for Andrew Gunn
    Andrew Gunn July 14th, 2011

    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!

  7. Avatar for Nick Schonning
    Nick Schonning July 14th, 2011

    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.

  8. Avatar for Filip Cornelissen
    Filip Cornelissen July 14th, 2011

    I still get English error messages @ demo.haacked.com/ModelMetadataDemo?culture=es

  9. Avatar for blowdart
    blowdart July 14th, 2011

    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.

  10. Avatar for BlueCoder
    BlueCoder July 14th, 2011

    Check the live demo Phil! It does't work right as @Andrew said...

  11. Avatar for John Teague
    John Teague July 15th, 2011

    OR
    You could just use FluentValidation which is far more appropriate for MVC apps than dataannotations anyway.

  12. Avatar for Matt Honeycutt
    Matt Honeycutt July 15th, 2011

    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/....

  13. Avatar for haacked
    haacked July 15th, 2011

    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.

  14. Avatar for Ignacio Fuentes
    Ignacio Fuentes July 20th, 2011

    Hey Phil, are you sure this is working correctly?? I cant get for the life of me a single spanish error message

  15. Avatar for Paul
    Paul July 21st, 2011

    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!

  16. Avatar for Brett
    Brett July 23rd, 2011

    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;

  17. Avatar for Andrej Slivko
    Andrej Slivko July 25th, 2011

    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?

  18. Avatar for kvic
    kvic July 28th, 2011

    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)>

  19. Avatar for Rui Jarimba
    Rui Jarimba July 28th, 2011

    Hi Phil,
    Is it possible to use a ModelMetadataProvider with ASP.NET Web Forms?

  20. Avatar for silverlight development
    silverlight development August 2nd, 2011

    Thanks for that Phil, very cool implementation!

  21. Avatar for BZ
    BZ August 5th, 2011

    The validation errors don't work. They always show English when submitting.

  22. Avatar for Kristoffer
    Kristoffer August 6th, 2011

    Great idea. Works perfectly after the fix by Brett.

  23. Avatar for NachoF
    NachoF August 7th, 2011

    I dont see any spanish error message at all... even after Brett's fix.... :s

  24. Avatar for Seitensprung
    Seitensprung August 10th, 2011

    "The validation errors don't work. They always show English when submitting."
    Same problem here. :-( Anyway thanks for your work!
    Greetings
    M. Seitensprung

  25. Avatar for Netzwelt
    Netzwelt August 24th, 2011

    Is there a solution for the problem yet?

  26. Avatar for Neroken
    Neroken August 30th, 2011

    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();
    }

  27. Avatar for Mustafa GULMEZ
    Mustafa GULMEZ September 10th, 2011

    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??

  28. Avatar for Immobilien Nachrichten
    Immobilien Nachrichten September 11th, 2011

    Thanks Neroken, your modeling works!

  29. Avatar for haacked
    haacked September 18th, 2011

    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.

  30. Avatar for nacho10f
    nacho10f September 21st, 2011

    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.

  31. Avatar for Luciano
    Luciano September 28th, 2011

    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.

  32. Avatar for Scott Xu
    Scott Xu October 7th, 2011

    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;

  33. Avatar for Hardy Wang
    Hardy Wang November 23rd, 2011

    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; }
    }

  34. Avatar for Zeitarbeit Neuss
    Zeitarbeit Neuss December 21st, 2012

    "Would be nice to have a interface to a kind of KeyProvider when we could implement custom key formats."
    Interessting idea! How far have you got with your work?

  35. Avatar for Giorgi
    Giorgi December 21st, 2012

    You should note in your article that these settings are important for this solution to work - properties for .resx file:
    Build Action - Embedded Resource
    Custom Tool - PublicResXFileCodeGenerator
    Thanks

  36. Avatar for joey
    joey January 15th, 2013

    it allows you to specify as little or as much of the metadata you need and it fills in the rest.
    IR2110

  37. Avatar for tek
    tek April 11th, 2013

    Cool stuff, but does not build in .NET 4.0, I really need to use it but my project is 4.0. And if I force a build ( pass an additional param of NULL) it only works for the Display attribute and not for any Error Messages or Prompts..... A bit Urgent so any help is appreciated.

  38. Avatar for haacked
    haacked April 12th, 2013

    Hi Tek,

    If you don't mind, please log an issue at the GitHub repository. That's the appropriate place for bug reports.

    https://github.com/Haacked/...

    Phil

  39. Avatar for sami
    sami June 19th, 2014

    Hi Haacked,

    I have a question about Model Validation & metadata with Resource messages

    Can we send for example name of the field to our Resource Bundle message in Model validation?

    [Required(ErrorMessageResourceType = typeof(Resources.Resources), ErrorMessageResourceName = "Required",.......name-of-the-field-to-show-in-error-message......)]