Introducing Fluent MetadataProvider for ASP.NET MVC

I have just included a Fluent Metadata provider in my open source System.Web.Mvc.Extensibility project. Currently it contains all of the features of the Built-in DataAnnotations Metadata provider that comes with the ASP.NET MVC 2 framework. Consider it as a holiday special from me for the ASP.NET MVC community :-).

The main reason I am not fond of the DataAnnotations provider is, it is completely Attribute based, In my opinion, attributes adds extra noise to the code which I always tried to avoid, I love pure POCO. Beside the noise, the most important drawback of using attribute is that there is no conventional way to inject your dependencies into your attribute by your preferred IoC container and DataAnnotations is no exception in this case. Another issue that is also true for the attribute based model is that you are always free to decorate your code with invalid attributes and there is no compile type checking, For example, in this case nothing going to stop you to add StringLength or data type DateTime attribute to a integer based property.

These are the main driving force to come up with my Fluent Medata provider.

Now, lets take a quick look how to define the metadata for the model with the fluent metadata provider, but, first lets see the DataAnnotations attributed based model of the sample application:

public class ProductDisplayModel
{
    [ScaffoldColumn(false)]
    public int Id { get; set; }

    public string Name { get; set; }

    public string CategoryName { get; set; }

    public string SupplierName { get; set; }

    [DataType(DataType.Currency)]
    public decimal Price { get; set; }
}

public class ProductEditModel
{
    [ScaffoldColumn(false)]
    public int Id { get; set; }

    [Required(ErrorMessage = "Name cannot be blank.")]
    [StringLength(64, ErrorMessage = "Name cannot be more than 64 characters.")]
    public string Name { get; set; }

    [DisplayName("Category")]
    [Required(ErrorMessage = "Category must be selected.")]
    public Category Category { get; set; }

    [DisplayName("Supplier")]
    [Required(ErrorMessage = "Supplier must be selected.")]
    public Supplier Supplier { get; set; }

    [Required(ErrorMessage = "Price cannot be blank.")]
    [Range(10, 1000, ErrorMessage = "Price must be between 10.00-1000.00.")]
    [DataType(DataType.Currency)]
    public decimal Price { get; set; }

    [ScaffoldColumn(false)]
    public SelectList Categories { get; set; }

    [ScaffoldColumn(false)]
    public SelectList Suppliers { get; set; }
}

For the fluent version, you first have to create a class which inherits from the BootstrapperTaskBase so that the System.Web.Mvc.Extensibility framework can automatically register it when the application starts, the following is the complete code which would work exactly same as the above:

public class RegisterModelMetadata : BootstrapperTaskBase
{
    protected override void ExecuteCore(IServiceLocator serviceLocator)
    {
        IModelMetadataRegistry registry = serviceLocator.GetInstance<IModelMetadataRegistry>();

        registry.Register<ProductDisplayModel>(configurator =>
                                               {
                                                   configurator.Configure(model => model.Id).Hide();
                                                   configurator.Configure(model => model.Price).AsCurrency();
                                               })
                .Register<ProductEditModel>(configurator =>
                                               {
                                                   configurator.Configure(model => model.Id).Hide();
                                                   configurator.Configure(model => model.Name).Required("Name cannot be blank.").MaximumLength(64, "Name cannot be more than 64 characters.");
                                                   configurator.Configure(model => model.Category).DisplayName("Category").Required("Category must be selected.");
                                                   configurator.Configure(model => model.Supplier).DisplayName("Supplier").Required("Supplier must be selected.");
                                                   configurator.Configure(model => model.Price).AsCurrency().Required("Price cannot be blank.").Range(10, 1000, "Price must be between 10.00-1000.00.");
                                                   configurator.Configure(model => model.Categories).Hide();
                                                   configurator.Configure(model => model.Suppliers).Hide();
                                               });

        ModelMetadataProviders.Current = new ExtendedModelMetadataProvider(registry);
    }
}

You can find the complete example in the sample folder.

When configuring the  model you will find that it gives you the options that are only applicable for that data type, for example, you cannot apply string length constraint to an integer property, currency is only valid for the decimal type etc etc. It also has a nice extensibility model to add more option which I will cover in the future blog post.

What do you think? Comments are suggestions are greatly appreciated.

Download: github

Shout it

11 Comments

  • Great! Thank you.

    Some thoughts: what are you think about common syntax like this (abstract):

    configurator.Configure(model => model.SomeModelParam)
    .Annotations(new {
    Required = true,
    MaxLenght = 64,
    Hidden = false,
    AsCurrency = true,
    DisplayName = "DisplayName"
    }).Messages(new {
    OnRequiredError = "Can't be blank",
    OnOverLenghtError = "String is too large"
    })

    it's much clear and simplier? isn't it?

  • I dig it, in terms of having some nice compile-time checking and testability. I just don't know that I'd describe it as less noisy. I mean, to me all of this fluent stuff is just a different kind of noise. :)

  • I don't know. This seems like unnecessary complexity. I would rather bear some attributes. Worst thing is - view model gets detached with metadata - developer will be forced to check out 2 places all the time. That's just a view model after all.

  • @Vladimir Yunev: It seems the code that you have pasted works with anonymous object which is not strongly typed, also it does not restrict to apply irrelevant meta data which is very much same as the attribute based model.

    Did you get my point.

  • @Jeff:

    "I dig it, in terms of having some nice compile-time checking and testability. "

    Don't you think the above two reasons are quite good enough?

    As mentioned in my post the most important thing in attribute based model is that there is no way to inject the dependencies with an IoC Container.

  • @Arnis L: Pls check my reply to Jeff, No ViewModel might not be as simple as you are thinking, Think of a Custom Valaidator which works on an external service that you would like to plugg in your validation.

  • @kazimanzurrashid yes, thanks

  • i like the idea =) however, what if i wanted to get some of the requirements from a database? for example, say that i want to restrict the "price" attribute to be in between the lowest and highest price currently in my database. how often would the database be queried? every time an object is to be validated, or just at application start/configuration? also, is it possible to localize the messages?

  • Thinking of fluent API for this is great, but, did you have a look at fluent NHibernate approach of doing this? It's very decent I'd say.

    Simulating this in current context would be something like:

    public class ProductEditRegister: Register
    {
    public ProductEditRegister()
    {
    Hide(model => model.Id);
    Register(model => model.Price).AsCurrency();
    }
    }

    Of course methods like "Hide()" and "Register()" are in the base Register class and call the configurator through dependency injection or whatever.

    Later the Registers (Mappings in Fluent NHibernate terms) can be added similar to:

    registery,FluentRegisterations.AddFromAssemblyOf()

    Of course "AddFromAssemblyOf" can be one of number of other ways to get assembly containing the registrations classes (by path or anything else, or maybe at all not depend on assembly.

  • I like the fluent config, but I would move the actual code into the ViewModel (in a Configure method) and simply call that method from the ExecuteCore method. That way the code stays with the ViewModel, but the bootstrapping is still automatic.

  • I got somewhat mixed feelings.
    The benefits of compile time checking, injection and testing are compelling.
    On the other hand though I have to admit I find the attributes a lot less noisy then the fluent syntax.

    It is good to see this discussion alive as I too don't feel extremely comfortable with the attribute solution.

Comments have been disabled for this content.