15 Generating responses with page handlers in Razor Pages
This chapter covers
• Selecting which page handler in a Razor Page to invoke for a request
• Returning an IActionResult from a page handler
• Handling status code errors with StatusCodePagesMiddleware
In chapter 14 you learned how the routing system selects a Razor Page to execute based on its associated route template and the request URL, but each Razor Page can have multiple page handlers. In this chapter you’ll learn all about page handlers, their responsibilities, and how a single Razor Page selects which handler to execute for a request.
In section 15.3 we look at some of the ways of retrieving values from an HTTP request in a page handler. Much like minimal APIs, page handlers can accept method arguments that are bound to values in the HTTP request, but Razor Pages can also bind the request to properties on the PageModel.
In section 15.4 you’ll learn how to return IActionResult objects from page handlers. Then you look at some of the common IActionResult types that you’ll return from page handlers for generating HTML and redirect responses.
Finally, in section 15.5 you’ll learn how to use the StatusCodePagesMiddleware to improve the error status code responses in your middleware pipeline. This middleware intercepts error responses such as basic 404 responses and reexecutes the middleware pipeline to generate a pretty HTML response for the error. This gives users a much nicer experience when they encounter an error browsing your Razor Pages app.
We’ll start by taking a quick look at the responsibilities of a page handler before we move on to see how the Razor Page infrastructure selects which page handler to execute.
15.1 Razor Pages and page handlers
In chapter 13 I described the Model-View-Controller (MVC) design pattern and showed how it relates to ASP.NET Core. In this design pattern, the “controller” receives a request and is the entry point for UI generation. For Razor Pages, the entry point is the page handler that resides in a Razor Page’s PageModel. A page handler is a method that runs in response to a request.
The responsibility of a page handler is generally threefold:
• Confirm that the incoming request is valid.
• Invoke the appropriate business logic corresponding to the incoming request.
• Choose the appropriate kind of response to return.
A page handler doesn’t need to perform all these actions, but at the very least it must choose the kind of response to return. Page handlers typically return one of three things:
• A PageResult object—This causes the associated Razor view to generate an HTML response.
• Nothing (the handler returns void or Task)—This is the same as the previous case, causing the Razor view to generate an HTML response.
• A RedirectToPageResult—This indicates that the user should be redirected to a different page in your application.
These are the most common results for Razor Pages, but I describe some additional options in section 15.4.
It’s important to realize that a page handler doesn’t generate a response directly; it selects the type of response and prepares the data for it. For example, returning a PageResult doesn’t generate any HTML at that point; it merely indicates that a view should be rendered. This is in keeping with the MVC design pattern in which it’s the view that generates the response, not the controller.
Tip The page handler is responsible for choosing what sort of response to send; the view engine in the MVC framework uses the result to generate the response.
It’s also worth bearing in mind that page handlers generally shouldn’t be performing business logic directly. Instead, they should call appropriate services in the application model to handle requests. If a page handler receives a request to add a product to a user’s cart, it shouldn’t manipulate the database or recalculate cart totals directly, for example. Instead, it should make a call to another class to handle the details. This approach of separating concerns ensures that your code stays testable and maintainable as it grows.
15.2 Selecting a page handler to invoke
In chapter 14 I said routing is about mapping URLs to an endpoint, which for Razor Pages means a page handler. But I’ve mentioned several times that Razor Pages can contain multiple page handlers. In this section you’ll learn how the EndpointMiddleware selects which page handler to invoke when it executes a Razor Page.
As you saw in chapter 14, the path of a Razor Page on disk controls the default route template for a Razor Page. For example, the Razor Page at the path Pages/Products/Search.cshtml has a default route template of Products/Search. When a request is received with the URL /products/search, the RoutingMiddleware selects this Razor Page, and the request passes through the middleware pipeline to the EndpointMiddleware. At this point, the EndpointMiddleware must choose which page handler to execute, as shown in figure 15.1.
Figure 15.1 The routing middleware selects the Razor Page to execute based on the incoming request URL. Then the endpoint middleware selects the endpoint to execute based on the HTTP verb of the request and the presence (or lack) of a handler route value.
Consider the Razor Page SearchModel shown in listing 15.1. This Razor Page has three handlers: OnGet, OnPostAsync, and OnPostCustomSearch. The bodies of the handler methods aren’t shown, as we’re interested only in how the EndpointMiddleware chooses which handler to invoke.
Listing 15.1 Razor Page with multiple page handlers
public class SearchModel : PageModel
{
public void OnGet() ❶
{
// Handler implementation
}
public Task OnPostAsync() ❷
{
// Handler implementation
}
public void OnPostCustomSearch() ❸
{
// Handler implementation
}
}
❶ Handles GET requests
❷ Handles POST requests. The async suffix is optional and is ignored for routing purposes.
❸ Handles POST requests where the handler route value has the value CustomSearch
Razor Pages can contain any number of page handlers, but only one runs in response to a given request. When the EndpointMiddleware executes a selected Razor Page, it selects a page handler to invoke based on two variables:
• The HTTP verb used in the request (such as GET, POST, or DELETE)
• The value of the handler route value
The handler route value typically comes from a query string value in the request URL, such as /Search?handler=CustomSearch. If you don’t like the look of query strings (I don’t!), you can include the {handler} route parameter in your Razor Page’s route template. For the Search page model in listing 15.2, you could update the page’s directive to
@page "{handler?}"
This would give a complete route template something like "Search/{handler?}", which would match URLs such as /Search and /Search/CustomSearch.
The EndpointMiddleware uses the handler route value and the HTTP verb together with a standard naming convention to identify which page handler to execute, as shown in figure 15.2. The handler parameter is optional and is typically provided as part of the request’s query string or as a route parameter, as described earlier. The async suffix is also optional and is often used when the handler uses asynchronous programming constructs such as Task or async/await.
Figure 15.2 Razor Page handlers are matched to a request based on the HTTP verb and the optional handler parameter.
NOTE The async suffix naming convention is suggested by Microsoft, though it is unpopular with some developers. NServiceBus provides a reasoned argument against it here (along with Microsoft’s advice): http://mng.bz/e59P.
Based on this convention, we can now identify what type of request each page handler in listing 15.1 corresponds to:
• OnGet—Invoked for GET requests that don’t specify a handler value
• OnPostAsync—Invoked for POST requests that don’t specify a handler value; returns a Task, so it uses the Async suffix, which is ignored for routing purposes
• OnPostCustomSearch—Invoked for POST requests that specify a handler value of "CustomSearch"
The Razor Page in listing 15.1 specifies three handlers, so it can handle only three verb-handler pairs. But what happens if you get a request that doesn’t match these, such as a request using the DELETE verb, a GET request with a nonblank handler value, or a POST request with an unrecognized handler value?
For all these cases, the EndpointMiddleware executes an implicit page handler instead. Implicit page handlers contain no logic; they simply render the Razor view. For example, if you sent a DELETE request to the Razor Page in listing 15.1, the EndpointMiddleware would execute an implicit handler. The implicit page handler is equivalent to the following handler code:
public void OnDelete() { }
DEFINITION If a page handler does not match a request’s HTTP verb and handler value, an implicit page handler is executed that renders the associated Razor view. Implicit page handlers take part in model binding and use page filters but execute no logic.
There’s one exception to the implicit page handler rule: if a request uses the HEAD verb, and there is no corresponding OnHead handler, the EndpointMiddleware executes the OnGet handler instead (if it exists).
NOTE HEAD requests are typically sent automatically by the browser and don’t return a response body. They’re often used for security purposes, as you’ll see in chapter 28.
Now that you know how a page handler is selected, you can think about how it’s executed.
15.3 Accepting parameters to page handlers
In chapter 7 you learned about the intricacies of model binding in minimal API endpoint handlers. Like minimal APIs, Razor Page page handlers can use model binding to easily extract values from the request. You’ll learn the details of Razor Page model binding in chapter 16; in this section you’ll learn about the basic mechanics of Razor Page model binding and the basic options available.
When working with Razor Pages, you’ll often want to extract values from an incoming request. If the request is for a search page, the request might contain the search term and the page number in the query string. If the request is POSTing a form to your application, such as a user logging in with their username and password, those values may be encoded in the request body. In other cases, there will be no values, such as when a user requests the home page for your application.
DEFINITION The process of extracting values from a request and converting them to .NET types is called model binding. I discuss model binding for Razor Pages in detail in chapter 16.
ASP.NET Core can bind two different targets in Razor Pages:
• Method arguments—If a page handler has method parameters, the arguments are bound and created from values in the request.
• Properties marked with a [BindProperty] attribute—Any properties on the PageModel marked with this attribute are bound to the request. By default, this attribute does nothing for GET requests.
Model-bound values can be simple types, such as strings and integers, or they can be complex types, as shown in the following listing. If any of the values provided in the request are not bound to a property or page handler argument, the additional values will go unused.
Listing 15.2 Example Razor Page handlers
public class SearchModel : PageModel
{
private readonly SearchService _searchService; ❶
public SearchModel(SearchService searchService) ❶
{ ❶
_searchService = searchService; ❶
} ❶
[BindProperty] ❷
public BindingModel Input { get; set; } ❷
public List<Product> Results { get; set; } ❸
public void OnGet() ❹
{ ❹
} ❹
public IActionResult OnPost(int max) ❺
{
if (ModelState.IsValid) ❻
{ ❻
Results = _searchService.Search(Input.SearchTerm, max); ❻
return Page(); ❻
} ❻
return RedirectToPage("./Index"); ❻
}
}
❶ The SearchService is injected from DI for use in page handlers.
❷ Properties decorated with the [BindProperty] attribute are model-bound.
❸ Undecorated properties are not model-bound.
❹ The page handler doesn’t need to check if the model is valid. Returning void renders the view.
❺ The max parameter is model-bound using values in the request.
❻ If the request was not valid, the method indicates the user should be
redirected to the Index page.
In this example, the OnGet handler doesn’t require any parameters, and the method is simple: it returns void, which means the associated Razor view will be rendered. It could also have returned a PageResult; the effect would have been the same. Note that this handler is for HTTP GET requests, so the Input property decorated with [BindProperty] is not bound.
Tip To bind properties for GET requests too, use the SupportsGet property of the attribute, as in [BindProperty(SupportsGet = true)].
The OnPost handler, conversely, accepts a parameter max as an argument. In this case it’s a simple type, int, but it could also be a complex object. Additionally, as this handler corresponds to an HTTP POST request, the Input property is also model-bound to the request.
NOTE Unlike most .NET classes, you can’t use method overloading to have multiple page handlers on a Razor Page with the same name.
When a page handler uses model-bound properties or parameters, it should always check that the provided model is valid using ModelState.IsValid. The ModelState property is exposed as a property on the base PageModel class and can be used to check that all the bound properties and parameters are valid. You’ll see how the process works in chapter 16 when you learn about validation.
Once a page handler establishes that the arguments provided to a page handler method are valid, it can execute the appropriate business logic and handle the request. In the case of the OnPost handler, this involves calling the injected SearchService and setting the result on the Results property. Finally, the handler returns a PageResult by calling the helper method on the PageModel base class:
return Page();
If the model isn’t valid, as indicated by ModelState.IsValid, you don’t have any results to display! In this example, the action returns a RedirectToPageResult using the RedirectToPage() helper method. When executed, this result sends a 302 Redirect response to the user, which will cause their browser to navigate to the Index Razor Page.
Note that the OnGet method returns void in the method signature, whereas the OnPost method returns an IActionResult. This is required in the OnPost method to allow the C# to compile (as the Page() and RedirectToPage() helper methods return different types), but it doesn’t change the final behavior of the methods. You could easily have called Page() in the OnGet method and returned an IActionResult, and the behavior would be identical.
Tip If you’re returning more than one type of result from a page handler, you’ll need to ensure that your method returns an IActionResult.
In listing 15.2 I used Page() and RedirectToPage() methods to generate the return value. IActionResult instances can be created and returned using the normal new syntax of C#:
return new PageResult()
However, the Razor Pages PageModel base class also provides several helper methods for generating responses, which are thin wrappers around the new syntax. It’s common to use the Page() method to generate an appropriate PageResult, the RedirectToPage() method to generate a RedirectToPageResult, or the NotFound() method to generate a NotFoundResult.
Tip Most IActionResult implementations have a helper method on the base PageModel class. They’re typically named Type, and the result generated is called TypeResult. For example, the StatusCode() method returns a StatusCodeResult instance.
In the next section we’ll look in more depth at some of the common IActionResult types.
15.4 Returning IActionResult responses
In the previous section, I emphasized that page handlers decide what type of response to return, but they don’t generate the response themselves. It’s the IActionResult returned by a page handler that, when executed by the Razor Pages infrastructure using the view engine, generates the response.
Warning Note that the interface type is IActionResult not IResult. IResult is used in minimal APIs and should generally be avoided in Razor Pages (and MVC controllers). In .NET 7, IResult types returned from Razor Pages or MVC controllers execute as expected, but they don’t have all the same features as IActionResult, so you should favor IActionResult in Razor Pages.
IActionResults are a key part of the MVC design pattern. They separate the decision of what sort of response to send from the generation of the response. This allows you to test your action method logic to confirm that the right sort of response is sent for a given input. You can then separately test that a given IActionResult generates the expected HTML, for example.
ASP.NET Core has many types of IActionResult, such as
• PageResult—Generates an HTML view for the associated page in Razor Pages and returns a 200 HTTP response.
• ViewResult—Generates an HTML view for a given Razor view when using MVC controllers and returns a 200 HTTP response.
• PartialViewResult—Renders part of an HTML page using a given Razor view and returns a 200 HTTP result; typically used with MVC controllers and AJAX requests.
• RedirectToPageResult—Sends a 302 HTTP redirect response to automatically send a user to another page.
• RedirectResult—Sends a 302 HTTP redirect response to automatically send a user to a specified URL (doesn’t have to be a Razor Page).
• FileResult—Returns a file as the response. This is a base class with several derived types:
• • FileContentResult—Returns a byte[] as a file response to the browser
• • FileStreamResult—Returns the contents of a Stream as a file response to the browser
• • PhysicalFileResult—Returns the contents of a file on disk as a file response to the browser
• ContentResult—Returns a provided string as the response.
• StatusCodeResult—Sends a raw HTTP status code as the response, optionally with associated response body content.
• NotFoundResult—Sends a raw 404 HTTP status code as the response.
Each of these, when executed by Razor Pages, generates a response to send back through the middleware pipeline and out to the user.
Tip When you’re using Razor Pages, you generally won’t use some of these action results, such as ContentResult and StatusCodeResult. It’s good to be aware of them, though, as you will likely use them if you are building Web APIs with MVC controllers, as you’ll see in chapter 20.
In sections 15.4.1–15.4.3 I give a brief description of the most common IActionResult types that you’ll use with Razor Pages.
15.4.1 PageResult and RedirectToPageResult
When you’re building a traditional web application with Razor Pages, usually you’ll be using PageResult, which generates an HTML response from the Razor Page’s associated Razor view. We’ll look at how this happens in detail in chapter 17.
You’ll also commonly use the various redirect-based results to send the user to a new web page. For example, when you place an order on an e-commerce website, you typically navigate through multiple pages, as shown in figure 15.3. The web application sends HTTP redirects whenever it needs you to move to a different page, such as when a user submits a form. Your browser automatically follows the redirect requests, creating a seamless flow through the checkout process.
Figure 15.3 A typical POST, REDIRECT, GET flow through a website. A user sends their shopping basket to a checkout page, which validates its contents and redirects to a payment page without the user’s having to change the URL manually.
In this flow, whenever you return HTML you use a PageResult; when you redirect to a new page, you use a RedirectToPageResult.
Tip Razor Pages are generally designed to be stateless, so if you want to persist data between multiple pages, you need to place it in a database or similar store. If you want to store data for a single request, you may be able to use TempData, which stores small amounts of data in cookies for a single request. See the documentation for details: http://mng.bz/XdXp.
15.4.2 NotFoundResult and StatusCodeResult
As well as sending HTML and redirect responses, you’ll occasionally need to send specific HTTP status codes. If you request a page for viewing a product on an e-commerce application, and that product doesn’t exist, a 404 HTTP status code is returned to the browser, and you’ll typically see a “Not found” web page. Razor Pages can achieve this behavior by returning a NotFoundResult, which returns a raw 404 HTTP status code. You could achieve a similar result using StatusCodeResult and setting the status code returned explicitly to 404.
Note that NotFoundResult doesn’t generate any HTML; it only generates a raw 404 status code and returns it through the middleware pipeline. This generally isn’t a great user experience, as the browser typically displays a default page, such as that shown in figure 15.4.
Figure 15.4 If you return a raw 404 status code without any HTML, the browser will render a generic default page instead. The message is of limited utility to users and may leave many of them confused or thinking that your web application is broken.
Returning raw status codes is fine when you’re building an API, but for a Razor Pages application, this is rarely good enough. In section 15.5 you’ll learn how you can intercept this raw 404 status code after it’s been generated and provide a user-friendly HTML response for it instead.
15.5 Handler status codes with StatusCodePagesMiddleware
In chapter 4 we discussed error handling middleware, which is designed to catch exceptions generated anywhere in your middleware pipeline, catch them, and generate a user-friendly response. In this section you’ll learn about an analogous piece of middleware that intercepts error HTTP status codes: StatusCodePagesMiddleware.
Your Razor Pages application can return a wide range of HTTP status codes that indicate some sort of error state. You’ve seen previously that a 500 “server error” is sent when an exception occurs and isn’t handled and that a 404 “file not found” error is sent when you return a NotFoundResult from a page handler. 404 errors are particularly common, often occurring when a user enters an invalid URL.
Tip 404 errors are often used to indicate that a specific requested object was not found. For example, a request for the details of a product with an ID of 23 might return a 404 if no such product exists. They’re also generated automatically if no endpoint in your application matches the request URL.
Returning “raw” status codes without additional content is generally OK if you’re building a minimal API or web API application. But as mentioned before, for apps consumed directly by users such as Razor Pages apps, this can result in a poor user experience. If you don’t handle these status codes, users will see a generic error page, as you saw in figure 15.4, which may leave many confused users thinking your application is broken. A better approach is to handle these error codes and return an error page that’s in keeping with the rest of your application or at least doesn’t make your application look broken.
Microsoft provides StatusCodePagesMiddleware for handling this use case. As with all error handling middleware, you should add it early in your middleware pipeline, as it will handle only errors generated by later middleware components.
You can use the middleware several ways in your application. The simplest approach is to add the middleware to your pipeline without any additional configuration, using
app.UseStatusCodePages();
With this method, the middleware intercepts any response that has an HTTP status code that starts with 4xx or 5xx and has no response body. For the simplest case, where you don’t provide any additional configuration, the middleware adds a plain-text response body, indicating the type and name of the response, as shown in figure 15.5. This is arguably worse than the default message at this point, but it is a starting point for providing a more consistent experience to users.
Figure 15.5 Status code error page for a 404 error. You generally won’t use this version of the middleware in production, as it doesn’t provide a great user experience, but it demonstrates that the error codes are being intercepted correctly.
A more typical approach to using StatusCodePagesMiddleware in production is to reexecute the pipeline when an error is captured, using a similar technique to the ExceptionHandlerMiddleware. This allows you to have dynamic error pages that fit with the rest of your application. To use this technique, replace the call to UseStatusCodePages with the following extension method:
app.UseStatusCodePagesWithReExecute("/{0}");
This extension method configures StatusCodePagesMiddleware to reexecute the pipeline whenever a 4xx or 5xx response code is found, using the provided error handling path. This is similar to the way ExceptionHandlerMiddleware reexecutes the pipeline, as shown in figure 15.6.
Figure 15.6 StatusCodePagesMiddleware reexecuting the pipeline to generate an HTML body for a 404 response. A request to the / path returns a 404 response, which is handled by the status code middleware. The pipeline is reexecuted using the /404 path to generate the HTML response.
Note that the error handling path "/{0}" contains a format string token, {0}. When the path is reexecuted, the middleware replaces this token with the status code number. For example, a 404 error would reexecute the /404 path. The handler for the path (typically a Razor Page, but it can be any endpoint) has access to the status code and can optionally tailor the response, depending on the status code. You can choose any error handling path as long as your application knows how to handle it.
With this approach in place, you can create different error pages for different error codes, such as the 404-specific error page shown in figure 15.7. This technique ensures that your error pages are consistent with the rest of your application, including any dynamically generated content, while also allowing you to tailor the message for common errors.
Figure 15.7 An error status code page for a missing file. When an error code is detected (in this case, a 404 error), the middleware pipeline is reexecuted to generate the response. This allows dynamic portions of your web page to remain consistent on error pages.
Warning As I mentioned in chapter 4, if your error handling path generates an error, the user will see a generic browser error. To mitigate this, it’s often better to use a static error page that will always work rather than a dynamic page that risks throwing more errors.
The UseStatusCodePagesWithReExecute() method is great for returning a friendly error page when something goes wrong in a request, but there’s a second way to use the StatusCodePagesMiddleware. Instead of reexecuting the pipeline to generate the error response, you can redirect the browser to the error page instead, by calling
app.UseStatusCodePagesWithRedirects("/{0}");
As for the reexecute version, this method takes a format string that defines the URL to generate the response. However, whereas the reexecute version generates the error response for the original request, the redirect version returns a 302 response initially, directing the browser to send a second request, this time for the error URL, as shown in figure 15.8. This second request generates the error page response, returning it with a 200 status code.
Figure 15.8 StatusCodePagesMiddleware returning redirects to generate error pages. A request to the / path returns a 404 response, which is intercepted by the status code middleware and converted to a 302 response. The browser makes a second request using the /404 path to generate the HTML response.
Whether you use the reexecute or redirect method, the browser ultimately receives essentially the same HTML. However, there are some important differences:
• With the reexecute approach, the original status code (such as a 404) is preserved. The browser sees the error page HTML as the response to the original request. If the user refreshes the page, the browser makes a second request for the original path.
• With the redirect approach, the original status code is lost. The browser treats the redirect and second request as two separate requests and doesn’t “know” about the error. If the user refreshes the page, the browser makes a request for the same error path; it doesn’t resend the original request.
In most cases, I find the reexecute approach to be more useful, as it preserves the original error and typically has the behavior that users expect. There may be some cases where the redirect approach is useful, however, such as when an entirely different application generates the error page.
Tip Favor using UseStatusCodePagesWithReExecute over the redirect approach when the same app is generating the error page HTML for your app.
You can use StatusCodePagesMiddleware in combination with other exception handling middleware by adding both to the pipeline. StatusCodePagesMiddleware modifies the response only if no response body has been written. So if another component, such as ExceptionHandlerMiddleware, returns a message body along with an error code, it won’t be modified.
NOTE StatusCodePagesMiddleware has additional overloads that let you execute custom middleware when an error occurs instead of reexecuting the middleware pipeline. You can read about this approach at http://mng.bz/0K66.
Error handling is essential when developing any web application; errors happen, and you need to handle them gracefully. The StatusCodePagesMiddleware is practically a must-have for any production Razor Pages app.
In chapter 16 we’ll dive into model binding. You’ll see how the route values generated during routing are bound to your page handler parameters, and perhaps more important, how to validate the values you’re provided.
15.6 Summary
A Razor Page page handler is the method in the Razor Page PageModel class that is executed when a Razor Page handles a request.
Page handlers should ensure that the incoming request is valid, call in to the appropriate domain services to handle the request, and then choose the kind of response to return. They typically don’t generate the response directly; instead, they describe how to generate the response.
Page handlers should generally delegate to services to handle the business logic required by a request instead of performing the changes themselves. This ensures a clean separation of concerns that aids testing and improves application structure.
When a Razor Page is executed, a single page handler is invoked based on the HTTP verb of the request and the value of the handler route value. If no page handler is found, an “implicit” handler is used instead, simply rendering the content of the Razor Page.
Page handlers can have parameters whose values are taken from properties of the incoming request in a process called model binding. Properties decorated with [BindProperty] can also be bound to the request. These are the canonical ways of reading values from the HTTP request inside your Razor Page.
By default, properties decorated with [BindProperty] are not bound for GET requests. To enable binding, use [BindProperty(SupportsGet = true)].
Page handlers can return a PageResult or void to generate an HTML response. The Razor Page infrastructure uses the associated Razor view to generate the HTML and returns a 200 OK response.
You can send users to a different Razor Page using a RedirectToPageResult. It’s common to send users to a new page as part of the POST-REDIRECT-GET flow for handling user input via forms
The PageModel base class exposes many helper methods for creating an IActionResult, such as Page() which creates a PageResult, and RedirectToPage() which creates a RedirectToPageResult. These methods are simple wrappers around calling new on the corresponding IActionResult type.
StatusCodePagesMiddleware lets you provide user-friendly custom error handling messages when the pipeline returns a raw error response status code. This is important for providing a consistent user experience when status code errors are returned, such as 404 errors when a URL is not matched to an endpoint.