blog post image
Andrew Lock avatar

Andrew Lock

~11 min read

Applying the MVC design pattern to Razor Pages

With the recent release of .NET 5.0, I'm hard at work updating the code and content of the second edition of my book ASP.NET Core in Action, Second Edition. This post gives you a sample of what you can find in the book. If you like what you see, please take a look - for now you can even get a 40% discount by entering the code bllock2 into the discount code box at checkout at manning.com. On top of that, you'll also get a copy of the first edition, free!

The Manning Early Access Program (MEAP) provides you full access to books as they are written, You get the chapters as they are produced, plus the finished eBook as soon as it’s ready, and the paper book long before it's in bookstores. You can also interact with the author (me!) on the forums to provide feedback as the book is being written. All of the chapters are currently available in MEAP, so now is the best time to grab it!

In this article we look in greater depth at how the MVC design pattern applies to Razor Pages in ASP.NET Core. This will also help clarify the role of various features of Razor Pages.

Applying the MVC design pattern to Razor Pages

If you’re reading this, you’re probably familiar with the MVC pattern as typically used in web applications; Razor Pages use this pattern. But ASP.NET Core also includes a framework called ASP.NET Core MVC. This framework (unsurprisingly) very closely mirrors the MVC design pattern, using controllers and action methods in place of Razor Pages and page handlers. Razor Pages builds directly on top of the underlying ASP.NET Core MVC framework, using the MVC framework “under the hood” for their behavior.

If you prefer, you can avoid Razor Pages entirely, and work with the MVC framework directly in ASP.NET Core. This was the only option in early versions of ASP.NET Core and the previous version of ASP.NET.

ASP.NET Core implements Razor Page endpoints using a combination of the EndpointRoutingMiddleware (often referred to simply as RoutingMiddleware) and EndpointMiddleware, as shown in figure 1. Once a request has been processed by earlier middleware (and assuming none of them handle the request and short-circuit the pipeline), the routing middleware will select which Razor Page handler should be executed, and the Endpoint middleware executes the page handler.

Image showing a middleware pipeline consisting of 4 middleware components.
Figure 1. The middleware pipeline for a typical ASP.NET Core application. The request is processed by each middleware in sequence. If the request reaches the routing middleware, the middleware selects an endpoint, such as a Razor Page, to execute. The endpoint middleware executes the selected endpoint.

Middleware often handles cross-cutting concerns or narrowly defined requests, such as requests for files. For requirements that fall outside of these functions, or that have many external dependencies, a more robust framework is required. Razor Pages (and/or ASP.NET Core MVC) can provide this framework, allowing interaction with your application’s core business logic, and the generation of a UI. It handles everything from mapping the request to an appropriate controller to generating the HTML or API response.

In the traditional description of the MVC design pattern, there’s only a single type of model, which holds all the non-UI data and behavior. The controller updates this model as appropriate and then passes it to the view, which uses it to generate a UI.

One of the problems when discussing MVC is the vague and ambiguous terms that it uses, such as “controller” and “model.” Model, in particular, is such an overloaded term that it’s often difficult to be sure exactly what it refers to—is it an object, a collection of objects, an abstract concept? Even ASP.NET Core uses the word “model” to describe several related, but different, components, as you’ll see shortly.

Directing A Request To A Razor Page And Building A Binding Model

The first step when your app receives a request is routing the request to an appropriate Razor Page handler. Let’s think about the category to-do list page again, from Listing 1 (repeated below).

public class CategoryModel : PageModel
{
    private readonly ToDoService _service;
    public CategoryModel(ToDoService service)
    {
        _service = service;
    }
 
    public ActionResult OnGet(string category)
    {
        Items = _service.GetItemsForCategory(category);
        return Page();
    }
 
    public List<ToDoListModel> Items { get; set; }
}
Listing 1. An example Razor Page for viewing all to-do items in a given category

On this page, you’re displaying a list of items that have a given category label. If you’re looking at the list of items with a category of “Simple,” you’d make a request to the /category/Simple path.

Routing takes the headers and path of the request, /category/Simple, and maps it against a preregistered list of patterns. These patterns match a path to a single Razor Page and page handler.

TIP I’m using the term Razor Page to refer to the combination of the Razor view and the PageModel that includes the page handler. Note that that PageModel class is not the “model” we’re referring to when describing the MVC pattern. It fulfills other roles, as you will see later in this section.

Once a page handler is selected, the binding model (if applicable) is generated. This model is built based on the incoming request, the properties of the PageModel marked for binding, and the method parameters required by the page handler, as shown in figure 2. A binding model is normally one or more standard C# objects, with properties that map to the requested data.

DEFINITION A binding model is one or more objects that act as a “container” for the data provided in a request that’s required by a page handler.

Image showing a request being routed to a Razor Page handler.
Figure 2. Routing a request to a controller and building a binding model. A request to the `/category/Simple` URL results in the `CategoryModel.OnGet` page handler being executed, passing in a populated binding model, category.

In this case, the binding model is a simple string, category, which is “bound” to the "Simple" value. This value is provided in the request URL’s path. A more complex binding model could also have been used, where multiple properties were populated.

This binding model in this case corresponds to the method parameter of the OnGet page handler. An instance of the Razor Page is created using its constructor, and the binding model is passed to the page handler when it executes, so it can be used to decide how to respond. For this example, the page handler uses it to decide which to-do items to display on the page.

Executing A Handler Using The Application Model

The role of the page handler as the controller in the MVC pattern is to coordinate the generation of a response to the request its handling. That means it should perform only a limited number of actions. In particular, it should

  • Validate that the data contained in the binding model provided is valid for the request.
  • Invoke the appropriate actions on the application model using services.
  • Select an appropriate response to generate based on the response from the application model.
