When posting a form the normal ModelBinder in ASP.Net MVC enforces required fields with the RequiredAttribute from the System.ComponentModel.DataAnnotations Namespace.

But when you forget or a hacker deliberalty removes a required field from the form and submits the form, the Required Field is not checked by the DefaultModelBinder.

This is called Underposting and is described in Brad Wilsons Blog

[Required] is not a safety feature so you will have to repeat the required logic elsewhere, I think this is not DRY and want to present a possible solution.

Hope to hear from others if I missed something, but it seems to do the trick.

Creating new ModelMetadata for our custom ModelBinder

We are going to create a custom ModelBinder that does enforce [Required] attribute to be really required.

But the Model Binder needs more info about the model to do this so:

Step 1 is to create more modelmetadata for our model.

There already is a IsRequired property in the default metadata but we can’t use it, because all non nullable types are IsRequired == true, I want just the properties with [Required] to be mandatory in a form post.

Here is an excelent post on how to make your own ModelMetadataProvider and what you could do with it.

We are going to create our own for the underposting scenario.

We need to override the CreateMetadata method of the DataAnnotationsModelMetadataProvider class

public class CustomDataAnnotationsModelMetadataProvider : DataAnnotationsModelMetadataProvider 
{
 
    protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes,
        Type containerType, 
        Func<object> modelAccessor, 
        Type modelType, 
        string propertyName)
    {
        var data = base.CreateMetadata(
                            attributes,
                            containerType,
                            modelAccessor,
                            modelType,
                            propertyName);
 
 
        var required = attributes.FirstOrDefault(a => typeof(RequiredAttribute) == a.GetType());
        if (required != null)
        {
            data.AdditionalValues.Add("mustPost", true);
        }
        return data;
    }
}

 

This one is pretty straight forward. We check for the RequiredAttribute and add a AdditionalValue (thanks for that one MVC Team) to the metadata.

You could check for other rules and attributes I you are using for example EntLib or a different validation system.

To make MVC use you new metadata provider register it in the Application_Start() of your Global.asax with the following line:

ModelMetadataProviders.Current = new CustomDataAnnotationsModelMetadataProvider();

 

Creating a custom ModelBinder to check for Underposting

Step 2 is to make a new ModelBinder to use your new metadata.

public class NoMoreUnderPostingModelBinder : DefaultModelBinder
{
 
    protected override void OnModelUpdated(ControllerContext controllerContext,
        ModelBindingContext bindingContext)
    {
        if (bindingContext.ModelMetadata.IsComplexType)
        {
            foreach (var item in bindingContext.ModelMetadata.Properties)
            {
                object dummy;
                if (item.AdditionalValues.TryGetValue("mustPost", out dummy))
                {
                    string name = CreatePropertyName(bindingContext.ModelName, item.PropertyName);
                    var value = bindingContext.ValueProvider.GetValue(name);
                    if (value == null && bindingContext.PropertyFilter(name))
                    {
                        string message = "Warning! value not posted: " + (item.DisplayName ?? item.PropertyName);
                        bindingContext.ModelState.AddModelError(name, message);
                    }
                }
            }
        }
    }
        
    private static string CreatePropertyName(string prefix, string propertyName)
    {
        if (string.IsNullOrEmpty(prefix))
        {
            return propertyName;
        }
        if (string.IsNullOrEmpty(propertyName))
        {
            return prefix;
        }
        return (prefix + "." + propertyName);
    }    
}

 

I override the OnModelUpdated method.

For the model currently being bound I check all Properties for a “mustPost” AdditionalValue which I added in the metadataprovider.

If we find one  we have a really Required property.

We get the name of the property including prefix and then ask the current valueProvider to check for this name,

If we don’t find it, it is not posted and we add an error to the ModelState.

 

We need to register this ModelBinder with MVC in the Application_Start() of your Global.asax with the following line:

ModelBinders.Binders.DefaultBinder = new NoMoreUnderPostingModelBinder();

Making it Work

Next we create a form and controller actions to submit a new StockItem with the following Model class:

public class StockItem 
{
    [Required]
    public string Name { get; set; }
 
    [Required]
    public int  StockLevel { get; set; }
 
    public string Description { get; set; }
 
    public DateTime Created { get; set; }
}


Submitting an empty form produces:

image

You see that Created is also required but has no [Required] attribute. A non nullable type is always required.

My next step is to use Firefox + Firebug (or equivalent) to remove the StockLevel and Created <input> elements from the html.

Right Click on the StockLevel textbox:

image

and click Inspect Element.

Firebug shows the html code, right click on the <input> and click Delete Element

Repeat this for the Created textbox.

If we now submit the form we get:

image

As you can see Created is not checked for underposting and gets the default value for the DateTime Type.

Stocklevel is checked because of the [Required] attribute and can’t be ommited by nasty hackers.

Partial Updates

What if you need to support the scenario where a user may to edit only parts of a Model.

For example a content editor can edit the description but not the Stocklevel of a StockItem.

Stocklevel is required but is not on the edit form of a content editor.

You could solve this with a viewspecific model but don’t have to:

Asp.net MVC had a [Bind] attribute with Include and Exclude properties.

So you could create an action method like this.

[HttpPost]
public ActionResult Create([Bind(Exclude = "StockLevel")] StockItem comment)

If you again look at my ModelBinder you will see that I not only check if the value is null (field not posted) but also if the property has been excluded/included by the [Bind] attribute

 if (value == null && bindingContext.PropertyFilter(name))

If I again delete the Stocklevel textbox with Firebug and repost my form with the new [Bind] attribute in place I get:

image

As you can see no errors, Stocklevel is binded to its default value even if it is [Required] (but excluded)

That’s it. Hope to gets some feedback before I do this in production. In real live I could use some more Localization support.

 

Here is the source code (compressed with 7zip ):

Solution vs2010, mvc3