2

I have a quite complicated existing asp.net core site with an SPA. This SPA uses # for routing, so all of the existing razor pages and API endpoints just work.

I first tried to host just using app.UseBlazorFrameworkFiles(), but all sorts of things break with this.

Then I tried to put it in a sub folder: app.UseBlazorFrameworkFiles("/UI"), updated the main Index.cshtml to redirect to /UI/ and serve the appropriate HTML there in /Areas/UI/Pages/Index.cshtml and added <StaticWebAssetBasePath>UI</StaticWebAssetBasePath> and hacked the AddHttpClient to use baseaddress of the whole site.

This works... Except when it doesn't, in particularly when using a url to a subpage in blazor or a from blazor navigating to a route that doesn't exists. This will end up visiting the hosted site and serve my 404 instead.

I then tried various variations of

app.MapWhen(ctx => ctx.Request.Path.StartsWithSegments("/UI/"), blazor => {
   blazor.UseEndpoints(endpoints => {
      endpoints.MapFallbackToAreaPage("/UI/{*path:nonfile}", "/", "UI");
   });
});

Result: InvalidOperationException: Cannot find the fallback endpoint specified by route values: { page: /, area: UI }.

app.MapFallbackToAreaPage("/", "UI");

Result: InvalidOperationException: Cannot find the fallback endpoint specified by route values: { page: /, area: UI }.

app.MapFallbackToAreaPage("/Index", "UI");

Result: AmbiguousMatchException: The request matched multiple endpoints. Matches: /Index /Index

or any other sorts of variations I could come up with, all with the result of either a) blows up the existing configuration or b) blows up at startup or c) blows up when visting a page that doesn't exist.

Help please. How do I make this blazor hosted on my site?

Cine
  • 4,255
  • 26
  • 46

3 Answers3

2

I can't be sure here - it's not easy building a picture from some code snippets - but you are probably missing the <base href=....> setup in index.html

There's an StackOverflow answer, repo and demo I put together recently for a very similar question - this one on hosting multiple SPAs on the same site.

Create a multiple WebAssembly projects in a single solution

Here's an example index.html:

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
    <title>Blazr.Medusa.Grey</title>
    <base href="/grey/" />
    <link href="/grey/css/bootstrap/bootstrap.min.css" rel="stylesheet" />
    <link href="/grey/css/app.css" rel="stylesheet" />
    <link href="Blazr.Medusa.Grey.styles.css" rel="stylesheet" />
</head>

Project File:

<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">

    <PropertyGroup>
        <TargetFramework>net6.0</TargetFramework>
        <Nullable>enable</Nullable>
        <ImplicitUsings>enable</ImplicitUsings>
        <StaticWebAssetBasePath>grey</StaticWebAssetBasePath>
    </PropertyGroup>

    <ItemGroup>
        <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="6.0.2" />
        <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="6.0.2" PrivateAssets="all" />
    </ItemGroup>

    <ItemGroup>
      <ProjectReference Include="..\Blazr.Medusa.SPA\Blazr.Medusa.SPA.csproj" />
    </ItemGroup>

</Project>

And Web Site program setup section:

app.MapWhen(ctx => ctx.Request.Path.StartsWithSegments("/grey"), app1 =>
{
    app1.UseBlazorFrameworkFiles("/grey");
    app1.UseRouting();
    app1.UseEndpoints(endpoints =>
    {
        endpoints.MapFallbackToFile("/grey/{*path:nonfile}", "/grey/index.html");
    });
});

Addition

Here's my full Program to show the middleware order:

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.MapWhen(ctx => ctx.Request.Path.StartsWithSegments("/grey"), app1 =>
{
    app1.UseBlazorFrameworkFiles("/grey");
    app1.UseRouting();
    app1.UseEndpoints(endpoints =>
    {
        endpoints.MapFallbackToFile("/grey/{*path:nonfile}", "/grey/index.html");
    });
});

