Razor Pages ASP.NET Core FAQ: 27 Beginner Questions

Razor Pages in ASP.NET Core: What This FAQ Covers

Razor Pages is a page-based way to build server-rendered web apps in ASP.NET Core. Each page is a .cshtml file for the markup, paired with a C# class called a PageModel that handles the page's requests. This Razor Pages ASP.NET Core FAQ answers 27 questions that tend to show up once you're past "hello world": why a handler isn't called, how to get a route parameter into a page, and whether a project should use Razor Pages or MVC. Each answer includes code you can adapt.

MVC (Model-View-Controller) is the pattern where controllers handle many views. Razor Pages ships as part of ASP.NET Core and organizes the same machinery around individual pages instead. The examples here work on .NET 8 and later. As of October 2026, .NET 10 is the current LTS (long-term support) release and the right choice for a new project, and Microsoft's support for both .NET 8 and .NET 9 ends on November 10, 2026. Project templates change between versions (newer templates, for example, may call MapStaticAssets() in Program.cs), so your generated files may look slightly different from the snippets.

Razor Page and PageModel Connection

You'll need basic C# and HTML, the .NET SDK, and a project you can run. Running dotnet new webapp creates a Razor Pages project. Some snippets assume an Entity Framework Core ApplicationDbContext and a Product class with Id and Name properties, so adjust those names to match your own project.

The questions are grouped into five areas: fundamentals and project structure, handlers and routing, model binding and validation, architecture and comparison with MVC, and security and testing. Skim to what you need or read straight through.

Fundamentals and Project Structure

1. What exactly is a Razor Page, and how is it different from a Razor view?

A Razor view (used in MVC) is just a template file that a controller renders. It has no code-behind of its own. A Razor Page is a self-contained unit: a .cshtml file with a @page directive at the top, usually paired with a PageModel class in a matching .cshtml.cs file. The page handles its own request logic instead of relying on an external controller.

2. What does the @page directive actually do?

It's what turns a .cshtml file into a routable, independently requestable Razor Page instead of a partial view or template fragment. @page must be the first directive in the file. Without it, ASP.NET Core treats the file as an ordinary Razor view, and it won't be reachable by its own URL.

csharp
@page
@model Codingvila.Pages.ContactModel

<h1>Contact Codingvila</h1>

Expected result: browsing to /Contact shows the heading. Remove @page and the same URL returns a 404.

3. What's the standard project structure for Razor Pages?

By convention, pages live in a Pages folder at the project root. A small site with a home page and a contact page looks like this:

code
Program.cs
/wwwroot
/Pages
  Index.cshtml
  Index.cshtml.cs
  Contact.cshtml
  Contact.cshtml.cs
  _ViewImports.cshtml
  _ViewStart.cshtml
  /Shared
    _Layout.cshtml

The wwwroot folder holds static files such as CSS, JavaScript, and images. Three special files sit in Pages. _ViewImports.cshtml holds directives shared by every page below it, such as @using, @namespace, and @addTagHelper. _ViewStart.cshtml runs before each page and usually just sets the layout. _Layout.cshtml is the shared page shell (header, navigation, footer) that wraps your pages.

Matching file names (Contact.cshtml and Contact.cshtml.cs) are a convention that helps tools like Visual Studio nest the two files together and helps humans find them. They are not what connects the page to its PageModel. That connection is the @model directive, covered in question 6. The only wiring in Program.cs is registering Razor Pages and mapping its endpoints.

4. How do I register Razor Pages in Program.cs?

csharp
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

var app = builder.Build();

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();

app.MapRazorPages();

app.Run();

AddRazorPages() registers the services needed for page discovery, model binding, and Tag Helpers (server-side components that let attributes such as asp-for and asp-page shape the HTML your page produces). MapRazorPages() wires up the routing endpoints based on the file structure under Pages.

Run dotnet run and open the URL printed in the console. The home page should load.

5. What is a PageModel, and is it required?

PageModel is a base class your code-behind class inherits from. It gives you access to things like ModelState, Request, User, and the Page() and RedirectToPage() helper methods. Technically you can write a Razor Page without a PageModel by using @functions blocks directly in the .cshtml. In any real project you'll want the PageModel, because it keeps logic testable and out of the markup.

csharp
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace Codingvila.Pages;

