ASP.NET Razor Pages vs MVC: How Do Razor Pages Fit in Your Toolbox?

By: mwatson
  |  February 28, 2024
ASP.NET Razor Pages vs MVC: How Do Razor Pages Fit in Your Toolbox?

As part of the release of .NET Core 2.0, there are also some updates to ASP.NET. Among these is the addition of a new web framework for creating a “page” without the full complexity of ASP.NET MVC. New Razor Pages are a slimmer version of the MVC framework and, in some ways, an evolution of the old “.aspx” WebForms.

In this article, we are going to delve into some of the finer points of using ASP.NET Razor Pages versus MVC.

  • The basics of Razor Pages
  • ASP.NET MVVM vs MVC
  • Pros and cons of Razor Pages and MVC
  • Using Multiple GET or POST Actions via Handlers
  • Why you should use Razor Pages for everything
  • Code comparison of ASP.NET Razor Page vs. MVC
A Razor Page is very similar to the view component that ASP.NET MVC developers use. It has all the same syntax and functionality

The Basics: What are ASP.NET Razor Pages?

Razor Page is very similar to the view component that ASP.NET MVC developers use. It has all the same syntax and functionality.

The key difference is that the model and controller code are also included within the Razor Page. It is more of an MVVM (Model-View-ViewModel) framework. It enables two-way data binding and a more straightforward development experience with isolated concerns.

Here is a basic example of a Razor Page using inline code within a @functions block. It is actually recommended to put the PageModel code in a separate file. This is akin to how we did code behind files with ASP.NET WebForms.

@page
@model IndexModel
@using Microsoft.AspNetCore.Mvc.RazorPages
@functions {
    public class IndexModel : PageModel
    {
        public string Message { get; private set; } = "In page model: ";
        public void OnGet()
        {
            Message += $" Server seconds  { DateTime.Now.Second.ToString() }";
        }
    }
}
<h2>In page sample</h2>
<p>
    @Model.Message
</p>

We Have Two Choices Now: ASP.NET MVVM or MVC

You could say that we now choose an MVC or MVVM framework. I’m not going to go into all the details of MVC vs MVVM. This article does a good job of that with some examples. MVVM frameworks are most noted for two-way data binding of the data model.

MVC works well with apps with many dynamic server views, single-page apps, REST APIs, and AJAX calls. Razor Pages are perfect for simple pages that are read-only or do basic data input.

MVC has been all the rage recently for web applications across most programming languages. It definitely has its pros and cons. ASP.NET WebForms was designed as an MVVM solution. You could argue that Razor Pages are an evolution of the old WebForms.

Pros and Cons of Razor Pages

As someone who has been doing ASP.NET development for about 15 years, I am pretty conversant with all the ASP.NET frameworks. Based on my experimentation with the new Razor Pages, here are my views on the pros and cons and how I envisage using them.

Pro: More organized and less magical

I don’t know about you, but the first time I ever used ASP.NET MVC, I spent a lot of time figuring out how it worked. The naming of things and the dynamically created routes caused a lot of magic that I wasn’t used to. The fact that /Home/ goes to HomeController.Index() that loads a view file from “Views\Home\Index.cshtml” is a lot of magic to get comfortable with when starting.

Razor Pages don’t have any of that “magic” and the files are more organized. You have a Razor View and a code behind the file, just like WebForms did, versus MVC having separate files in different directories for the controller, view, and model.

Compare simple MVC and Razor Page projects. (Will show more code differences later in this article.)

Compare MVC vs Razor Page Files
Compare MVC vs Razor Page Files

Pro: Single Responsibility

If you have ever used an MVC framework before, you have likely seen some huge controller classes that are filled with many different actions. They are like a virus that grows over time as things get added.

With Razor Pages, each page is self-contained, with its view and code organized together. This follows the Single Responsibility Principle.

Con: Requires New Learning 

Since Razor Pages represent a new way of doing things, developers used to the MVC framework might have a learning curve to overcome.

Con: Limitations for Complex Scenarios

While Razor Pages are great for simple pages, there might be better choices for complex scenarios that require intricate routing, multiple views, or complex state management.

Pros and Cons of MVC

Pro: Flexibility

