A friend of mine has been employed to write code in the new Microsoft cross-platform ASP.Net Mvc Core and one day he asked be about the difference between the [Required] and the [BindRequired] attributes. Not being an expert in ASP.Net MVC and even less in the Core version, I had no idea so I went exploring.

Fast and dirty

I created a small project and ran it several times to see what the attributes were doing. RequiredAttribute seemed to work quite straightforward: decorate a property of an object with it and, when used as a parameter in an MVC API controller method, that property needs to be set. If it is not, the Controller ModelState.IsValid value will be set to false. Something like this:
public IActionResult Test([FromBody]TestModel model)
TestModel is a simple POCO with Required and BindRequired attributes set on its properties:
public class TestModel
{
[Required]
public string A { get; set; }
[BindRequired]
public string B { get; set; }
}
Now, if I send an object like this
{ A:'Something',B:'Something else' }
I get a true ModelState.IsValid value. If I send only property A, I also get a valid model. If I send only B or an empty object, IsValid will be false. So [Required] works as expected, [BindRequired] doesn't seem to do anything.

Searching through the methods of the controller I noticed one in particular called TryUpdateModelAsync. It tries to populate a model, given as a parameter, with values from the URL or form parameters for the action method. That means if you call the API with something like /api/test/test&A=Something&B=SomethingElse then run
var valid = TryUpdateModelAsync(model).Result;
, valid will be true. However, if you fail to specify a B parameter, valid will be false. Note that the properties of the model change either way. Property A will be filled with whatever you send it, only the result of the attempt will be false.

My friend was not completely satisfied with what I discovered, but it was what I could do in a few minutes of work.

More work

Frankly, I wasn't satisfied either. The only mention I could find of this BindRequired attribute was in the ASP.Net Core documentation, thrown in a list together with [FromBody], which applied to parameters, not object properties, and in unit tests code. So I started digging.

I went to the GitHub repository for aspnet/Mvc and downloaded the source. I then looked for a class named BindRequiredAttribute. It is a very simple class that inherits from BindingBehaviorAttribute and sends BindingBehavior.Required as the base constructor parameter. Its description is "Indicates that a property is required for model binding. When applied to a property, the model binding system requires a value for that property. When applied to a type, the model binding system requires values for all properties of that type." This doesn't say much. So I started to go through the chain of extension methods, interfaces, dependency injection. It wasn't pretty. Let's just say that after a few pages of blog post where I described wandering through classes and properties and trying everything - you wouldn't have understood anything because I didn't - I've decided to delete everything and search the web for more information.

In the end, experimenting with the project is what made it clear(er) for me. In the example I created, the Test method received an object [FromBody], but if I removed that attribute, the content of the call to the API was ignored. Instead, the values from the URL or form POST would be used to fill the object - this explains why FromBody and BindRequired were grouped in the same list. So for the same code without [FromBody]:
public IActionResult Test(TestModel model)
and now the request /api/test/test&A=Something&B=SomethingElse fills the object without the need for content (and ignoring it if it exists). If I remove either A or B, ModelState.IsValid becomes false.

What does it mean?


To me it feels as the [Required] attribute is all that I need since it works in both cases: form values and content, however besides [BindRequired], there is also [BindNever], which removes that property from binding. Imagine you would have a property like "IsAdmin" and you would set it to true programatically - you don't want it to be bound from the URL of the call. I was expecting that attempting to set a property decorated with BindNever would invalidate the model state, but it doesn't work like that, it just ignores the parameter. The System.ComponentModel.DataAnnotations namespace doesn't have an equivalent to BindNeverAttribute.

There is also the issue of the many many many points where the behavior of MVC can be changed and customized. There are a lot of out of the box classes that come with ASP.Net MVC; it only makes sense to use Attribute classes from the same package.

What about the inconsistency? Why does a [BindRequired] attribute not matter when populating a model from the body of the request? Frankly, I don't know. I believe it is because in that case the entire model class has been "bound", its individual properties being "populated" instead.

Hope that helps out a little.

Comments

Derek Broughton

And, in fact, you had put me on entirely the right track. So, my model has "Field" ([BindRequired]) & "CompositeField" ([Required]). My form has only CompositeField. My controller action method has: public async Task<iactionresult> Create([Bind("CompositeField,Field")] ModelClass model){ model.Field = model.CompositeField.Split('-')[0]; await TryUpdateModelAsync(model); } My only error had been not including "Field" in the Bind list in the method invocation. If I don't set model.Field, ModelState.IsValid is true. If I never call TryUpdateModelAsync(), ModelState.IsValid is true. If I set model.Field and call TryUpdateModelAsync(), ModelState.IsValid is true. ModelState.IsValid is only false when model.Field is empty, it's listed in the Bind variables, and I call TryUpdateModelAsync() Now, so far, I've only tested that my field is required, not whether if I put something invalid in there the server side validation works.

Derek Broughton

Derek Broughton

Your blog had me thinking I had the answer to my problem. I have a field that is derived from other form data, so if it's [Required], I get an error. If it's [BindRequired], there's no error at the point where I enter my Action method, but there will be (even if I assign a value to the field in the Model) if I execute TryUpdateModelAsync(). If it has neither [Required] not [BindRequired], ModelState.IsValid remains true. So, BindRequired definitely affects models updated via TryUpdateModelAsync(), but unfortunately not in the way I'd hoped... (or at least, not as simply).

Derek Broughton

Post a comment