Model Binding in ASP.NET Core: Handling User Input and Validation

Understanding Models in Razor Pages and MVC

MVC is centered on the principle of separation of concerns. The idea is that by isolating each aspect of an application to focus on a single responsibility, we can reduce dependencies within the system. This separation makes it easier to make changes without affecting other parts of the application.

The classic MVC design pattern consists of three distinct components:

  • Controller - Invokes methods on models and selects a view
  • View - Displays the data representation that constitutes the model
  • Model - The data to be displayed and methods to update itself

In this representation, there's only one model—the application model—which represents all business logic and how to update and modify its internal state. ASP.NET Core employs multiple models, which takes the single responsibility principle further than some interpretations of MVC.

In Chapter 5, we examined a todo list application example that could display all todos for a given category and username. With this application, you make a request to a URL using the todo/listcategory/{category}/{username} route. This returns a response displaying all relevant todos, as shown in Figure 6.1.

The application uses the same MVC structure you've seen, such as routing to Razor page handlers, and many different models. Figure 6.2 shows how a request to this application maps to the MVC design pattern and how it generates the final response, including additional details about model binding and request validation.

Figure 6.1 Basic todo list application showing todo list items. Users can filter the item list by changing the category and username parameters in the URL.
Figure 6.2 MVC pattern processing in ASP.NET Core to view a subset of items in a todo list Razor Pages application.

ASP.NET Core Razor Pages utilizes several different models, most of which are POCOs, along with the application model, which is more centered around the concept of service collections. Each model in ASP.NET Core handles different aspects of the overall requirements:

  • Binding Model - The binding model consists of all information provided by the user when making a request, along with additional context data. This includes route parameters parsed from the URL, query strings, and form or JSON data from the request body. The binding model itself is one or more POCO objects that you define. In Razor Pages, binding models are typically defined by creating a public property on the page's PageModel and decorating it with the [BindProperty] attribute. They can also be passed as parameters to page handlers.

For this example, the binding model would include the category name "open" and the username "Andrew". The Razor Pages infrastructure checks the binding model before executing page handlers to verify if the provided values are valid, though the handlers will execute even if they're not, as you'll see when discussing validation in section 6.3.

  • Application Model - The application model isn't really a true ASP.NET Core model at all. It's typically a collection of different services and classes, more of a concept—any thing needed to perform some business operation in your application. It might include domain models (representing things your application tries to describe) and database models (representing data stored in a database), along with any additional services.

In the todo list application, the application model would contain the complete list of todos, possibly stored in a database, and know how to find only those todos in the "open" category assigned to "Andrew".

  • Page Model - The PageModel for a Razor Page has two primary functions: it acts as the application's controller by exposing page handler methods, and serves as the view model for the Razor view. All data needed for view generation is exposed on the PageModel, such as the list of todos in the "open" category assigned to "Andrew".

The derived PageModel base class for Razor Pages contains various helper properties and methods. One of these is the ModelState property, which contains the model validation results as a series of key-value pairs.

These models form the backbone of any Razor Pages application, handling input, business logic, and output for each page handler. Imagine you have an e-commerce application that allows users to search for clothing by sending requests to the /search/{query} URL, where {query} contains their search term:

  • Binding Model - This would extract the {query} route parameter from the URL and any values posted in the request body (perhaps sorting order, or number of items to display), and bind them to a C# class that typically acts as a one-time data transfer class. When the page handler is invoked, this will be set as a property on the PageModel.
  • Application Model - This is the services and classes that perform the logic. When the page handler is invoked, this model loads all clothing items that match the query, applies necessary sorting and filtering, and returns the results to the controller.
  • Page Model - The values provided by the application model are set as properties on the Razor page's PageModel along with other metadata, such as total number of available items, or whether the user can currently check out. The Razor view will use this data to render the Razor view as HTML.

An important point about all these models is that their responsibilities are clearly defined and distinct. Keeping them separate and avoiding reuse helps ensure your application remains agile and easy to update.

The obvious exception to this separation is the PageModel, as it's where binding models and page handlers are defined, and it also holds the data needed for rendering the view. Some might consider this lack of separation a heresy, but it's typically not an issue. The boundary lines are very clear. For instance, as long as you don't try to call page handlers from Razor views, you won't encounter any problems!

From Request to Model: Making Requests Useful

By now, you should be familiar with how ASP.NET Core handles requests by executing page handlers on Razor pages. You've also seen several page handlers, such as:

public void OnPost(ProductModel product)