MVC is incredibly flexible and can accommodate a variety of scenarios. It works well for applications with many dynamic server views, single-page apps, REST APIs, and AJAX calls.

Pro: Familiarity

Since MVC has been the mainstay for web applications across most programming languages, many developers are already familiar with its structure and functioning.

Con: Complexity

Due to its flexibility, MVC can become quite complex, especially for beginners needing help understanding the interactions between the model, view, and controller.

Con: Risk of Bloated Controllers

With MVC, there’s a risk of having substantial controller classes filled with many different actions, making the code harder to maintain and reason with.

Using Multiple GET or POST Actions via Handlers

In a default setup, a Razor Page is designed to have a single OnGetAsync and OnPostAsync method. If you want to have different actions within your single page, you need to use a feature called a handler. If your page has AJAX callbacks, multiple possible form submissions, or other scenarios, you will need this.

So, for example, if you were using a Kendo grid and wanted the grid to load via an AJAX call, you would need to use a handler to handle that AJAX call back. Any single-page application would use a lot of handlers, or you should point all of those AJAX calls to an MVC controller.

I made an additional method called OnGetHelloWorldAsync() on my page. How do I invoke it?

From my research, there seem to be three different ways to use handlers:

  1. Querystring  – Example: “/managepage/2177/?handler=helloworld”
  2. Define as a route in your view: @page “{handler?}” and then use /helloworld in the url
  3. Define on your input submit button in your view. Example: <input type=”submit” asp-page-handler=”JoinList” value=”Join” />

You can learn more about multiple-page handlers here.

Special thanks to those who left comments about this! Article updated!

Using Pages would force a separation between how you load the page and what services the AJAX callbacks

Why you should use Razor Pages for everything! (maybe?)

One could argue that Razor Pages are the ideal solution to anything, essentially a web page within your app. It would draw a clear line in the sand that any HTML “pages” in your app are actual pages. Currently, an MVC action could return an HTML view, JSON, a file, or anything. Using Pages would force a separation between how you load the page and what services the AJAX callbacks.

Think about it. This solves a lot of problems with this forced separation.

Razor PageHTML ViewsMVC/Web APIREST API calls, SOA

This would prevent MVC controllers that contain tons of actions that are a mix of not only different “pages” in your app but also a mixture of AJAX callbacks and other functions.

Of course, I haven’t actually implemented this strategy yet. It could be terrible or brilliant. Only time will tell how the community ends up using Razor Pages.

Code Comparison of ASP.NET Razor Page vs MVC

While experimenting with Razor Pages, I built a straightforward form in both MVC and as a Razor Page. Let’s delve into a comparison of how the code looks in each case. It is just a text box with a submit button.

Here is my MVC view:

@model RazorPageTest.Models.PageClass
<form asp-action="ManagePage">
    <div class="form-horizontal">
        <h4>Client</h4>
        <hr />
        <div asp-validation-summary="ModelOnly" class="text-danger"></div>
        <input type="hidden" asp-for="PageDataID" />
        <div class="form-group">
            <label asp-for="Title" class="col-md-2 control-label"></label>
            <div class="col-md-10">
                <input asp-for="Title" class="form-control" />
                <span asp-validation-for="Title" class="text-danger"></span>
            </div>
        </div>
      
        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Save" class="btn btn-default" />
            </div>
        </div>
    </div>
</form>

Here is my MVC controller. (My model is PageClass which just has two properties and is really simple.)

   public class HomeController : Controller
    {
        public IConfiguration Configuration;
        public HomeController(IConfiguration config)
        {
            Configuration = config;
        }
        public async Task<IActionResult> ManagePage(int id)
        {
            PageClass page;
            using (var conn = new SqlConnection(Configuration.GetConnectionString("contentdb")))
            {
                await conn.OpenAsync();
                var pages = await conn.QueryAsync<PageClass>("select * FROM PageData Where PageDataID = @p1", new { p1 = id });
                page = pages.FirstOrDefault();
            }
            return View(page);
        }
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> ManagePage(int id, PageClass page)
        {
            if (ModelState.IsValid)
            {
                try
                {
                    //Save to the database
                    using (var conn = new SqlConnection(Configuration.GetConnectionString("contentdb")))
                    {
                        await conn.OpenAsync();
                        await conn.ExecuteAsync("UPDATE PageData SET Title = @Title WHERE PageDataID = @PageDataID", new { page.PageDataID, page.Title});
                    }
                }
                catch (Exception)
                {
                   //log it
                }
                return RedirectToAction("Index", "Home");
            }
            return View(page);
        }
    }

