A Review of Model Binding in ASP.NET Razor Pages

in

Introduction

Model binding in ASP.NET is a convenient way to convert data from an HTTP request into an object to be used in your application code. In this article, I will review the process of binding data using Razor pages. This review is focused on the model binding process of converting the request data to .NET types with the assumption that data has been successfully sent from the client. I am a big fan of Razor Pages, and the model binding system makes working with request data painless.

The three methods to bind data from a request to a page model.

  1. Page handler parameters
  2. BindPropertyAttribute
  3. BindPropertiesAttribute

I will review each method in order and provide examples. Let’s get started!

Method 1: Page handler parameters

The first approach is a good option if you need Request data to perform some logic within a given page handler and don’t need to set any properties on your class. Let’s look at an example.

POST Request

https://localhost:7269/Books/23
public class BookModel : PageModel
{
    public void OnPost(int id)
    {
        Console.WriteLine(id); // 23
    }
}

We get the id as a page handler parameter in the preceding code. The id route parameter gets converted to an int during the modeling binding process. Now that we have access to the id in the OnPost handler, we can use it to write any logic in the handler’s scope. It’s worth noting that we would get the same result if the id was passed as a query string parameter.

POST Request URL

https://localhost:7269/Books?id=23

Or even as form data:

POST Request Body

{
    "id": "23"
}

I will cover more about binding sources and how the model binder chooses which object to bind to later. I will also share how you can override the default behavior.

Let’s move on to another way to declare which objects should be included in the model binding process.

Method 2: BindPropertyAttribute

What if we want to set the id as a property on the PageModel class? One option would be to create an id property and then manually set the id property using the id page handler parameter like so:

public class IndexModel : PageModel
{
    public string Id { get; set; }

    public void OnPost(int id)
    {
        Id = id;
    }
}

This works, but there is a better way to update the id property that will be less verbose, especially if we have many page handler parameters. We can use the BindProperty attribute to rewrite the preceding code.

public class IndexModel : PageModel
{
    [BindProperty]
    public string Id { get; set; }

    public void OnPost()
    {
    }
}

So far, I’ve shown you two different ways to bind day to a PageModel. We can use multiple methods of binding our data. For example, we can bind some data via page handler parameters and some data with the BindProperty attribute. Suppose we send the following post request:

Request URL

https://localhost:7269/23

Request Body

{
  "Email" : "will@example.com",
  "Message": "hello world"
}

I placed a breakpoint set at the beginning of the scope of the page handler so that we could observe the values of the properties.

Using Visual Studio debugger, we can observe that the id has a value of 23, and the Email and Message properties have been updated with the values from the request body.

But What about GET requests?

You can access bound data as page handler parameters on GET requests.

public class SearchModel : PageModel
{
    public void OnGet(string SearchTerm)
    {
        // Do something with SearchTerm
    }
}

You can use the BindPropertyAttribute to bind data on GET requests, but you must opt into this behavior for security purposes.

The code looks very similar to the POST request, with the only difference being the SupportsGet=true parameter.

public class SearchModel : PageModel
{
    [BindProperty(SupportsGet=true)]
    public string SearchTerm { get; set; }

    public void OnGet()
    {
        // Do something with SearchTerm
    }
}

Method 3: BindPropertiesAttribute

This property enables binding for all properties on the page model. 👀

[BindProperties]
public class CompanyModel : PageModel
{
    public int UserId { get; set; }
    public int GroupId { get; set; }
    public int CompanyId { get; set; }

    public void OnPost()
    {
    // Do something with the UserId, GroupId and CompanyId
    }
}

I have never used this method, but it’s good to know this feature is available if needed. I prefer the page handler parameter or BindPropertyAttribute as it offers more control. What are your thoughts on this approach? Let me know in the comments.

Model Binding Priority

I have demonstrated how model binding works with Razor pages, considering there are three ways to bind data from a request. It is essential to understand the order of precedence for model binding.

Here are the three ways data will be bound to the model in order, with form values taking the highest priority.

  1. Form values
  2. Route values
  3. Query parameters

Consider the following example POST request.

Form Body

{
    "firstname": "Will",
    "lastname": "Pickeral"
}

URL

https://www.mywebsite/Sam?lastname=williams

Page Handler

public class IndexModel : PageModel
{
    [BindProperty]
    public string FirstName { get; set; }

    [BindProperty]
    public string LastName { get; set; }

    public void OnPost()
    {
    }
}

What is the value of firstname and lastname after a successful request to the OnPost handler? This is not a trick question. If you said firstname=Will and lastname=Pickeral, you are correct!. Not because my name is actually Will Pickeral, but because Form values take priority over route parameters, and route parameters take priority over query parameters in the model bounding process.

Binding Complex Types

So far, I have only mentioned model binding with simple types. Most of the time, I need to bind to a property with a complex type such as with the example below.

public class IndexModel : PageModel
{
    [BindProperty]
    public Person Person { get; set; }