Page handlers are ordinary C# methods, so the ASP.NET Core framework needs to be able to invoke them in the usual way. When a page handler accepts parameters as part of its method signature, such as the product in the previous example, the framework needs a way to create these objects. Where exactly do they come from, and how are they produced?

Model binding extracts values from a request and uses them to create .NET objects. These objects are passed as method parameters to the executing page handler or set as properties on the PageModel that are marked with the [BindProperty] attribute.

The model binder is responsible for looking at the incoming request and finding values to use. It then creates an appropriate type of object and assigns these values to your model in a process called binding.

Any property on a Razor page's PageModel (in the .cshtml.cs file) decorated with the [BindProperty] attribute is created using model binding from the incoming request, as shown in the following listing. Similarly, if your page handler method has any parameters, these are also created using model binding.

Listing 6.1 Model binding a request to a property in a Razor page

public class IndexModel: PageModel
{
    // Properties decorated with [BindProperty] participate in model binding.
    [BindProperty]
    public string Category { get; set; }
    
    // Properties are not model bound for GET requests unless you use SupportsGet.
    [BindProperty(SupportsGet = true)]
    public string Username { get; set; }
    public void OnGet()
    {
    }
    public void OnPost(ProductModel model)
    {
    }
}

As shown in the previous listing, PageModel properties are not model bound for GET requests, even if you add the [BindProperty] attribute. For security reasons, only requests using verbs like POST and PUT are bound. If you do want to bind GET requests, you can set the SupportsGet property on the [BindProperty] attribute to opt-in to model binding.

To bind PageModel properties for GET requests, use the SupportsGet property of the attribute, for example [BindProperty(SupportsGet = true)].

Which Part is the Binding Model?

Listing 6.1 shows a Razor page using multiple binding models: the Category property, Username property, and ProductModel property (in the OnPost handler) are all model bound.

Using multiple models in this way is fine, but I prefer an approach that keeps all model binding in a single nested class, which I typically call InputModel. Using this approach, the Razor page in Listing 6.1 could be written as:

public class IndexModel: PageModel
{
    [BindProperty]
    public InputModel Input { get; set; }
    public void OnGet()
    {
    }
    public class InputModel
    {
        public string Category { get; set; }
        public string Username { get; set; }
        public ProductModel Model { get; set; }
    }
}


ASP.NET Core automatically populates binding models for you using attributes of the request, such as the request URL, any headers sent in the HTTP request, any explicitly posted data in the request body, and so on.

By default, ASP.NET Core uses three different binding sources when creating binding models. It checks each one in order and takes the first value it finds that matches the binding model name (if any):

  • Form values - Sent in the HTTP request body when a form is POSTed to the server
  • Route values - Obtained from URL segments or through default values after matching the route
  • Query string values - Passed at the end of the URL, not used during routing

The model binding process is shown in Figure 6.3. The model binder checks each binding source to see if it contains values that can be set on the model. Alternatively, the model can specify which particular source the value should come from, as you'll see in section 6.2.3. Once each property is bound, the model is validated and set as a property on the PageModel or passed as a parameter to the page handler. You'll learn about the validation process later in this chapter.

Figure 6.3 Model binding involves mapping values from binding sources that correspond to different parts of the request.

PageModel Properties or Page Handler Parameters?

There are two different approaches to using model binding in Razor Pages:

  • Decorating properties on PageModel with the [BindProperty] attribute.
  • Adding parameters to your page handler methods.

Which approach should you choose?

The answer to this question is largely a matter of taste. Setting properties on the PageModel and marking them with [BindProperty] is the method you most commonly see in examples. If you use this approach, you'll be able to access the binding model when rendering the view.

The alternative approach is to add parameters to your page handler methods, which provides more separation between different MVC phases since you won't be able to access the parameters outside of the page handler. The downside is that you do need to manually copy the parameters to properties that can be accessed in the view if you do need to display these values in the Razor view.

My chosen approach tends to depend on the specific Razor page I'm building. If I'm creating a form, I tend toward the [BindProperty] method because I typically need to access request values in the Razor view. For example, for a simple page where the binding model is a product ID, I tend to use the page handler parameter method because it's simple, especially when the handler is used for GET requests.

Figure 6.4 shows an example of a request that uses model binding to create the ProductModel method parameter, shown at the beginning of this section:

public void OnPost(ProductModel product)

Figure 6.4 Creating a model instance using model binding to execute a Razor page.

The Id property was bound from the URL route parameter, but the Name and SellPrice properties were bound from the request body. One major advantage of using model binding is that you don't need to write your own code to parse the request and map the data. This code is typically repetitive and error-prone, so using the built-in conventional approach lets you focus on the important aspects of your application: business requirements.