public class ContactModel : PageModel
{
    [BindProperty]
    public string Email { get; set; } = string.Empty;

    public void OnGet()
    {
    }
}

The two using lines matter. The default web project's implicit usings don't include Microsoft.AspNetCore.Mvc.RazorPages, so PageModel won't compile without it. The namespace line matters too, because the type it produces (Codingvila.Pages.ContactModel) has to be the one the page's @model directive resolves to (see question 6). The project templates generate the namespace for you, so keep it if you copy this class into your own project.

6. How does the PageModel connect to its .cshtml file?

Through the @model directive, which names the PageModel's type. That name has to resolve to a real class. You can write it fully qualified, as below:

csharp
@page
@model Codingvila.Pages.ContactModel

Or you can use the short name, which is what the project templates do:

csharp
@page
@model ContactModel

The short form works because the template's _ViewImports.cshtml normally contains an @namespace line that matches the namespace of your pages. If you move a page into another folder or rename a namespace, the short name can stop resolving.

If the name doesn't resolve, or the PageModel class isn't public, the build fails with an error pointing at the view. A wrong or missing namespace is the most common cause.

Handlers and Routing

7. What are OnGet and OnPost, and why do they matter?

These are handler methods, the Razor Pages equivalent of controller action methods. OnGet runs when the page receives an HTTP GET request, and OnPost runs on POST. Other verbs follow the same pattern (OnPut, OnDelete), though browser forms can only send GET and POST, so those handlers are called from JavaScript. Both OnGet and OnPost have asynchronous equivalents, OnGetAsync and OnPostAsync, which you should prefer whenever the handler does I/O such as a database call. Pick one or the other for each verb: a page can't have both OnGet and OnGetAsync, because the framework treats them as the same handler. A handler can return void, IActionResult, or the Task versions of those.

The example below receives its database context through the constructor. This is dependency injection: ASP.NET Core creates the context and passes it in, as long as you registered it in Program.cs with AddDbContext (for example with UseSqlServer or UseSqlite). The AsNoTracking() call tells Entity Framework Core not to track these rows for changes, which saves work when the page only displays them.

csharp
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;

public class ProductsModel : PageModel
{
    private readonly ApplicationDbContext _context;

    public ProductsModel(ApplicationDbContext context)
    {
        _context = context;
    }

    public List<Product> Products { get; set; } = new();

    public async Task OnGetAsync()
    {
        Products = await _context.Products
            .AsNoTracking()
            .ToListAsync();
    }
}

The diagram below shows a simplified view of what happens to a request before and after your handler runs. Routing picks the page, the framework picks a handler by HTTP method and optional handler name, model binding fills your properties and parameters, and then your code runs and returns a result.

Razor Pages request flow A flowchart read from top to bottom. The browser sends a request. Routing maps the URL to a page. The framework picks a handler such as OnGet or OnPost. Model binding runs, then your handler runs. A decision asks which result the handler returns. If it returns Page(), the cshtml file is rendered and HTML is sent back. If it returns RedirectToPage(), a redirect response is sent to the new URL. Browser sends a request Routing maps the URL to a page Pick a handler (OnGet, OnPost) Model binding, then handler runs Which result does it return? Render the .cshtml and send HTML Send a redirect to the new URL Page() RedirectToPage()
Request flow in Razor Pages (simplified): routing picks the page, a handler is selected and runs after model binding, and its result either renders the page or redirects. Handlers can return other results too, such as NotFound() or JSON.

8. How do named handlers (OnPostDelete, OnPostPublish) work, and when should I use them?

A single page can have multiple POST handlers distinguished by a suffix after OnPost, for example OnPostDeleteAsync. The handler name is what's left after you remove OnPost and Async. You select which one runs by adding asp-page-handler="Delete" to the form. The Tag Helper puts a handler value in the request URL (for example ?handler=Delete), and the framework matches it to the method. This matters once a page has more than one meaningful action, like a product edit page with both "Save" and "Delete" buttons, without splitting it into two pages.

Start with the page's markup. The route template {id:int} makes the product's Id part of the URL, so /EditProduct/42 edits product 42.

csharp
@page "{id:int}"
@model Codingvila.Pages.EditProductModel
csharp
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;

namespace Codingvila.Pages;

public class EditProductModel : PageModel
{
    private readonly ApplicationDbContext _context;

