Mert Özen Further With Every Line
Backend ASP.NET Core MVC and Razor Pages — #13

What's New in MVC and Razor Pages from .NET 8 to .NET 11

Mert Özen Nov 8, 2026 10 min 3 views
What's New in MVC and Razor Pages from .NET 8 to .NET 11

With .NET 8 and 9 support ending on November 10, 2026, the target for MVC and Razor Pages apps is .NET 10. Running one project on net8.0 and net10.0, we show cookie auth returning 401 instead of 302 for API endpoints, the Minimal API form binding change, the ASP0026 warning and RazorComponentResult, plus .NET 11's MVC items and an upgrade checklist.

Version note: The code in this post was built and run with .NET 10 (SDK 10.0.402); the version comparisons come from running the same project with its net8.0 (runtime 8.0.25) and net10.0 (runtime 10.0.13) targets. The .NET 11 items were taken from the documentation of the release, which was at the RC (Release Candidate) stage at the time of writing, and weren't compiled.

Guess First

Bookstore's MVC (Model-View-Controller) app uses cookie authentication, and next to the views there's a small JSON (JavaScript Object Notation) API (Application Programming Interface): an OrdersApiController marked with [ApiController] and [Authorize]. JavaScript running for a signed-out user calls GET /api/orders. What does this request return while the app is on .NET 8? If you move the same code to net10.0 without changing a single line, does the answer change? Second question: what happens when a signed-out request reaches a report page that has [AllowAnonymous] on the class and [Authorize] on the action?

Why Now?

According to Microsoft's support policy page, support for .NET 8 (LTS (Long-Term Support)) and .NET 9 (STS (Standard-Term Support)) ends on the same day: November 10, 2026. STS releases now get 24 months of support, which is why .NET 9 was extended to the same date as .NET 8. .NET 10 is an LTS release supported until November 14, 2028. .NET 11, as an odd-numbered release, is STS; at the time of writing it's at the RC stage, with GA (General Availability) expected on November 10, 2026.

So for every MVC and Razor Pages app on .NET 8 or 9, the target as of this week is .NET 10. This post brings the series' topics together version by version and shows, with real output, the behavior changes that get missed during an upgrade. The .NET 8, .NET 9 and .NET 10 posts in the Web API series cover the API side; here we're on the view, form and Razor side.

For the comparison I wrote a single project and targeted two frameworks at once:

<TargetFrameworks>net8.0;net10.0</TargetFrameworks>

The same code runs on port 17830 with dotnet run -f net8.0 and on port 17831 with dotnet run -f net10.0.

.NET 8: Razor Components Inside MVC

The big story of .NET 8 was the Blazor Web App: Blazor components can now be rendered statically on the server, without interactivity (static SSR (Server-Side Rendering)). Microsoft's UI (User Interface) selection documentation started recommending Blazor for new projects with this release; we discussed that decision in the first post of this series. The practical consequence for MVC and Razor Pages: you can now return a Razor component (a .razor file) directly from an MVC action:

// a Razor component returned from an MVC action
[HttpGet("/mvc/badge")]
public IResult Badge() =>
    new RazorComponentResult<BookBadge>(new { Title = "Clean Lines", Year = 2023 });
<span class="badge">@Title (@Year)</span>

@code {
    [Parameter] public string Title { get; set; } = "";
    [Parameter] public int Year { get; set; }
}

After registering builder.Services.AddRazorComponents(), both versions return the same response:

HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
<span class="badge">Clean Lines (2023)</span>

This is a step beyond the <component> tag helper inside views (which we saw in the partial view and view component post of this series): instead of embedding the component in a view, you can return it on its own, like the partial responses in the htmx post. It's a bridge for teams that want to move an MVC app gradually to Blazor components.

