dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.21k stars 9.95k forks source link

Allow multiple root components and/or render modes to be registered in Blazor Web Apps #52631

Closed yugabe closed 9 months ago

yugabe commented 9 months ago

Is there an existing issue for this?

Is your feature request related to a problem? Please describe the problem.

Currently, when registering multiple component roots like so:

    app.MapRazorComponents<AppRoot>()
        .AddInteractiveWebAssemblyRenderMode();

    app.MapRazorComponents<ServerAppRoot>();

, there will be a late-appearing runtime error because serving "/_framework" files will be handled by both and an AmbiguousMatchException will prevent both roots from being able to be served side-by-side.

It's unclear if the above is supported or not, but the related source seems to imply it will/would be:0 image

The problem is very visible in the default Blazor Web App template's Error.razor page when using WebAssembly. The page gets rendered statically on server-side, then gets overwritten by the Router's NotFound RenderFragment of the WebAssembly app, which can't resolve the /Error route, as that's only available on the server.

The new unified model doesn't allow for different pages to describe different interactivity at different levels of the DOM hierarchy. However, because of the above limitation, it's also not possible to register different pages or routes to different interactivity scenarios manually either.

A current workaround is:

The second part is particularly hard to overcome by developers, as there are no currently available public APIs that allow only registering the page routes (and not the /_framework static files and opaque endpoint).

using Microsoft.AspNetCore.Routing;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;

namespace Microsoft.AspNetCore.Builder;

public static class PodRazorComponentsEndpointBuilderExtensions
{
    public static RazorComponentsEndpointConventionBuilder MapRazorComponentsOnly<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TRootComponent>(this IEndpointRouteBuilder endpoints)
    {
        // This is an intermediate fix for the app not allowing WASM and Server/SSR side-by-side in the same pipeline.
        // As of 8.0.0 (https://github.com/dotnet/aspnetcore/blob/b98185b6376b966c5a051926986acaf204fe4e76/src/Components/Endpoints/src/Builder/RazorComponentsEndpointRouteBuilderExtensions.cs#L36),
        // the problem is related to how each endpoint group wants to serve "_framework" files and routes.
        // This method skips to only the component registrations. All related symbols are not visible, so this is a
        // very error-prone workaround that should be removed as soon as this gets fixed in an upcoming release.
        var method = typeof(RazorComponentsEndpointRouteBuilderExtensions).GetMethod("GetOrCreateDataSource", 1, BindingFlags.NonPublic | BindingFlags.Static, null, [typeof(IEndpointRouteBuilder)], null)!;
        var genericMethod = method.MakeGenericMethod(typeof(TRootComponent));
        var obj = genericMethod.Invoke(null, [endpoints])!;
        var result = (RazorComponentsEndpointConventionBuilder)obj.GetType().GetProperty("DefaultBuilder", BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(obj)!;
        return result;
    }
}

Describe the solution you'd like

Define how multiple interactivity modes can be hosted side-by-side. Allow registering multiple roots (and don't register the /_framework assets multiple times).

Directly related to this, fix the app templates to allow for hosting both SSR and CSR components so that they "just work", as the problem can be seen with the current Error.razor page.

Additional context

The error, specifically:

Microsoft.AspNetCore.Routing.Matching.AmbiguousMatchException: The request matched multiple endpoints. Matches: 

Blazor web static files
Blazor web static files

   at Microsoft.AspNetCore.Routing.Matching.DefaultEndpointSelector.ReportAmbiguity(Span`1 candidateState)
   at Microsoft.AspNetCore.Routing.Matching.DefaultEndpointSelector.ProcessFinalCandidates(HttpContext httpContext, Span`1 candidateState)
   at Microsoft.AspNetCore.Routing.Matching.DefaultEndpointSelector.Select(HttpContext httpContext, Span`1 candidateState)
   at Microsoft.AspNetCore.Routing.Matching.DfaMatcher.MatchAsync(HttpContext httpContext)
   at Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware.Invoke(HttpContext httpContext)
   at Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware.TryServeStaticFile(HttpContext context, String contentType, PathString subPath)
   at Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.HttpsPolicy.HttpsRedirectionMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Builder.Extensions.MapMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddlewareImpl.Invoke(HttpContext context)
javiercn commented 9 months ago

@yugabe thanks for contacting us.

You need to map every app in its own virtual path subfolder. (app1, app2, and so on). Otherwise you get conflicts (which is expected).

While some files are identical (and harmless) it won't be the case for things like dlls, etc. which are actually different between apps.

yugabe commented 9 months ago

Thanks @javiercn for the answer.

I would expect that to be the case, yes, however, seeing as this conflict already happens in the template app when using Auto render mode (for example), and the Error page specifically being rendered correctly first on the server then overwritten on the client with the "Not found" text, I believe there are some holes left in the current design that should be considered.

I also expect it would be a useful and common use-case for many that a Blazor WASM app and a Blazor Server (/static) app could be hosted side-by-side. Multiple WASM apps would certainly cause conflicts, but not when WASM and Server are being hosted together, I assume.

ghost commented 9 months ago

This issue has been resolved and has not had any activity for 1 day. It will be closed for housekeeping purposes.

See our Issue Management Policies for more information.