Model binding is excellent for reducing repetitive code. Take advantage of it whenever possible, and you'll rarely find yourself needing to directly access the Request object.

Binding Simple Types

We'll begin our model binding journey by considering a simple Razor page handler. The next listing shows a simple Razor page that accepts a number as a method parameter and squares it by multiplying the number by itself.

Listing 6.2 Razor page accepting simple parameters

public class CalculateSquareModel : PageModel
{
public void OnGet(int number) // Method parameter is the binding model.
{
Square = number * number; // A more complex example would do this work in an external service in the application model.
}
public int Square { get; set; } // Application model. The result is exposed as a property and used by the view to generate the response.
}

In the previous chapter, you learned about routing and how it selects which Razor page to execute. You can update the Razor page's route template to "CalculateSquare/{number}" by adding the {number} segment in the Razor page's @page directive in the .cshtml file, as we discussed in Chapter 4:

@page "{number}"

When a client requests the URL /CalculateSquare/5, the Razor page framework uses routing to parse it to obtain the route parameter. This produces the route value pair:

number=5

The Razor page's OnGet page handler contains a parameter - an integer called number - which is your binding model. When ASP.NET Core executes this page handler method, it will discover the expected parameter, look through the route values associated with the request, and find the number=5 pair. It can then bind the number parameter to this route value and execute the method. The page handler method itself doesn't care where this value comes from; it just takes it as is, calculates the square of the value, and sets it on the Square property.

The key to appreciate is that when the method executes, you don't need to write any additional code to try to extract the number from the URL. All you need to do is create a method parameter (or public property) with the correct name, and then let model binding do its work.

Route values aren't the only values the model binder can use to create binding models. As you saw earlier, the framework will look through the three default binding sources to find matches for your binding model:

  • Form values
  • Route values
  • Query string values

Each of these binding sources stores values as name-value pairs. If none of the binding sources contains the required value, the binding model will be set to a new default instance of that type. In this case, the exact value of the binding model depends on the type of the variable:

  • For value types, the value will be default(T). For an int parameter, this will be 0, while for a bool, it will be false.
  • For reference types, the type is created using the default (parameterless) constructor. For custom types like ProductModel, it will create a new object. For nullable types like int? or bool?, the value will be null.
  • For string types, the value will be null.

When model binding can't bind a method parameter, it's important to consider the behavior of the page handler. If none of the binding sources contains the value, the value passed to the method might be null or might unexpectedly have a default value (for value types).

Listing 6.2 shows how to bind a single method parameter. Let's take the next logical step and see how to bind multiple method parameters.

In the previous chapter, we discussed routing for building a currency converter application. As a next step in your development, your boss asks you to create a method where a user provides a value in one currency, and you must convert it to another currency. You first create a Razor page named Convert.cshtml, then use the @page directive to customize the page's route template to use an absolute path containing two route values:

@page "/{currencyIn}/{currencyOut}"

Then, you create a page handler that accepts the three values you need, as shown in the following listing.

Listing 6.3 Razor page handler accepting multiple binding parameters

public class ConvertModel : PageModel
{
public void OnGet(
string currencyIn,
string currencyOut,
int qty
)
{
/* method implementation */
}
}

As you can see, to bind three different parameters. The question is, where will these values come from and what will they be set to? The answer is, it depends! Table 6.1 shows various possibilities. All these examples use the same route template and page handler, but depending on the data sent, different values will be bound. The actual values might differ from your expectations because the available binding sources provide conflicting values!

Table 6.1 Binding request data to page handler parameters from multiple binding sources

URL (route values) HTTP body data (form values) Parameter values bound
/GBP/USD currencyIn=GBP currencyOut=USD qty=0
/GBP/USD?currencyIn=CAD QTY=50 currencyIn=GBP currencyOut=USD qty=50
/GBP/USD?qty=100 qty=50 currencyIn=GBP currencyOut=USD qty=50
/GBP/USD?qty=100 currencyIn=CAD&currencyOut=EUR&qty=50 currencyIn=CAD currencyOut=EUR qty=50

For each example, make sure you understand why the bound values have the values they have. In the first example, the qty value isn't found in form data, route values, or the query string, so its default value is 0. In each of the other examples, the request contains one or more duplicate values; in these cases, it's important to remember the order in which the model binder queries the binding sources. By default, form values will take precedence over other binding sources, including route values!

The default model binder is case-insensitive, so the bound value QTY=50 will happily bind to the qty parameter.

