| description | Razor Pages component and application patterns |
|---|---|
| applyTo | **/*.cshtml, **/*.cshtml.cs |
- Write idiomatic, efficient Razor Pages and C#.
- Stick to the conventions the framework is built around: handler-based PageModels, not MVC controller patterns shoehorned into pages.
- Keep PageModels focused on request/response orchestration; business logic belongs in injected domain services.
- Trivial handlers can stay inline. For pages with lots of handlers and dependencies, reach for a mediator like MediatR.
- Use async/await end-to-end so handlers don't block the request pipeline.
- PascalCase for PageModel classes, handler methods, and public members (
CreateModel,OnPostAsync,OnPostDeleteAsync). - camelCase for private fields and locals, with the
_prefix on private fields per the .NET convention (_context,_logger). - Interface names start with "I" (
IEmailService). - Named handlers drop the
OnPost/Asyncaffixes when routed.OnPostJoinListAsyncis reached ashandler=JoinList.
- Don't put
[BindProperty]on EF or domain entities directly. An attacker can post extra fields likeIsAdminorSecretand the binder will happily set them, even if the form doesn't render them. - Bind to a dedicated Input Model or View Model that exposes only the properties the page is allowed to accept, then map to the entity.
TryUpdateModelAsync<T>with an explicit allow-list of properties is another option, especially in edit scenarios.- Avoid
[Bind]for edits. Excluded properties get reset todefault(T)rather than left alone, which is rarely what you want. Prefer Input Models. - Don't enable
[BindProperty(SupportsGet = true)]broadly. Razor Pages skips GET binding by default for a reason; opt in per-property and validate what comes in. - For custom types (including strongly-typed IDs), implement
TryParseor aTypeConverterso they bind from route and query values. Without one, the binder treats them as complex types and binding silently fails. One of those bugs that wastes an afternoon. [BindRequired]and[Required]aren't the same thing.[BindRequired]errors when the source value is absent from the posted form;[Required]validates that the bound value isn't null/empty.[BindRequired]only applies to form binding, since JSON and XML go through input formatters instead.
- Always use Post-Redirect-Get on successful POSTs. Return
RedirectToPage("./Index"), neverPage(). ReturningPage()on success means a browser refresh resubmits the form.
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid) return Page(); // re-render on error
await _service.CreateAsync(Input);
return RedirectToPage("./Index"); // PRG on success
}- Guard every persistence path with
if (!ModelState.IsValid) return Page();. Client-side validation can be bypassed; the server is authoritative. - Use a handler parameter (
OnGetAsync(int id)) for single-request route or query values. Use[BindProperty]for POST data that needs to round-trip back to the view on validation errors. - Named handlers (
OnPostDeleteAsync,OnPostApproveAsync) need theasp-page-handlertag helper on the submit button. Without it, plain buttons fall back toOnPostAsyncor 404. - If
OnGetdoes expensive work, add a lightweightOnHead. Razor Pages falls back toOnGetfor HEAD requests otherwise, so every probe pays the full GET cost. - Filters work differently here than in MVC:
[ActionFilter]attributes are silently ignored on page handlers. UseIPageFilter/IAsyncPageFilter, or register global conventions throughoptions.ConventionsinProgram.cs.
- Shared layouts, partials, and templates go in
Pages/Shared/, notViews/Shared/. Razor Pages resolves views hierarchically from the page's folder up throughPages/, and mixing in MVC conventions just fights the framework. - Set
LayoutinPages/_ViewStart.cshtml. UsePages/_ViewImports.cshtmlfor@namespace,@addTagHelper, and shared directives. - Keep
.cshtmland.cshtml.cscolocated. Per-page locality is one of the main reasons to use Razor Pages in the first place, and splitting them across folders throws that away.
- Trust Razor's default
@expression HTML encoding. Don't reach for@Html.Raw()on user-supplied content; it disables encoding and opens the door to XSS. - Stick with
<form method="post">and the Form Tag Helper so the antiforgery token gets injected automatically. For AJAX orfetch, render the token with@Html.AntiForgeryToken()and send it as theRequestVerificationTokenheader. - Don't commit secrets to
appsettings.json. Useappsettings.{Environment}.jsonfor environment overrides, User Secrets (dotnet user-secrets) locally, and Azure Key Vault or environment variables in production. Bind viaIOptions<T>.
- Watch for the scoped-in-singleton captive dependency trap. If a singleton holds a reference to a scoped service (like an EF
DbContext), that instance leaks across requests. Common bug in PageModel-adjacent services. - Don't register a
DbContextasSingleton. The defaultAddDbContextregistration isScopedfor a reason.
- Project EF entities to DTOs or View Models with
.Select(...)before returning them to the view. Passing entities with navigation properties straight through causes lazy-loading exceptions, N+1 queries, or serialization cycles when the view renders. - Use
.AsNoTracking()on read-only queries like list pages or details pages without edit. The change tracker has overhead you don't need there. - Prefer
FindAsync(key)overFirstOrDefaultAsync(x => x.Id == key)when fetching by primary key withoutInclude.FindAsyncchecks the change tracker first.
TempDatais for one-shot, cross-redirect messages like flash notifications after a PRG. It's read-once, cookie-serialized by default, and not a substitute for session storage.- For actual per-user session state, use
ISession. For per-request data,HttpContext.Items. For shared state within a single request, request-scoped DI services. - Call
TempData.Keep()orTempData.Peek()when a value needs to survive multiple redirects without being consumed.
- Unit-test
PageModelclasses directly. Instantiate them with mocked dependencies (Moq, NSubstitute) and assert on the returnedIActionResult:PageResultfor re-renders,RedirectToPageResultfor successful PRG,NotFoundResultfor 404 paths. - For integration tests that exercise routing, model binding, and antiforgery, use
WebApplicationFactory<TEntryPoint>withMicrosoft.AspNetCore.Mvc.Testing. - When testing handlers that read
ModelState, populate it manually withPageModel.ModelState.AddModelError(...). The binding pipeline doesn't run in unit tests.