Now let’s compare that to my Razor Page.

My Razor Page:

@page "{id:int}"
@model RazorPageTest2.Pages.ManagePageModel
<form asp-action="ManagePage">
    <div class="form-horizontal">
        <h4>Manage Page</h4>
        <hr />
        <div asp-validation-summary="ModelOnly" class="text-danger"></div>
        <input type="hidden" asp-for="PageDataID" />
        <div class="form-group">
            <label asp-for="Title" class="col-md-2 control-label"></label>
            <div class="col-md-10">
                <input asp-for="Title" class="form-control" />
                <span asp-validation-for="Title" class="text-danger"></span>
            </div>
        </div>
        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Save" class="btn btn-default" />
            </div>
        </div>
    </div>
</form>

Here is my Razor PageModel, aka code behind:

   public class ManagePageModel : PageModel
    {
        public IConfiguration Configuration;
        public ManagePageModel(IConfiguration config)
        {
            Configuration = config;
        }
        [BindProperty]
        public int PageDataID { get; set; }
        [BindProperty]
        public string Title { get; set; } 
        public async Task<IActionResult> OnGetAsync(int id)
        {
            using (var conn = new SqlConnection(Configuration.GetConnectionString("contentdb")))
            {
                await conn.OpenAsync();
                var pages = await conn.QueryAsync("select * FROM PageData Where PageDataID = @p1", new { p1 = id });
                var page = pages.FirstOrDefault();
                this.Title = page.Title;
                this.PageDataID = page.PageDataID;
            }
            return Page();
        }
        public async Task<IActionResult> OnPostAsync(int id)
        {
            if (ModelState.IsValid)
            {
                try
                {
                    //Save to the database
                    using (var conn = new SqlConnection(Configuration.GetConnectionString("contentdb")))
                    {
                        await conn.OpenAsync();
                        await conn.ExecuteAsync("UPDATE PageData SET Title = @Title WHERE PageDataID = @PageDataID", new { PageDataID, Title });
                    }
                }
                catch (Exception)
                {
                   //log it
                }
                return RedirectToPage("/");
            }
            return Page();
        }
    }

Deciphering the comparison

The code between the two is nearly identical. Here are the key differences:

  • The MVC view part of the code is exactly the same except for the inclusion of “@page” in the Razor Page.
  • ManagePageModel has OnGetAsync and OnPostAsync, which replaced the two MVC controller “ManagePage” actions.
  • ManagePageModel includes my two properties that were in the separate PageClass before.

In MVC for an HTTP POST, you pass in your object to the MVC action (i.e., “ManagePage(int id, PageClass page)”). With a Razor Page, you are instead using two-way data binding. I annotated my two properties (PageDataID, Title) with [BindProperty] to get Razor Pages to work correctly with two-way data binding. My OnPostAsync method only has a single id input since the other properties are automatically bound.

Will it Prefix?

Do Razor Pages work with Prefix? Yes! Our free ASP.NET Profiler, Prefix, supports ASP.NET Razor Pages. Our Retrace and Prefix products have full support for ASP.NET Core.

Summary

I really like Razor Pages and can definitely see using them in an ASP.NET Core project I am working on. I like the idea of Razor Pages being the true pages in my app and implementing all AJAX/REST API functions with MVC. I’m sure there are also other use cases that Razor Pages don’t work for. The good news is MVC is super flexible, but that is what also makes it more complex. The true beauty of Razor Pages is their simplicity.

References:

Improve Your Code with Retrace APM

Stackify's APM tools are used by thousands of .NET, Java, PHP, Node.js, Python, & Ruby developers all over the world.
Explore Retrace's product features to learn more.

Learn More

Want to contribute to the Stackify blog?

If you would like to be a guest contributor to the Stackify blog please reach out to [email protected]