While this might seem a bit overwhelming, binding from all these different sources simultaneously is relatively uncommon. More commonly, your values all come from the request body as form values, possibly with an ID from the URL route values. If you're unsure how things work, this is more of a cautionary tale about how you can shoot yourself in the foot.

In these examples, you're happily binding the qty integer property to incoming values, but as I mentioned earlier, the values stored in the binding sources are all strings. What types can you convert strings to? The model binder will convert almost any primitive .NET type, such as int, float, decimal (obviously also string), and anything with a TypeConverter. There are also some other special cases that can be converted from strings, such as Guid types, but treating them as primitives will only get you so far!

Binding Complex Types

If it seems limiting that you can only bind simple primitive types, then you're right! Fortunately, the model binder isn't so limited. Although it can only directly convert strings to those primitive types, it's also capable of binding complex types by traversing any properties exposed by the binding model.

If this doesn't immediately excite you, then let's see how you would build a page handler if simple types were your only option. Imagine that users of your currency converter application have reached the checkout page and are ready to exchange some currency. Great! All you need now is to collect their name, email, and phone number. Unfortunately, your page handler method would have to look like this:

public IActionResult OnPost(string firstName, string lastName, string phoneNumber, string email)

Ugh! Four parameters might not seem so bad right now, but what happens when requirements change and you need to collect additional details? The method signature would constantly grow. The model binder would be quite happy to bind values, but it's not entirely clean code. Using the [BindProperty] approach wouldn't help either - you'd still need to clutter your PageModel with lots of properties and attributes!

Simplifying Method Parameters by Binding to Complex Objects

When you have many method parameters, a common pattern in any C# code is to extract a class that encapsulates the data needed by the method. If you need to add extra parameters, you can add a new property to that class. This class becomes your binding model, and it might look like this.

Listing 6.4 Binding model for capturing user details

public class UserBindingModel
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
public string PhoneNumber { get; set; }
}

With this model, you can now update your page handler method signature to:

public IActionResult OnPost(UserBindingModel user)

Or, using the [BindProperty] approach, create a property on the PageModel:

[BindProperty]
public UserBindingModel User { get; set; }

You can now further simplify your page handler signature:

public IActionResult OnPost()