    public EditProductModel(ApplicationDbContext context)
    {
        _context = context;
    }

    [BindProperty]
    public Product Product { get; set; } = new();

    public async Task<IActionResult> OnGetAsync(int id)
    {
        var product = await _context.Products.FindAsync(id);

        if (product is null)
        {
            return NotFound();
        }

        Product = product;
        return Page();
    }

    public async Task<IActionResult> OnPostSaveAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        _context.Attach(Product).State = EntityState.Modified;
        await _context.SaveChangesAsync();
        return RedirectToPage("./Index");
    }

    public async Task<IActionResult> OnPostDeleteAsync(int id)
    {
        var product = await _context.Products.FindAsync(id);

        if (product is not null)
        {
            _context.Products.Remove(product);
            await _context.SaveChangesAsync();
        }

        return RedirectToPage("./Index");
    }
}
html
<form method="post" asp-page-handler="Save">
    <input type="hidden" asp-for="Product.Id" />
    <input asp-for="Product.Name" />
    <button type="submit">Save</button>
</form>

<form method="post" asp-page-handler="Delete" asp-route-id="@Model.Product.Id">
    <button type="submit">Delete</button>
</form>

Expected result: opening /EditProduct/42 loads the product into the form. Clicking "Save" runs OnPostSaveAsync. Clicking "Delete" posts to /EditProduct/42?handler=Delete, runs OnPostDeleteAsync, and redirects to the index page. The hidden Product.Id field matters: without it, the Save handler receives a product with Id 0 and can't update the right row.

A common mistake is leaving off asp-page-handler. The request then goes to the plain OnPost or OnPostAsync method if one exists. If it doesn't, the exact outcome depends on your version and setup, so test it and check the ASP.NET Core docs for your version instead of assuming a specific HTTP status code.

One more caution: binding a whole entity like Product with [BindProperty] is fine for a demo, but it lets a client submit values for every property on that entity (over-posting). In production, bind a small input class that holds only the fields the form should change, or call TryUpdateModelAsync with an explicit list of properties. Also remember that a Delete handler like this one lets anyone who can reach the page delete any row, so protect it with authorization (questions 13 and 26).

9. What's the default routing convention for Razor Pages?

The route is derived from the file's path relative to the Pages folder. Pages/Products/Edit.cshtml maps to /Products/Edit, and Pages/Admin/Users/Index.cshtml maps to /Admin/Users. A file named Index.cshtml is treated specially: its segment can be dropped from the route, the same way MVC treats an Index action. So Pages/Index.cshtml answers at / (and also at /Index).

10. Can I customize the route for a specific page?

Yes, by adding a route template directly to the @page directive. This is the Razor Pages equivalent of attribute routing on a controller action. A template that starts with / replaces the file-based route entirely. A template without the leading slash is appended to the page's file-based route.

csharp
@page "/catalog/{category}/{id:int}"
@model Codingvila.Pages.Catalog.DetailsModel
csharp
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace Codingvila.Pages.Catalog;

public class DetailsModel : PageModel
{
    public void OnGet(string category, int id)
    {
        // category and id are bound from the route
    }
}

The {id:int} segment uses a route constraint, which restricts that segment to integers. If a non-numeric value is supplied, the route doesn't match and the request returns a 404. This is the same constraint syntax used in MVC attribute routing.

Expected result: /catalog/shoes/42 reaches OnGet with category set to shoes and id set to 42, while /catalog/shoes/abc returns a 404. Don't try to set routes with attributes like [HttpGet("/some-url")] on handler methods. Handlers are chosen after routing finishes, so routes belong in @page or in conventions.

11. How do route parameters get into my handler method?

ASP.NET Core model binding matches route values, query string values, and form values to parameter names on your handler method, or to properties on your PageModel marked with [BindProperty]. Names are matched case-insensitively, but otherwise they must match exactly. For simple GET scenarios, method parameters are usually cleaner than properties. If the @page template has no {id} segment, id is read from the query string instead (?id=7).

csharp
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

public class ProductDetailsModel : PageModel
{
    // _context is injected through the constructor, as in question 7
    public Product? Product { get; set; }

    public async Task<IActionResult> OnGetAsync(int id)
    {
        Product = await _context.Products.FindAsync(id);

        if (Product is null)
        {
            return NotFound();
        }

        return Page();
    }
}