Image showing a page handler calling methods in the application model
Figure 3. When executed, an action will invoke the appropriate methods in the application model.

Figure 3 shows the page handler invoking an appropriate method on the application model. Here, you can see that the application model is a somewhat abstract concept that encapsulates the remaining non-UI part of your application. It contains the domain model, a number of services, and the database interaction.

DEFINITION The domain model encapsulates complex business logic in a series of classes that don’t depend on any infrastructure and can be easily tested.

The page handler typically calls into a single point in the application model. In our example of viewing a to-do list category, the application model might use a variety of services to check whether the current user is allowed to view certain items, to search for items in the given category, to load the details from the database, or to load a picture associated with an item from a file.

Assuming the request is valid, the application model will return the required details to the page handler. It’s then up to the page handler to choose a response to generate.

Building Html Using The View Model

Once the page handler has called out to the application model that contains the application business logic, it’s time to generate a response. A view model captures the details necessary for the view to generate a response.

DEFINITION A view model is all the data required by the view to render a UI. It’s typically some transformation of the data contained in the application model, plus extra information required to render the page, for example the page’s title.

The term view model is used extensively in ASP.NET Core MVC, where it typically refers to a single object that is passed to the Razor view to render. However, with Razor Pages, the Razor view can access the Razor Page’s page model class directly. Therefore, the Razor Page PageModel typically acts as the view model in Razor Pages, with the data required by the Razor view exposed via properties, as you saw previously in Listing 1.

NOTE Razor Pages use the PageModel class itself as the view model for the Razor view, by exposing the required data as properties.

The Razor view uses the data exposed in the page model to generate the final HTML response. Finally, this is sent back through the middleware pipeline and out to the user’s browser, as shown in figure 4.

Passing the PageModel to a Razor View, which uses it to generate HTML
Figure 4. The page handler builds a view model by setting properties on the `PageModel`. It’s the view that generates the response.

It’s important to note that although the page handler selects whether to execute the view, and the data to use, it doesn’t control what HTML is generated. It’s the view itself that decides what the content of the response will be.

Do Razor Pages use MVC or MVVM?

Occasionally I’ve seen people describe Razor Pages as using the Model-View-View Model (MVVM) design pattern, rather than the MVC design pattern. Personally, I don’t agree, but it’s worth being aware of the differences.

MVVM is a UI pattern that is often used in mobile apps, desktop apps, and in some client-side frameworks. It differs from MVC in that there is a bi-directional interaction between the view and the view model. The view model tells the view what to display, but the view can also trigger changes directly on the view model. It’s often used with two-way databinding where a view model is “bound” to a view.

Some people consider the Razor Pages PageModel to be filling this role, but I’m not convinced. Razor Pages definitely seems based on the MVC pattern to me (it’s based on the ASP.NET Core MVC framework after all!) and doesn’t have the same “two-way binding” that I would expect with MVVM.

Putting It All Together: A Complete Razor Page Request

Now that you’ve seen each of the steps that goes into handling a request in ASP.NET Core using Razor Pages, let’s put it all together from request to response. Figure 5 shows how the steps combine to handle the request to display the list of to-do items for the “Simple” category. The traditional MVC pattern is still visible in Razor Pages, made up of the page handler (controller), the view, and the application model.

Routing a request to a page handler, calling into the domain model, and using a view to generate HTML
Figure 5 A complete Razor Pages request for the list of to-dos in the “Simple” category.

By now, you might be thinking this whole process seems rather convoluted—so many steps to display some HTML! Why not allow the application model to create the view directly, rather than having to go on a dance back and forth with the page handler method?

The key benefit throughout this process is the separation of concerns:

  • The view is responsible only for taking some data and generating HTML.
  • The application model is responsible only for executing the required business logic.
  • The page handler (controller) is responsible only for validating the incoming request and selecting which response is required, based on the output of the application model.

By having clearly defined boundaries, it’s easier to update and test each of the components without depending on any of the others. If your UI logic changes, you won’t necessarily have to modify any of your business logic classes, so you’re less likely to introduce errors in unexpected places.

The dangers of tight coupling

Generally speaking, it’s a good idea to reduce coupling between logically separate parts of your application as much as possible. This makes it easier to update your application without causing adverse effects or requiring modifications in seemingly unrelated areas. Applying the MVC pattern is one way to help with this goal.

As an example of when coupling rears its head, I remember a case a few years ago when I was working on a small web app. In our haste, we had not properly decoupled our business logic from our HTML generation code, but initially there were no obvious problems—the code worked, so we shipped it!

A few months later, someone new started working on the app, and immediately “helped” by renaming an innocuous spelling error in a class in the business layer. Unfortunately, the names of those classes had been used to generate our HTML code, so renaming the class caused the whole website to break in users’ browsers! Suffice it to say, we made a concerted effort to apply the MVC pattern after that, and ensure we had a proper separation of concerns.

The examples shown in this article demonstrate the bulk of the Razor Pages functionality. It has additional features, such as the filter pipeline, and more advanced behavior around binding models, but the overall behavior of the system is the same.

How the MVC design pattern applies when you’re generating machine-readable responses using Web API controllers is, for all intents and purposes, identical, apart from the final result generated (I discuss this in depth in the book).

Summary

That’s all for this article. If you want to see more of the book’s contents, you can preview them on our browser-based liveBook platform here. Don’t forget to save 40% with code bllock2 at manning.com.

Andrew Lock | .Net Escapades
Want an email when
there's new posts?