In a previous post, we discussed the implementation details for an admission management application written in ASP.NET Core MVC. This post describes how we migrated that application from ASP.NET Core MVC to Razor Pages and how we re-architected it for improved maintainability. The rest of the technologies used remain the same as in the original MVC application.

Our motivation for this migration was to take advantage of the increased structure Pages provides as compared to MVC. Even though both frameworks are based on the same core principles, MVC allows for a more liberal layout, which, unless the developers apply their own strict conventions, can lead to quite convoluted applications over time. Mainly, this is related to the flexibility of a controller scope and the views it can serve via its endpoints, leaving the question of how to group web pages and functionality within controllers up to the developers to answer.

On the other hand, Pages enforces a strict structure, whereby each controller is responsible for a single page, while providing some useful tools and conventions for common page functionality (e.g. methods for getting and posting a page, view model binding etc.). While Pages opinionated stance on controller contents may seem restrictive, it is very much aligned with server-based web applications, which are centered around the concept of a page and, thus, it feels much more natural than MVC. Flexibility and component reusability can still be achieved (albeit with some extra work) using partial views, shared view models and shared injected services.

Overall, we realized that MVC feels more like a Rest API that can also serve html pages, while Pages feels like a web application that can also include JSON endpoints. Yes, the difference is subtle, and you can do everything with either framework, but our conclusion from this migration was that Pages is a better fit for server-based applications and worth the little extra work necessary to achieve some of the MVC flexibility.

Technical highlights: Before the migration, the application used Areas to distinguish between outward-facing and inward-facing interfaces, for external users and organization employees respectively. We dropped the use of Areas and replaced it with an appropriate folder structure in conjunction with folder authorization conventions, greatly simplifying the application folder structure. Indeed, the concept of Areas does not seem to add any value to a Pages application. The External and Internal Pages folders contain the respective pages, while the Shared folder contains views and partial views used by both external and internal pages.

Folder structure
  • Pages automatically bind properties marked as BindProperty in the PageModel from post data when receiving a request and to view data when responding to a request. Having all properties flat in the PageModel does not allow for reusing a ViewModel, so we resorted to creating a view model bind property and referencing the view model inner properties in the relevant views. In doing so, we had to live with having to repeat the PageModel property name for every form field (such as in asp-for=”@Model.ViewModelProperty.FirstName”), as there did not seem to be a way to bind view data to a property of the PageModel.
  • In the original application, a controller served multiple pages which happened to share some common functionality, not required in other controllers. This functionality, which could involve model validation, database access via services in a lower-level project, and view rendering/returning, used to be implemented as private methods in the controller. After migrating to Pages, such common functionality (not related to database access) was moved to services in the top-level project (the one containing the controllers), which were injected in the appropriate controllers.
  • As described in the original post, the application consists of three projects: The top-level application, containing the application and related code, the Services, containing the database access and business logic services, and the Entities (renamed to Infrastructure during migration), containing the domain entities, database context and migrations. Before the migration, services in the Services project exchanged domain entities with the application project, leaving mapping between entities and view models in the hands of the application. However, it became clear during migration that exchanging view models instead of domain entities would better facilitate code reuse and heavily simplify the code in the application project, especially when non-trivial mapping between entities and view models was needed. Thus, we pushed view model classes to the Services project and modified services so that they map domain entities to view models internally and exchange view models instead of entities with the application project. Though this architectural modification is not strictly related to the Pages migration, it was the new application structure in Pages that highlighted the need to push view models down the services layer.