12. What's the difference between [BindProperty] and a plain method parameter?

A method parameter is bound only for that specific handler call and discarded afterward. A property marked with [BindProperty] is bound automatically on non-GET requests such as POST (by default) and is accessible throughout the PageModel instance, including from the Razor markup itself. If you need the value on GET too, you must explicitly set SupportsGet = true. To bind every public property on a PageModel instead of marking them one by one, put [BindProperties] on the class (it accepts SupportsGet as well), but be sure you really want all of them exposed to the client.

csharp
[BindProperty(SupportsGet = true)]
public string? SearchTerm { get; set; }

public void OnGet()
{
    // SearchTerm is populated from the query string here
}

Leaving SupportsGet off and expecting a [BindProperty] value to populate on a GET request is one of the more common sources of "why is this null" bugs in Razor Pages.

13. How do folders affect routes, and how do I apply rules to a whole folder?

Nested folders under Pages become nested URL segments automatically, with no extra configuration. To apply a rule to every page in a folder, such as requiring authorization, or to give a page an extra URL, use conventions in AddRazorPages(options => options.Conventions...) instead of editing each page.

csharp
builder.Services.AddRazorPages(options =>
{
    options.Conventions.AuthorizeFolder("/Admin");
    options.Conventions.AddPageRoute("/Catalog/Index", "/shop");
});

AuthorizeFolder applies an authorization requirement to every page under that folder without decorating each PageModel individually. AddPageRoute adds an additional route to an existing page without moving its file. The original file-based route still works, but links that ASP.NET Core generates for that page (for example with asp-page) will use /shop.

Note that AuthorizeFolder only does something useful once authentication is set up in your app (for example, ASP.NET Core Identity plus app.UseAuthentication() before app.UseAuthorization()). Without it, a request to a protected page typically fails at runtime with an error about a missing authentication scheme instead of sending the visitor to a login page. The Program.cs in question 4 doesn't include authentication.

14. Does Razor Pages support Areas?

Yes. Areas partition a large app into feature modules, each with its own Pages, Controllers, and Views folders. They work with Razor Pages the same way they do with MVC, under an Areas/{AreaName}/Pages folder structure. You reference them with asp-area in Tag Helpers, or with an area route value in RedirectToPage calls.

html
<a asp-area="Admin" asp-page="/Dashboard/Index">Admin Dashboard</a>

Areas are worth it once a Razor Pages app grows past a handful of features, since they keep related pages grouped instead of one sprawling Pages folder.

Model Binding and Validation

15. How do data annotations work for validation in Razor Pages?

The same System.ComponentModel.DataAnnotations attributes used in MVC work directly on PageModel properties bound with [BindProperty]. ModelState.IsValid reflects the result after model binding runs. With nullable reference types enabled, a non-nullable string property is also treated as required by default.

csharp
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

public class RegisterModel : PageModel
{
    [BindProperty]
    [Required]
    [EmailAddress]
    public string Email { get; set; } = string.Empty;

    [BindProperty]
    [Required]
    [StringLength(100, MinimumLength = 8)]
    public string Password { get; set; } = string.Empty;

    public IActionResult OnPost()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        // registration logic
        return RedirectToPage("./Confirmation");
    }
}

This example only demonstrates validation. In a real app, never store the password as plain text. A library such as ASP.NET Core Identity handles hashing for you.

16. How do I display validation errors in the view?

Use the asp-validation-for and asp-validation-summary Tag Helpers, and include the client-side validation scripts through the page's Scripts section. The default layout renders that section, and the project template provides the _ValidationScriptsPartial file. The summary below uses ModelOnly, so it shows only errors that don't belong to a single field. With All, every field error would appear twice: once under its field and again in the summary.

html
<form method="post">
    <label asp-for="Email"></label>
    <input asp-for="Email" class="form-control" />
    <span asp-validation-for="Email" class="text-danger"></span>

    <label asp-for="Password"></label>
    <input asp-for="Password" type="password" class="form-control" />
    <span asp-validation-for="Password" class="text-danger"></span>

    <div asp-validation-summary="ModelOnly" class="text-danger"></div>

    <button type="submit">Register</button>
</form>

@section Scripts {
    <partial name="_ValidationScriptsPartial" />
}