Other .NET 8 items that touch MVC and Razor Pages: Identity endpoints for SPA (Single-Page Application) and mobile clients with MapIdentityApi (they don't replace the cookie-based Identity UI), the UseAntiforgery middleware for Minimal APIs and Blazor (MVC and Razor Pages carry on with their own filters), the ShortCircuit() endpoint convention that responds right after routing, and keyed DI (Dependency Injection) and IExceptionHandler, which we saw in the Web API series.

.NET 9: MapStaticAssets and New Analyzers

The most visible .NET 9 change for MVC and Razor Pages is MapStaticAssets and the .WithStaticAssets() calls in the template. We measured it in detail in the static assets post: gzip and Brotli at build time, fingerprinted URLs (Uniform Resource Locator), immutable caching. In the same post we also saw that the template's MapControllerRoute(...).WithStaticAssets() line does nothing for MVC views and that MapControllers().WithStaticAssets() fixes it. That bug is still there in the .NET 10.0.13 runtime; it's one of the first places to look when upgrading.

The client libraries the templates bring have changed too. The wwwroot/lib difference between a dotnet new mvc project created with SDK 8.0.419 and one produced by SDK 10.0.402:

Library.NET 8 template.NET 10 template
Bootstrap5.1.05.3.3
jQuery3.6.03.7.1
jQuery Validation1.19.51.21.0
jQuery Validation Unobtrusive4.0.04.0.0

If you upgrade an old project by only changing the TFM (Target Framework Moniker), these files aren't updated on their own; the copy under wwwroot/lib stays as it was when it was created. That's why we recommended LibMan in the static assets post.

.NET 9 also brought an analyzer that catches misleading authorization attributes. The answer to the second question:

[AllowAnonymous]
public class ReportsController : Controller
{
    [Authorize]
    [HttpGet("/reports/sales")]
    public IActionResult Sales() => Content("sales report");
}

It's natural to think the [Authorize] sitting closer to the action "wins". The real response, on both versions:

HTTP/1.1 200 OK
sales report

A signed-out user sees the sales report. Being closer to the action doesn't make [Authorize] win: as long as the endpoint's metadata contains an [AllowAnonymous], the authorization middleware skips the authorization check. The behavior hasn't changed, but the net10.0 build now warns; the net8.0 target compiles the same code silently:

Controllers/BooksController.cs(36,6): warning ASP0026: This [Authorize] attribute is overridden by an [AllowAnonymous] attribute from farther away on 'ReportsController'. See https://aka.ms/aspnetcore-warnings/ASP0026 for more details. (https://aka.ms/aspnet/analyzers) [Bookstore.Web.csproj::TargetFramework=net10.0]

Don't suppress new warnings after an upgrade as "build noise"; this one points to an open security hole. We saw the folder convention version of the same trap in the Razor Pages post.

.NET 10: Silent Behavior Changes

The riskiest part of an upgrade isn't new APIs but the same code behaving differently. The answer to the first question:

##### .NET 8.0.25
HTTP/1.1 302 Found
Location: http://localhost:17830/login?ReturnUrl=%2Fapi%2Forders

##### .NET 10.0.13
HTTP/1.1 401 Unauthorized
Location: http://localhost:17831/login?ReturnUrl=%2Fapi%2Forders

In .NET 10, cookie authentication returns 401 (403 if unauthorized) instead of a redirect to the login page for signed-out requests to known API endpoints. "API endpoint" is defined through IApiEndpointMetadata; controllers marked with [ApiController], Minimal API endpoints that read or write JSON, endpoints returning TypedResults, and SignalR endpoints get it automatically. It's an interesting detail that the response still carries a Location header, but since the status is 401 the browser doesn't follow it.

This change is mostly good: JavaScript calling an API with fetch no longer tries to parse a login HTML (HyperText Markup Language) page that came back with 200 as JSON. But if old code was written as "on a 302, go to login", it now breaks. If you need the old behavior, you do the redirect yourself with CookieAuthenticationEvents.OnRedirectToLogin and OnRedirectToAccessDenied; Microsoft's documentation gives the code. Regular controllers returning views and Razor Pages aren't affected; the /Manage folder from the Razor Pages post still goes to login with a 302.

The second silent change is in form binding. The .NET 10 notes include an item saying "with [FromForm] on a complex object, empty strings are now converted to null", but that item is for Minimal APIs only. I used the same record on both sides:

public record BookDraft(string Title, DateOnly? PublishedOn, decimal? Price);

Real results with the body Title=Clean Lines&PublishedOn=&Price=:

##### .NET 8.0.25
-- minimal
HTTP/1.1 400 Bad Request
-- mvc
MVC: valid=True published=null price=null

##### .NET 10.0.13
-- minimal
HTTP/1.1 200 OK
Minimal: published=null price=null
-- mvc
MVC: valid=True published=null price=null

MVC model binding has always bound an empty string to null for nullable value types; only the Minimal API side changed. There's nothing to do for the MVC and Razor Pages forms from the forms post of this series. But if the same app has a Minimal API endpoint that processes forms (for example, a small endpoint called by htmx), requests that returned 400 on .NET 8 now succeed, and a field you thought was "required" silently arrives as null.

Other .NET 10 items: the [ProducesResponseType], [Produces] and [ProducesDefaultResponseType] attributes gained a Description parameter that flows into the OpenAPI document. Exceptions for which IExceptionHandler.TryHandleAsync returns true are no longer logged at error level by the exception handler middleware; if you set up global error handling as in the Web API series and tied your alerts to that log, alerts going quiet after the upgrade isn't a success. You can restore the old behavior with ExceptionHandlerOptions.SuppressDiagnosticsCallback. Built-in validation for Minimal APIs (AddValidation) also arrived in this release; the ModelState-based validation of MVC and Razor Pages (the validation post of this series) carries on unchanged.

.NET 11: What's Coming?

Since .NET 11 isn't GA and I don't have a .NET 11 SDK, there's no code in this section; the items come from Microsoft's "What's new in ASP.NET Core in .NET 11" page. Those relevant to MVC and Razor Pages:

  • The [ShortCircuit] attribute: the attribute form of the ShortCircuit() convention; it can also be applied to MVC controllers and actions, and a status code can be given with [ShortCircuit(404)].
  • C# union types work in JSON request and response bodies in both Minimal APIs and MVC; they aren't supported for route, query string, header or form values.
  • IAuthorizationRequirementData attributes are now applied on MVC controllers and actions too; before .NET 11 they were only honored on Minimal API and routed endpoints.
  • Authorization metadata (IAuthorizeData, AuthorizationPolicy, IAuthorizationRequirementData) is combined the same way in MVC filters, SignalR and Blazor's AuthorizeView; the shared code is a new AuthorizationPolicy.CombineAsync overload. The legacy MVC path running with EnableEndpointRouting = false is unchanged.
  • ApiExplorer now keeps multiple [ProducesResponseType] declarations for the same status code instead of collapsing them into one.
  • ASP.NET Core adds OpenTelemetry semantic convention attributes to the HTTP (Hypertext Transfer Protocol) server activity itself; no separate instrumentation package is needed.
  • The response compression middleware adds Vary: Accept-Encoding to every response, even when the response itself isn't compressed.

Localization support for Microsoft.Extensions.Validation also arrives in .NET 11, but the documentation explicitly says it doesn't apply to MVC and Razor Pages; the AddDataAnnotationsLocalization path from the localization post stays valid for MVC. Once .NET 11 is GA, we'll cover these items with examples built and run on the stable release in the .NET 11 post of the Web API series.

Upgrade Checklist

When moving an MVC or Razor Pages app from .NET 8 or 9 to .NET 10, in order:

  • Update global.json and TargetFramework, move Microsoft.* packages to 10.0.x; align dependencies like Entity Framework Core (EF Core) to the same major version.
  • Do a clean build and read the new analyzer warnings (especially ASP0026) one by one; don't suppress any of them in bulk.
  • Compare Program.cs with the current template: MapStaticAssets, WithStaticAssets and the MapControllers().WithStaticAssets() fix for MVC.
  • In the published output, look at the <head> of an MVC view and of a Razor Page: do fingerprinted addresses and the import map show up?
  • Call every [ApiController] endpoint protected by cookie authentication while signed out; is there JavaScript expecting a 302?
  • If you use IExceptionHandler, verify that error logs and alerts still arrive.
  • Check the Bootstrap and jQuery versions under wwwroot/lib; if you'll update them, move them to LibMan or npm.
  • Update the container image to the .NET 10 runtime; if you use localization, verify that ICU (International Components for Unicode) is in the image.

Production Notes

Protect version upgrades with behavior tests. The comparisons in this post were done with curl, but writing the same checks as integration tests (with WebApplicationFactory, in the upcoming Testing and Quality series) answers the same questions automatically at the next upgrade: what status does a signed-out API request get, how is an empty form field bound, is there a fingerprint in the <head>.

Multi-targeting works as a migration tool. A project targeting net8.0;net10.0 like the one in this post lets you run the same tests on two runtimes and see the differences. Go to production with a single target, though; supporting two runtimes at once is a permanent cost.

Choose STS releases by the calendar. Moving to .NET 11 doesn't extend .NET 10's support; on the contrary, you enter a 24-month STS calendar. Unless you really need a new feature (for example [ShortCircuit] or union types for MVC), staying on LTS is less tiring for most enterprise apps.

Common Pitfalls

Closing the upgrade as "it compiles, so it's done". The two important differences in this post (401 and Minimal API form binding) produce no compile errors; they only change behavior.

Turning analyzer warnings off in bulk with NoWarn. ASP0026 points to a security hole.

Assuming the WithStaticAssets() line written by the template works in MVC. Check it with your own eyes in the Production output.

Reading a release note item without mapping it to your own framework. The "empty string becomes null with FromForm" item was for Minimal APIs; nothing changed in MVC. The shortest way to verify is to run with two targets.

Thinking that staying on .NET 9 gives longer support than .NET 8. Both end on November 10, 2026.

A Small Experiment

★☆☆ Turn on two targets in your own project with <TargetFrameworks>net8.0;net10.0</TargetFrameworks> and compare the dotnet build output. Which warnings appear only for the net10.0 target?

★★☆ Add an [ApiController] protected by cookie authentication to Bookstore and see that a signed-out request gets 401 on .NET 10. Then write a rule with the OnRedirectToLogin event that returns 401 only for requests under /api and redirects the rest, and verify both paths with curl.

★★★ Turn the comparisons in this post into an integration test project: run the same test class with the net8.0 and net10.0 targets and conditionally verify the expected result per version (302 versus 401, 400 versus 200).

With this post we close the ASP.NET Core MVC and Razor Pages series. Starting from the MVC pattern, we built every layer of a server-side rendered app on Bookstore: Razor syntax, partial views and view components, tag helpers, forms, validation, Razor Pages, static assets, localization, interactivity with htmx, migration from MVC 5 and version differences. From here the road splits two ways: if you want to continue the same server-side approach with a component model, the Blazor series in the Frontend category; if you want to learn in depth the cookie, Identity and policy topics we deliberately kept on the surface in this series, the upcoming Authentication and Authorization series are the natural next steps.