Functionally, the model binder handles this new complex type slightly differently. Instead of looking for values that match the parameter name (user or the property's user), the model binder creates a new instance of the model using new UserBindingModel().

Although the name of the model isn't required in this example, the model binder will also look for properties prefixed with the property name, such as user.FirstName and user.LastName for a property named User. You can use this approach when you have multiple complex page handler parameters or multiple complex [BindProperty] attributes. Generally, for simplicity, you should avoid this situation where possible. For all model binding, the case of the prefix doesn't matter.

Once all bindable properties on the binding model are set, the model is passed to the page handler (or set as the [BindProperty] attribute), and then the handler executes as usual. From this point forward, the behavior is the same as when you have many individual parameters - the same values will ultimately be set on the binding model - but the code is cleaner and more usable.

For the class to be model bound, it must have a default public constructor. You can only bind public and settable properties.

Using this technique, you can bind complex hierarchical models where the properties themselves are complex models. As long as each property exposes a type that can be bound by the model binder, it can easily traverse it.

Binding Collections and Dictionaries

In addition to ordinary custom classes and primitives, you can also bind to collections, lists, and dictionaries. Imagine you have a page where users select all currencies they're interested in; you would display rates for all selected objects, as shown in Figure 6.5.

To do this, you could create a page handler that accepts a List<string> type, such as:

public void OnPost(List currencies);

Figure 6.5 Selection list in a currency converter application that sends a list of selected currencies to the application. Model binding can bind the selected currencies and customize the view for the user to display equivalent costs for selected currencies.

You could then POST data to this method in several different formats:

  • currencies[index] - Where currencies is the name of the parameter to bind to, index is the index of the item to bind to, for example currencies[0]=GBP&currencies[1]=USD.
  • [index] - If you're only binding to a single list (as in this example), you can omit the parameter name, for example [0]=GBP&[1]=USD.
  • currencies - Alternatively, you can omit the index and send currencies as the key for each value, for example, currencies=GBP&currencies=USD.

The key-value pairs can come from route values and query values, but more commonly they're posted in a form. Dictionaries can use similar binding, where dictionary keys replace the index in parameter naming and when omitted.

If all this seems a bit confusing, don't worry too much. If you're building a traditional web application and using Razor views to generate HTML, the framework will take care of generating the correct names for you.

Binding File Uploads with IFormFile

ASP.NET Core supports file uploads through the IFormFile interface. You can use this interface as your binding model, either as a method parameter to your page handler or using the [BindProperty] approach, which will populate the details of the file upload:

public void OnPost(IFormFile file);

If you need to accept multiple files, you can also use IEnumerable<IFormFile>:

public void OnPost(IEnumerable file);

The IFormFile object exposes several properties and utility methods for reading the contents of the uploaded file, some of which are shown here:

public interface IFormFile
{
string ContentType { get; }
long Length { get; }
string FileName { get; }
Stream OpenReadStream();
}

As you can see, this interface exposes a FileName property that returns the filename used when the file was uploaded. But you know not to trust users, right? You should never use the filename directly in your code - always generate a new filename for it before you save it anywhere.

If users are only expected to upload small files, the IFormFile approach works well. When your method accepts an IFormFile instance, the entire contents of the file are buffered in memory and disk before you receive it. You can then use the OpenReadStream method to read the data.

If a user posts a large file to your website, you might find that you run out of memory or disk space because it buffers each file. In this case, you might need to stream the file directly to avoid saving all data at once. Unfortunately, unlike the model binding approach, streaming large files can be complex and error-prone, so it's beyond the scope of this book.

Don't use the IFormFile interface to handle large file uploads as you might see performance problems. Be aware that you can't rely on users not uploading large files, so it's best to avoid file uploads altogether!

For the vast majority of Razor pages, the default configuration for simple and complex type model binding works well, but you might find you need more control in some situations. Fortunately, this is completely possible, and if necessary, you can completely override the process by replacing the ModelBinders used internally by the framework.

Selecting Binding Sources

As you've already seen, by default, the ASP.NET Core model binder will try to bind your binding model from three different binding sources: form data, route data, and query strings.

Sometimes, you might find it necessary to explicitly declare which binding source to bind to. In other cases, these three sources aren't enough at all. The most common scenarios are when you want to bind a method parameter to a request header value, or when the request body contains JSON-formatted data that you want to bind to a parameter. In these cases, you can decorate your binding model with attributes that specify where to bind from, as shown in the following listing.

Listing 6.5 Selecting binding sources for model binding

public class PhotosModel: PageModel
{
public void OnPost(
[FromHeader] string userId, // userId will be bound from HTTP headers in the request.
[FromBody] List photos) // The list of photo objects will be bound to the request body, typically in JSON format.
{
/* method implementation */
}
}

In this example, the page handler updates a collection of photos with a user ID. The photos have a user ID to be marked, userId, and a list of Photo objects to be marked, photos method parameters

Instead of binding these method parameters using the standard binding sources, I've added attributes to each parameter indicating which binding source to use. The [FromHeader] attribute has been applied to the userId parameter. This tells the model binder to bind that value to the HTTP request header value named userId.

We also use the [FromBody] attribute to bind the list of photos to the HTTP request body. This will read JSON from the request body and bind it to the List<Photo> method parameter.

Developers familiar with previous versions of ASP.NET should note that when binding to JSON requests in Razor Pages, the [FromBody] attribute is explicitly required. This is different from the behavior in ASP.NET where the attribute wasn't needed.

You're not limited to binding JSON data from the request body - you can use other formats as well, depending on the InputFormatter you configure the framework to use. By default, only the JSON input formatter is configured.

You can use several different attributes to override the defaults and specify a binding source for each binding model (or each property on the binding model):

  • [FromHeader] - Bind to header values
  • [FromQuery] - Bind to query string values
  • [FromRoute] - Bind to route parameters
  • [FromForm] - Bind to form data posted in the request body
  • [FromBody] - Bind to the contents of the request body

You can apply each of these to any number of handler method parameters or properties, as seen in Listing 6.5, except for the [FromBody] attribute - only one value can be decorated with the [FromBody] attribute. Additionally, since form data is sent in the request body, the [FromBody] and [FromForm] attributes are actually mutually exclusive.

Only one parameter can be decorated with the [FromBody] attribute. This attribute will consume the incoming request, as the HTTP request body can only be safely read once.

In addition to these attributes for specifying binding sources, there are some other attributes that can further customize the binding process:

  • [BindNever] - The model binder will completely skip this parameter.
  • [BindRequired] - If the parameter isn't provided or is empty, the binder will add a validation error.
  • [FromServices] - This is used to indicate that the parameter should be provided using dependency injection

Additionally, you have the [ModelBinder] attribute, which puts you into "god mode" for model binding. With this attribute, you can specify the exact binding source, override the name of the parameter to bind to, and specify the binding type to perform. You rarely need this, but when you do, at least it's there!

By combining all these attributes, you should find that you're able to configure the model binder to bind to almost any request data that your page handlers want to use. Generally speaking, however, you'll probably find that you rarely need to use them. In most cases, the defaults should work for you.

Handling User Input with Model Validation

The Necessity of Validation

Data can come from many different sources in a web application - you can load it from files, read it from a database, or accept values that users input into forms in requests. Although you might be inclined to trust data already existing on the server (though this is sometimes a dangerous assumption!), you should absolutely never trust data sent as part of a request.

Validation occurs in the Razor Pages framework after model binding but before page handler execution, as shown in Figure 6.2. Figure 6.6 shows a more compact view of where model validation fits in this process, showing how binding and validation work for a request to a checkout page for user personal details.

Figure 6.6 Validation occurs after model binding but before page handler execution. The page handler executes regardless of whether validation succeeds.

However, validation isn't just about checking for security threats. It's also needed to check for non-malicious errors:

  • Data should be in the correct format (email fields have a valid email format).
  • Numbers might need to be within a specific range (you can't buy -1 copies of this book!).
  • Some values might be required while others are optional (profiles might require a name, but phone numbers are optional).
  • Values must conform to your business requirements (you can't convert a currency to itself; it needs to be converted to a different currency).

Some of these can be easily handled in the browser. For example, if users select a currency to convert, don't let them select the same currency; we've all seen the "Please enter a valid email address" message.

Unfortunately, although this client-side validation is useful for users as it provides immediate feedback, you can never rely on it because it can always be bypassed. When data reaches your web application, you always need to validate it on the server side.

If this feels a bit redundant, like you'll be repeating logic and code, then unfortunately you're right. This is one of the unfortunate aspects of web development; repetition is a necessary evil. Thankfully, ASP.NET Core provides some features to try to alleviate this burden.

Using DataAnnotations Attributes for Validation

Validation attributes, or more accurately DataAnnotations attributes, allow you to specify rules that a binding model should adhere to. They provide metadata about the model by describing what type of data the binding model should contain, rather than the data itself.

Metadata describes other data, specifying rules and characteristics that the data should adhere to.

You can apply DataAnnotations attributes directly to binding models to indicate acceptable data types. For example, this allows you to check whether required fields are provided, whether numbers are within the correct range, and whether email fields are valid email addresses.

For example, let's consider the checkout page for your currency converter application. You need to collect user details to proceed, so you ask them for their name, email, and phone number (optional). The following listing shows the UserBindingModel decorated with validation attributes that represent model validation rules. This extends the example you saw in Listing 6.4.

Listing 6.6 Adding DataAnnotations to binding models to provide metadata

public class UserBindingModel
{
[Required] // Values marked as required must be provided.
[StringLength(100)] // StringLengthAttribute sets the maximum length for the property.
[Display(Name = "Your name")] // Customize the name used to describe the property
public string FirstName { get; set; }
[Required]
[StringLength(100)]
[Display(Name = "Last name")] // Customize the name used to describe the property
public string LastName { get; set; }
[Required]
[EmailAddress] // Validates the value is a valid email adress
public string Email { get; set; }
[Phone]
[Display(Name = "Phone number")]
public string PhoneNumber { get; set; }
}

Suddenly, your binding model contains a lot more information than it did before, when its details were sparse. For example, you've specified that the FirstName property should always be provided, its maximum length should be 100 characters, and when referring to it (for example in error messages), it should be called "Your name".

The benefit of these attributes is that they clearly declare the expected state of the model. By looking at these attributes, you know what these properties will contain or should contain. They also provide hooks for the ASP.NET Core framework to validate whether the data set on the model during model binding is valid, which you'll see shortly.

When applying DataAnnotations to models, you have a large selection of attributes to choose from. I've listed some common ones here, but you can find more in the System.ComponentModel.DataAnnotations namespace.

  • [CreditCard] - Validates that the property has a valid credit card format.
  • [EmailAddress] - Validates that the property has a valid email address format.
  • [StringLength(max)] - Validates that a string contains at most a maximum number of characters.
  • [MinLength(min)] - Validates that a collection contains at least a minimum number of items.
  • [Phone] - Validates that the property has a valid phone number format.
  • [Range(min, max)] - Validates that the value of the property is between min and max.
  • [Regular Expression(regex)] - Validates that the property matches the regex regular expression pattern.
  • [Url] - Validates that the property has a valid URL format.
  • [Required] - Indicates that the property cannot be empty.
  • [Compare] - Allows you to confirm that two properties have the same value (for example, Email and ConfirmEmail).

The [EmailAddress] and other attributes only validate that the format of the value is correct. They don't validate that the email address actually exists.

DataAnnotations attributes aren't a new feature - they've been part of the .NET Framework since version 3.5 - and their use in ASP.NET Core is almost identical to their use in previous versions of ASP.NET.

In addition to validation, they're also used for other purposes. Entity Framework Core (and others) use DataAnnotations to define column types and rules to use when creating database tables from C# classes.

If the out-of-the-box DataAnnotation attributes don't cover everything you need, you can also write custom attributes by deriving from the base ValidationAttribute.

DataAnnotations works well for validating input for individual properties, but not for validating business rules. You'll likely need to perform this validation outside the DataAnnotations framework.

Whatever validation method you use, always remember that these techniques alone don't protect your application. The Razor Pages framework will ensure that validation occurs, but if validation fails, it won't automatically do anything. In the next section, we'll look at how to check the validation results on the server and handle validation failures.

Validating Security on the Server

Validation of the binding model occurs before page handler execution, but note that the handler always executes, whether validation fails or succeeds. Checking the validation results is the responsibility of the page handler.

The Razor Pages framework stores the output of validation attempts in a property on the PageModel called ModelState. This property is a ModelStateDictionary object that contains a list of all validation errors that occurred after model binding, along with some utility properties for using it.

For example, the following listing shows the OnPost page handler for the Checkout.cshtml Razor page. The Input property is marked as bound and uses the UserBindingModel type shown earlier in Listing 6.6. This page handler currently doesn't do anything with the data, but the pattern of checking ModelState early in the method is key here.

Listing 6.7 Checking model state to see validation results

public class CheckoutModel : PageModel // ModelState property is available in the PageModel base class.
{
[BindProperty]
public UserBindingModel Input { get; set; } // Input property contains the model bound data.
public IActionResult OnPost() // Binding model is validated before executing the page handler.
{
if (!ModelState.IsValid) // If there are validation errors, IsValid will be false.
{
return Page(); // Validation failed, so redisplay the form with errors and exit the method early.
}
/* Save to the database, update user, return success */
// Validation passed, so it's safe to use the data provided in the model.
return RedirectToPage("Success");
}
}

If the ModelState property indicates that an error occurred, the method immediately calls the Page helper method. This returns a PageResult, which will ultimately generate HTML to return to the user, as you saw in Chapter 3. The view uses the (invalid) values provided in the Input property to repopulate the form when displayed, as shown in Figure 6.7. Additionally, validation errors from the ModelState property are automatically added with messages useful to the user.

The error messages displayed on the form are the default values for each validation attribute. You can customize the messages by setting the ErrorMessage property on any validation attribute. For example, you can customize the [Required] attribute using [Required(ErrorMessage="Required")].

If the request is successful, the page handler returns a RedirectToPageResult (using the RedirectToPage() helper method), redirecting the user to the Success.cshtml Razor page. This pattern of returning a redirect response after a successful POST is called the POST-REDIRECT-GET pattern.

Figure 6.7 When validation fails, you can redisplay the form to show the ModelState validation errors to the user. Note that unlike other fields, your name field has no associated validation error.

Your application doesn't control this validation; it's built into modern HTML5 browsers. Another approach is to perform client-side validation by running JavaScript on the page and checking user input values before submitting the form. This is the most common approach in Razor Pages.

With this approach, users can immediately see any errors in the form, even before the request is sent to the server, as shown in Figure 6.9. This provides a shorter feedback cycle and a better user experience.

If you're building a SPA, it's your responsibility to validate data on the client side using the client framework before posting it to the Web API. The Web API will still validate the data when it reaches the server, but the client framework is responsible for providing a smooth user experience.

Figure 6.9 With client-side validation, clicking submit triggers validation, displaying in the browser before the request is sent to the server. As shown in the right pane, no request is sent.

Organizing Binding Models in Razor Pages

There are many equivalent approaches to model binding in ASP.NET Core, so there's no "correct" way to do it. The following listing shows an example of how I would design a simple Razor page. This Razor page displays a form for a product with a given ID and allows you to edit its details using a POST request. This is a longer sample than what we've seen so far, but I've highlighted the key points.

Listing 6.8 Designing an edit product Razor page

public class EditProductModel : PageModel
{
// ProductService is injected using DI and provides access to the application model.
private readonly ProductService _productService;
public EditProductModel(ProductService productService)
{
_productService = productService;
}
[BindProperty]
public InputModel Input { get; set; } // Single property marked with BindProperty.
public IActionResult OnGet(int id) // id parameter is model bound from the route template for both OnGet and OnPost handlers.
{
var product = _productService.GetProduct(id); // Load product details from the application model.
Input = new InputModel // Build an instance of InputModel from existing product details for editing in the form.
{
Name = product.ProductName,
Price = product.SellPrice,
};
return Page();
}
public IActionResult OnPost(int id) // id parameter is model bound from the route template for both OnGet and OnPost handlers.
{
// Redisplay the form without saving if the request is invalid.
if (!ModelState.IsValid)
{
return Page();
}
// Update the product in the application model using ProductService.
_productService.UpdateProduct(id, Input.Name, Input.Price);
// Redirect to a new page using the POST-REDIRECT GET pattern.
return RedirectToPage("Index");
}

// Define InputModel as a nested class within the Razor page.
public class InputModel
{
[Required]
public string Name { get; set; }
[Range(0, int.MaxValue)]
public decimal Price { get; set; }
}
}

This page shows the PageModel for a typical "edit form." These are very common in many line-of-business applications, among other things, which is a scenario that Razor Pages is particularly well-suited for.

This form shows several patterns related to model binding that I try to follow when building Razor pages:

  • Only use [BindProperty] to bind a single property. I tend to use a single property decorated with [BindProperty] for model binding. When I need to bind multiple values, I create a separate class InputModel to hold these values and decorate that single property with [BindProperty]. Decorating a single property like this makes it harder to forget to add the attribute, which means all your Razor pages use the same pattern.
  • Define binding models as nested classes. I define InputModel as a nested class within the Razor page. Binding models are often highly specific to that single page, so doing this keeps everything you're working with together. Additionally, I typically use the exact class name InputModel for all my pages. Again, this adds consistency to your Razor pages.
  • Don't use [BindProperties]. In addition to the [BindProperty] attribute, there's also a [BindProperties] attribute (note the different spelling) that can be applied directly to the Razor Page PageModel. This causes all properties in the model to be model bound, which could potentially expose you to overposting attacks if you're not careful. I recommend that you don't use the [BindProperties] attribute and instead stick to using [BindProperty] to bind single properties.
  • Accept route parameters in page handlers. For simple route parameters, such as the id passed to the OnGet and OnPost handlers in Listing 6.8, I add the parameter to the page handler method itself. This avoids the clunky SupportsGet=true syntax for GET requests.
  • Always validate before using data. I've said it before, so I'll say it again. Validate user input!

That's a wrap for model binding in Razor Pages. You've seen how the ASP.NET Core framework uses model binding to simplify extracting values from requests and converting them into ordinary .NET objects that you can use quickly. The most important aspect of this chapter is the focus on validation - something all web applications have in common, and which can be easily added to models using DataAnnotations.

Summary

  • Razor Pages uses three different models, each responsible for different aspects of a request. Binding models encapsulate data sent as part of a request. Application models represent the state of the application. PageModel is the backing class for a Razor page that exposes data used by Razor views to generate responses.
  • Model binding extracts values from a request and uses them to create .NET objects that page handlers can use when they execute.
  • Any property on PageModel marked with the [BindProperty] attribute and method parameters of page handlers will participate in model binding.
  • Properties decorated with [BindProperty] are not bound for GET requests. To bind GET requests, you must use [BindProperty(SupportsGet = true)] instead.
  • By default, there are 3 binding sources: POST form values, route values, and query strings. When trying to bind the binding model, the binder will ask these in order.
  • When binding values to a model, parameter and property names are case-insensitive.
  • You can bind to simple types or to properties of complex types.
  • To bind complex types, they must have a default constructor and public settable properties.
  • Simple types must be convertible to strings to be automatically bound; for example, numbers, dates, and booleans.
  • Collections and dictionaries can be bound using the [index]=value and [key]=value syntax respectively.
  • You can use [From*] attributes applied to methods to customize the binding source for binding models, such as [FromHeader] and [FromBody]. These can be used to bind to non-default binding sources, such as headers or JSON body content.
  • Unlike previous versions of ASP.NET, the [FromBody] attribute is required when binding JSON properties (it wasn't required before).
  • Validation is necessary for checking security threats. Check that data is in the correct format, confirm it has expected values, and that it conforms to your business rules.
  • ASP.NET Core provides DataAnnotations attributes to allow you to define expected values declaratively.
  • Validation happens automatically after model binding, but you must manually check the validation results and take appropriate action in your page handler by asking the ModelState property.
  • Client-side validation provides a better user experience than server-side validation alone, but you should always use server-side validation.
  • Client-side validation uses JavaScript and attributes applied to HTML elements to validate form values.

Tags: ASP.NET Core Model Binding Razor Pages mvc Validation

Posted on Mon, 29 Jun 2026 17:34:19 +0000 by Eggzorcist