Expected result: submitting the form with an empty email shows the error message under the field without a full page reload. If the scripts aren't loaded, the same message appears after the server round trip instead.

17. How do client-side and server-side validation work together?

The jQuery Validation scripts included via _ValidationScriptsPartial read the same data annotation metadata, which the Tag Helpers render into data-val-* HTML attributes. Client-side checks run first for instant feedback. Server-side validation through ModelState.IsValid always runs regardless, because client-side checks can be bypassed with disabled JavaScript or a hand-built request sent from a tool like curl or Postman. Treat client-side validation as a usability layer on top of the server-side check, never a replacement for it.

18. Can I write a custom validation attribute?

Yes, by inheriting from ValidationAttribute and overriding IsValid. This works identically in Razor Pages and MVC since it's part of the shared data annotations infrastructure (you need the same using System.ComponentModel.DataAnnotations; line as in question 15).

csharp
public class NoBlockedWordsAttribute : ValidationAttribute
{
    private static readonly string[] BlockedWords = { "spam", "scam" };

    protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
    {
        if (value is string text &&
            BlockedWords.Any(word => text.Contains(word, StringComparison.OrdinalIgnoreCase)))
        {
            return new ValidationResult("This field contains disallowed content.");
        }

        return ValidationResult.Success;
    }
}

Apply it to a property the same way as a built-in attribute: [NoBlockedWords] above a Comment property, for example. A custom attribute like this runs on the server only. Getting the same check in the browser takes extra client-side work, so keep relying on ModelState.IsValid. Also note that Contains matches substrings, so this filter will flag any word that merely includes a blocked string. Real content filters need more care than this demo.

19. How do I validate and submit a form with AJAX instead of a full page post?

You still define a handler, usually a named POST handler that returns JSON instead of Page(), and submit with fetch or a similar client-side call. The name matters: ?handler=Comment targets a method named OnPostCommentAsync. Because you're building the request yourself, you also have to send the anti-forgery token yourself. The page needs a hidden token field to read it from. A form that uses the form Tag Helper adds one, or you can call @Html.AntiForgeryToken() in the markup.

Send the token in a request header. The default header name is RequestVerificationToken in current ASP.NET Core versions, which is what the example below uses. You only need to set AntiforgeryOptions.HeaderName in Program.cs if you want a different name.

csharp
public async Task<IActionResult> OnPostCommentAsync([FromBody] CommentDto dto)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }

    await _commentService.AddAsync(dto);
    return new JsonResult(new { success = true });
}
javascript
const token = document.querySelector('input[name="__RequestVerificationToken"]').value;

async function submitComment(dto) {
    const response = await fetch('/Articles/Details?handler=Comment', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'RequestVerificationToken': token
        },
        body: JSON.stringify(dto)
    });

    if (!response.ok) {
        const errors = await response.json();
        console.error(errors);
        return;
    }

    const result = await response.json();
}

The _commentService field is assumed to be injected through the constructor, and /Articles/Details stands in for your own page URL. The script must run after the token field exists in the page. If the field is missing, querySelector returns null and the script throws. If the token is missing or wrong, the server rejects the request, typically with a 400. The ?handler=Comment query string is how you target a named handler from a raw HTTP request. The asp-page-handler Tag Helper does the same job, but only in markup rendered on the server.

20. What is ViewData, and how is it different from PageModel properties?

ViewData is a loosely typed dictionary (IDictionary<string, object>, essentially) for passing small, incidental bits of data from a handler to the page, most often things like a page title. PageModel properties are strongly typed and are the preferred way to pass your actual data model to the view, since they give you compile-time checking and IntelliSense that ViewData can't. In practice, reach for ViewData["Title"] for layout-level metadata and use PageModel properties for everything that's part of the page's data.

csharp
public void OnGet()
{
    ViewData["Title"] = "Product Catalog — Codingvila";
    Products = GetProducts();
}

Here GetProducts() stands in for whatever method loads your data.

Razor Pages vs MVC: Architecture and When to Choose Which

21. What's the real difference between Razor Pages and MVC, beyond file layout?

MVC centers on controllers, where a single controller class often handles several related actions and returns different views. Razor Pages centers on the page itself. Each page is its own small unit with its own handlers, which keeps page-specific logic from leaking into a shared controller that handles five unrelated views. Functionally they sit on the same foundation: both use the same routing engine, the same model binding, the same Tag Helpers, and the same validation pipeline. The difference is organizational, not a difference in underlying capability.

