ASP.NET Core Blazor Routing NavigationManager – 404 Not Found ForceLoad Hack

Introduction

It is critical for developers to know how to properly use the NavigationManager to manage URIs and navigation from Blazor code. In this blog post, we aim to raise awareness of a small, but important feature of the NavigateTo method provided by NavigationManager

TLDR;

There is a forceLoad parameter on the NavigationManager‘s NavigateTo method that is important to understand. When you set this Boolean to true in code, it instructs the NavigationManager to bypass its usual client-side routing and forces the browser to load the newly-requested page from the server, whether or not the URI would normally be handled by the client-side router.

Backstory (MultiHead Solution Structure)

MultiHead Solution Structure

When developing Blazor applications, I like to create a “MultiHead”, or Dual Mode, application architecture. In practice, that means all but a few pages, components, and content live in a separate project assembly. Then, separate Blazor WebAssembly and Blazor Server (SignalR) projects both reference and consume ASP.NET Core Razor components from this Razor class library. There are a few reasons for doing this, but one of the main ones is that I find it easier to debug during development using the “Server” (SignalR) project and I prefer to deploy the “Client” (Wasm) project for end users. The yellow arrows in the image point to each project “head”. Then, the “App” project, highlighted in blue, contains all the Razor pages, ViewModels inheriting from ComponentBase, and wwwroot content (i.e. css, js, images) that get shared between both heads. That is not to be confused, however, with the “Shared” project, which contains code shared between the “App” and Web API (“Api”) projects in the solution, but I’m starting to digress.

I would say this approach works for more than 95% of the required code files. However, every now and then, there are variances in behavior that require different NuGet packages or approaches, depending upon which deployment model is being used.

WebAssembly versus SignalR Deployment Model Differences

As mentioned, there is code that is specific to the deployment model such as when writing code to integrate with Identity Server 4. For example, the RemoteAuthenticatorView component is only used by WebAssembly (found in the Microsoft.AspNetCore.Components.WebAssembly.Authentication namespace). For obvious reasons, it makes no sense to reference that authentication library designed for WebAssembly from the Server (SignalR) project. Likewise, in the Server project, it is possible to make use of HttpContext in ways that are not valid for WebAssembly. Sometimes, the code differences are more subtle, as seen in the Router component that can *almost*, but not quite, be used across both projects:

<CascadingAuthenticationState>
	<Router AppAssembly="@typeof(MainLayoutViewModel).Assembly" PreferExactMatches="@true" 
			AdditionalAssemblies="new[] { typeof(MSC.YourApp.Server.Program).Assembly }">
		<Found Context="routeData">
			<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
				<NotAuthorized>
					<RedirectToLogin />
				</NotAuthorized>
			</AuthorizeRouteView>
		</Found>
		<NotFound>
			<LayoutView Layout="@typeof(MainLayout)">
				<p>Sorry, there's nothing at this address.</p>
			</LayoutView>
		</NotFound>
	</Router>
</CascadingAuthenticationState>

The Router component (shown above) and used by the App component in Blazor apps, is what enables routing to Razor components in a Blazor app. The AdditionalAssemblies parameter is used above to specify, surprise, additional assemblies for the Router component to use when searching for routable components. Really the only change that must be made between deployment modes here is how the Server project is used in the AdditionalAssemblies attribute. This attribute value then gets flipped for the Client-side project.

However, with a small amount of conditional logic, both heads can use the same RedirectToLogin component:

protected override void OnInitialized()
{
	if (AppSettings.IsWebAssembly)
	{
	NavigationManager.NavigateTo($"authentication/login?returnUrl={Uri.EscapeDataString(NavigationManager.Uri)}");
	}
	else
	{
	NavigationManager.NavigateTo($"LoginIDP?redirectUri={Uri.EscapeDataString(NavigationManager.Uri)}", 
		forceLoad: true);
	}
}

In the code above, a simple check is performed to see whether the application is running completely on the client using WebAssembly or using a SignalR connection. Of course, how all this authentication is setup could easily be one or more blog posts by itself but knowing that is not necessary for understanding the problem, which we will now cover.

The Problem

Examine the code above and, more specifically, the NavigateTo method of NavigationManager. What is important to know is that a 404 Page Not Found error will result if the forceLoad parameter is not set to true. The reason for this is because the router component uses “client-side routing”. Even though we provided the AdditionalAssemblies parameter of the Router component with the assembly containing the LoginIDP page it wants to navigate to, that page does not exist in the client-side code. Say what?

You see, the actual file name of that LoginIDP page is LoginIDP.cshtml and, in the world of Blazor server projects, .cshtml files contain code that is compiled and executed on the server. Razor pages (.razor), on the other hand, get compiled into .dll files and sent to client side for execution by a .NET runtime integrated with the browser.

Red Herring Alert

Originally when I hit this 404 Not Found issue, the LoginIDP page worked when it was hit via an anchor tag that only used “LoginIDP”, without the redirectUri parameter shown in the code above. So, what did I do? I entered the URL to the LoginIDP page, again without the redirectUri parameter, into the browser’s address bar, pressed Enter and it worked that way, too! Using the NavigationManager, which included the redirectUri parameter did not work, however.

It was getting late after a long day, and I was convinced the problem had to do with the parameter routing. I tried every trick I knew from adding “options.Conventions.AddPageRoute” in Startup, adding optional page route parameters, even switching my OnGetAsync(string redirectUri) to use [BindProperty(SupportsGet = true)] instead. Nothing worked and then my Google searches lead me to this article:

Describe the bug

Unable to use routing in when creating host and using controllers from separate projects. When using everything from one project, routes work fine.

Of course, that must be it, I thought – maybe a regression bug from Microsoft! I went down this incorrect path and lost more precious time than I would like to admit on it. Let’s just say that, by the time I found and understood the actual root cause and solution, getting to sleep was looking really good.

The Solution

Just to reiterate the solution in more detail, the trick is to set the forceLoad parameter to true if you want to bypass client-side routing. Doing so will force the app to load the requested page from the server, whether or not the URI would normally be handled by the client-side router.

Now, with code executing on the server-side, it is safe to make use of HttpContext to have that LoginIDP page check the user’s authentication status and challenge the current request using the OpenIdConnect scheme, as specified in the code below.

public async Task OnGetAsync(string redirectUri)
{
  if (string.IsNullOrWhiteSpace(redirectUri))
  {
    redirectUri = Url.Content("~/");
  }

  if (HttpContext.User.Identity.IsAuthenticated)
  {
    Response.Redirect(redirectUri);
  }

  await HttpContext.ChallengeAsync(
    OpenIdConnectDefaults.AuthenticationScheme,
    new AuthenticationProperties { RedirectUri = redirectUri });
}

Congratulations!

You just added a little more deep knowledge to your Blazor toolbelt, assuming you were not already aware of this forceLoad nuance of NavigationManager. As always, hopefully, reading this post comes in handy at some point or saves you some time and frustration in special scenarios!

1 thought on “ASP.NET Core Blazor Routing NavigationManager – 404 Not Found ForceLoad Hack”

Leave a Comment

Your email address will not be published.