    public void OnPost()
    {
    }

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

The model binder will set the FirstName and LastName properties on an instance of the Person class and assign the object to the Person property on the IndexModel. We can now use the Person property to perform any logic in our class.

I typically will use the BindPropertyAttribute and follow the pattern described by Andrew Lock in is book ASP.NET in Action, in which all binding properties live in an InputModel nested class and then only bind to the InputModel. I have found this pattern to be very clean and fun to work with.

Here is an example:

public class IndexModel : PageModel
{
    [BindProperty]
    public InputModel Input { get; set; }

    public class InputModel
    {
        public int GroupId {get; set;}
        public Person Person {get; set;}
    }
}

A Few Things To Keep In Mind

You cannot decorate a property with BindProperty on the Input property and add BindProperty(SupportsGet=True) to a property on the InputModel.

public class IndexModel : PageModel
{
    [BindProperty]
    public InputModel Input { get; set; }

    public void OnGet()
    {
    }

    public void OnPost()
    {
    }

    public class InputModel
    {
        [BindProperty(SupportsGet = true)] //  This won't work!
        public string CompanyName { get; set; } = string.Empty;
        public string Email { get; set; } = string.Empty;
        public string Message { get; set; } = string.Empty;
    }
}

Remember that if you follow the InputModel pattern, you do not include any properties you want to bind on a GET request. Or you if find yourself wondering why your data won’t bind, make sure you are not making this mistake. It happened to me, which is why I am sharing this tip with you.

You can move those properties to the PageModel class alongside your Input property, and it will bind on the GET request as expected.

public class ContactModel : PageModel
{
    [BindProperty]
    public InputModel Input { get; set; }

    [BindProperty(SupportsGet = true)]
    public string CompanyName { get; set; } = string.Empty;

    public void OnGet()
    {
    }

    public void OnPost()
    {
        // Handle the post request
    }

    public class InputModel
    {
        public string Email { get; set; } = string.Empty;
        public string Message { get; set; } = string.Empty;
    }
}

In the preceding code, we decorate the CompanyName property with BindProperty(SupportsGet=true) within the InputModel class, which is already decorated with the BindProperty attribute.

The name attribute

When submitting a form, the model binder looks at the name attribute value on an HTML input element to determine which object to bind the data to. If you use the asp-for attribute, by default the name will be set to the property name, which works because the model binder will successfully bind to that value.

In some cases, you will need to override the name value. The model binder will not bind the value to a property on the class unless you also change the name of your property to match the name attribute of the input element. The value is not case sensitive, so a property name FirstName will bind to an input element value with a name value of firstname.

Consider we had the following form for the Contact page.

<form method="post">
    <label asp-for="Input.Email"></label>
    <input asp-for="Input.Email"/>
    <label asp-for="Input.Message"></label>
    <textarea  asp-for="Input.Message"></textarea>
    <button type="submit">Send</button>
</form>

Which would render in the browser as:

<form method="post">
        <label for="Input_Email">Email</label>
        <input type="email" data-val="true" data-val-email="The Email field is not a valid e-mail address." data-val-required="The Email field is required." id="Input_Email" name="Input.Email" value="">
        <label for="Input_Message">Message</label>
        <textarea data-val="true" data-val-length="The field Message must be a string with a maximum length of 250." data-val-length-max="250" data-val-required="The Message field is required." id="Input_Message" maxlength="250" name="Input.Message"></textarea>
        <button type="submit">Send</button>
    <input name="__RequestVerificationToken" type="hidden" value="CfDJ8Juq-NTioThIpu9Oc6nYyg5trLHAUvLf9PM1ht7dyb-4ZDqIO-qc8E4bMhvdU7GrMeu7PoupOQNvxBoV8L2yiEqwbQAPPlwX2UFioCfXNIT8faUm5C_EfcrG_dIq_ksJhZWiv9E6oNSbYGEVc8pXxYM">
</form>

The Input.Email and Input.Message properties will bind as expected because the form name values’ matches the binding object’s property names.

Let’s manually set the name attribute on the email input element in our Razor page like so,

<input name="emailAddress" asp-for="Input.EmailAddress"/>

We override the default name value. The model binder would not find a property on the binding object that matches the name emailAddress, so this value would not bind.

Keep this in mind when you have to set the name value manually for specific use cases, such as when working with radio buttons.

Additional Binding Attributes

The framework does provide additional Binding Attributes not covered in this article to provide more control over the binding source. Here are a few examples.

  • [FromQuery] - Gets values from the query string.
  • [FromRoute] - Gets values from route data.
  • [FromForm] - Gets values from posted form fields.
  • [FromBody] - Gets values from the request body.
  • [FromHeader] - Gets values from HTTP headers.

Model Validation

Although this article did not cover model validation, it is essential to remember to validate all user input. Razor Pages makes this process easy by allowing the developer to declaratively set model validation on the input model. The validation results will be available after the model binding process before the page handler is executed. Learn more about validation in Razor pages.

Conclusion

This article reviewed the three ways to set the model binding in Razor pages: page handler parameters, the BindPropertyAttribute, and the BindPropertiesAttribute. I focused some of the ways I have worked with model binding in ASP.NET Razor pages and shared some lessons learned. Check out the official Microsoft documentation to learn more about Model Binding In ASP.NET. Thanks for reading!