Aspect Razor Pages MVC
Basic unit A page: .cshtml file plus PageModel A controller with actions, plus separate views
Request code Handler methods such as OnGet and OnPostAsync Action methods on a controller
Default routing Based on the file path under Pages, plus an optional @page template Conventional routes or attribute routing
Shared foundation Same model binding, Tag Helpers, and validation

22. When should I choose Razor Pages over MVC for a new project?

Razor Pages tends to fit content-and-forms-driven applications well: admin panels, internal tools, CRUD-heavy (create/read/update/delete) line-of-business apps, and sites where most pages map cleanly to one URL and one primary action. For many internal business apps, Razor Pages ends up with a flatter, easier-to-navigate codebase than MVC, because you're not hunting through a controller with a dozen actions to find the one tied to a specific view. That is a common view among developers rather than a measured result, so weigh it against your team's habits.

MVC tends to make more sense when you have a genuinely shared-logic surface: multiple views rendered by overlapping logic, complex action filters applied across many endpoints, or a team that's standardized on it for consistency with existing controllers. If the app is mostly an API with little to no server-rendered HTML, use minimal APIs or controller-based Web API instead of either UI approach. If you need a highly interactive UI, Blazor is Microsoft's component-based alternative, and it's a separate topic from this FAQ.

23. Can Razor Pages and MVC controllers coexist in the same ASP.NET Core project?

Yes, and this is more common than people expect. Register both sets of services and map both sets of endpoints in the same Program.cs. Use AddControllers() if your controllers only return data such as JSON, or AddControllersWithViews() if they also render Razor views. A typical real-world case is a Razor Pages site for the user-facing UI with a handful of controllers or minimal API endpoints exposing JSON for AJAX calls or a mobile client.

csharp
builder.Services.AddRazorPages();
builder.Services.AddControllers();

var app = builder.Build();

app.MapRazorPages();
app.MapControllers();

24. How do I migrate an existing MVC view/controller pair to a Razor Page?

Don't expect a one-click converter. Plan on doing it by hand, though the work is fairly mechanical. Move the markup from the .cshtml view into a new file under Pages, add the @page directive at the top, and move the controller action's logic into OnGet and OnPost handler methods on a new PageModel class. return View() becomes return Page(), and RedirectToAction becomes RedirectToPage. Links that used asp-controller and asp-action need to switch to asp-page.

In the markup, Model now refers to the PageModel itself. If your MVC view used @Model.Name on a view model, expose that data as a property such as Product and write @Model.Product.Name instead.

Pay close attention to any [HttpPost], [ValidateAntiForgeryToken], or model binding attributes on the original action. They map to handler naming conventions, automatic anti-forgery validation, and [BindProperty] in Razor Pages rather than being copied verbatim, since the mechanisms are similar but not identical in syntax.

Security and Testing

25. How do anti-forgery tokens work in Razor Pages?

Anti-forgery protection is a defense against cross-site request forgery (CSRF) attacks, where a malicious site tricks a browser into submitting a request on your behalf. In Razor Pages, the form Tag Helper adds the token to forms that use <form method="post">, and token validation is applied automatically to POST and other non-GET requests. You don't need to add [ValidateAntiForgeryToken] manually the way you often do in MVC. The flip side is that a hand-built form or AJAX call that doesn't send the token will be rejected (see question 19 for the AJAX version).

One catch: the Tag Helper skips the hidden token field if you set a plain action attribute on the form yourself. Let asp-page or asp-page-handler build the URL when you can, or add asp-antiforgery="true" to the form to force the token.

26. How do I require authorization on a Razor Page?

You can decorate a PageModel class with [Authorize] (from the Microsoft.AspNetCore.Authorization namespace) just like a controller, or apply it at the folder level as shown in question 13 with AuthorizeFolder. Put the attribute on the class, not on individual handler methods, because Razor Pages doesn't support it on handlers. Folder-level authorization is usually the better choice for an admin section, since it's one line instead of repeating the attribute on every page.