app.MapWhen(ctx => ctx.Request.Path.StartsWithSegments("/green"), app1 =>
{
    app1.UseBlazorFrameworkFiles("/green");
    app1.UseRouting();
    app1.UseEndpoints(endpoints =>
    {
        endpoints.MapFallbackToFile("/green/{*path:nonfile}", "/green/index.html");
    });
});

app.MapWhen(ctx => ctx.Request.Path.StartsWithSegments("/purple"), app1 =>
{
    app1.UseBlazorFrameworkFiles("/purple");
    app1.UseRouting();
    app1.UseEndpoints(endpoints =>
    {
        endpoints.MapFallbackToFile("/purple/{*path:nonfile}", "/purple/index.html");
    });
});

app.UseBlazorFrameworkFiles("");

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();


app.MapFallbackToFile("/index.html");

app.Run();

Any questions - ask.

MrC aka Shaun Curtis
  • 19,075
  • 3
  • 13
  • 31
  • Cant have app1.UseRouting(); in the MapWhen, that doesnt compile with the msg "The call to UseAuthorization should appear between app.UseRouting() and app.UseEndpoints(..) for authorization to be correctly evaluated" – Cine Mar 01 '22 at 11:43
  • The order of the middleware is important - it often takes a bit of tweaking to get it right. I've added the full program for my setup to the answer. You can see the full setup in the Repo. If you are still having problems after that, add your full `program` to the question. – MrC aka Shaun Curtis Mar 01 '22 at 11:54
  • Thanks for your help. I was unable to get it working with this approach, either authentication, or error handlers would mess things up. See my self answer for solution. – Cine Mar 02 '22 at 04:41
2

I created a standard Blazor WASM hosted solution [Client/Server/Shared].

It built and ran OK in Visual studio but when I published it to a subfolder on my website it failed to load.

I finally identified that the problem was with the <base href="/" /> in the Client index.html.

The subfolder was named the same as the solution [BlazorWASMHosted2022] so I changed the tag to <base href="/BlazorWASMHosted2022/" /> and published the solution again and all worked ok.

From what I have googled it appears that this is a problem with Blazor WASM.

So I have come up with a script added into index.html that tests the current URL and sets the base.href accordingly which works OK - here is my sample (with some info displayed while loading):

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
    <title>BlazorWASMHosted2022</title>

    <script type="text/javascript">
        var base = document.createElement('base');
        var currentUrl = window.location.href;
        var localHostUrl = 'https://localhost';
        if (currentUrl.startsWith(localHostUrl))
        {
            base.href = '/';
        } else {
            base.href = '/BlazorWASMHosted2022/';
        }
        document.getElementsByTagName('head')[0].appendChild(base);
    </script>

    <link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
    <link href="css/app.css" rel="stylesheet" />
    <link href="BlazorWASMHosted2022.Client.styles.css" rel="stylesheet" />
</head>

<body>
    <div id="app">Loading...</div>
    <div id="blazor-error-ui">
        An unhandled error has occurred.
        <a href="" class="reload">Reload</a>
        <a class="dismiss"></a>
    </div>

    <script type="text/javascript">
        document.write("location.pathname:- <br/>");
        document.write(window.location.href);
        document.write("<br/>");
        document.write("base.href:- <br/>");
        document.write(base.href);
    </script>
    <script src="_framework/blazor.webassembly.js"></script> 
</body>
</html>
1

TLDR; Fix it at the start of pipeline, not at the end (with mapfallback)

I was able to find a solution simply by hacking the pipeline and rewriting the request.

app.UseHttpsRedirection(); //after this

//host blazor in this folder
app.UseBlazorFrameworkFiles("/UI");
app.Use((ctx, next) => {
   if (ctx.Request.Path.StartsWithSegments("/UI", out var rest) && !rest.StartsWithSegments("/"))
   {
      //detected paths that needs to be routed by Blazor and not server
      ctx.Request.Path = new PathString("/UI/");
      return next();
   }
   return next();

});

If this is placed after UseHttpsRedirection and before everything else (can be after UseStaticFiles also if the new path is a non-file, like a razor page), it will rewrite EVERYTHING like /UI/* into /UI/.

Cine
  • 4,255
  • 26
  • 46