csharp
[Authorize(Roles = "Admin")]
public class DeleteUserModel : PageModel
{
    // _userService is injected through the constructor (omitted here)
    public async Task<IActionResult> OnPostAsync(int id)
    {
        await _userService.DeleteAsync(id);
        return RedirectToPage("./Index");
    }
}

Authorization decides who can reach a page. It doesn't decide which records they may touch, so if users should only edit their own data, check ownership inside the handler as well.

27. How do I unit test a PageModel?

PageModel classes are ordinary C# classes and can be unit tested directly without spinning up a web server, as long as you keep business logic out of the markup and inject dependencies through the constructor so they can be mocked. The example below tests the RegisterModel from question 15. Depending on which members your handler touches, a PageModel may need a PageContext outside a real request, so this example sets one up.

csharp
// Needs the xUnit package plus these usings: Xunit, Microsoft.AspNetCore.Http,
// Microsoft.AspNetCore.Mvc, Microsoft.AspNetCore.Mvc.ModelBinding,
// Microsoft.AspNetCore.Mvc.RazorPages, Microsoft.AspNetCore.Mvc.ViewFeatures,
// and Microsoft.AspNetCore.Routing.
[Fact]
public void OnPost_WithInvalidModel_ReturnsPage()
{
    var modelState = new ModelStateDictionary();
    var actionContext = new ActionContext(
        new DefaultHttpContext(),
        new RouteData(),
        new PageActionDescriptor(),
        modelState);
    var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider(), modelState);

    var model = new RegisterModel
    {
        PageContext = new PageContext(actionContext) { ViewData = viewData },
        Email = string.Empty,
        Password = "short"
    };

    // Model binding doesn't run in a unit test, so add the error yourself.
    model.ModelState.AddModelError("Email", "Email is required");

    var result = model.OnPost();

    Assert.IsType<PageResult>(result);
}

One limitation worth flagging: constructing a PageModel directly in a unit test bypasses the real model binding and routing pipeline, so this kind of test verifies your handler's logic, not the full HTTP request/response cycle. For that level of coverage, an integration test using WebApplicationFactory is the more reliable approach, though that's a broader topic than this FAQ can cover in full.

Common Errors and How to Fix Them

  • The page returns a 404. Check that @page is the first directive, that the file is under Pages, and that any route constraint (like {id:int}) matches the URL you're requesting.
  • A [BindProperty] value is null on GET. Add SupportsGet = true (question 12).
  • A POST returns a 400. The anti-forgery token is probably missing. Use the form Tag Helper, or send the token in a header for AJAX calls (questions 19 and 25).
  • The wrong handler runs, or none does. The handler name is the method name minus OnPost and Async, and it must match asp-page-handler or ?handler= (question 8).
  • An error about ambiguous or multiple handlers. A page can't define both OnGet and OnGetAsync (or both OnPost and OnPostAsync). Keep one of each (question 7).
  • A build error mentions @model. Make sure the PageModel class is public and that the name in @model resolves, either fully qualified or through the namespace in _ViewImports.cshtml (question 6).
  • The app fails to start with a missing-service error. A constructor parameter such as ApplicationDbContext must be registered in Program.cs before builder.Build() (question 7).
  • A protected page throws an authentication-scheme error instead of showing a login page. AuthorizeFolder or [Authorize] is in place, but authentication isn't configured (questions 13 and 26).
  • A route attribute on a handler is ignored or flagged. Set routes with @page or conventions, not attributes like [HttpGet("/url")] (question 10).

Summary

Razor Pages in ASP.NET Core gives you the same routing, model binding, and validation pipeline as MVC, organized around individual pages instead of shared controllers. Once you know three things, most of the "why isn't this working" confusion goes away. The first is that @model is what links a page to its PageModel. The second is how handler names are matched. The third is that [BindProperty] only populates on non-GET requests unless you opt in. Add the anti-forgery rules, folder-level authorization, and a clear sense of when MVC fits better, and you can make most architecture decisions with confidence. For a next step, build a small CRUD feature end to end (list, create, edit, delete) using named handlers on a single page. That one exercise runs into most of the routing and binding edge cases covered here.

Codingvila provides articles and blogs on web and software development for beginners as well as free Academic projects for final year students in Asp.Net, MVC, C#, Vb.Net, SQL Server, Angular Js, Android, PHP, Java, Python, Desktop Software Application and etc.

If you have any questions, contact us on info.codingvila@gmail.com