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.59k stars 10.06k forks source link

Compile time discovery of SSR pages/endpoints for Blazor #46980

Open javiercn opened 1 year ago

javiercn commented 1 year ago
ghost commented 1 year ago

Thanks for contacting us.

We're moving this issue to the .NET 8 Planning milestone for future evaluation / consideration. We would like to keep this around to collect more feedback, which can help us with prioritizing this work. We will re-evaluate this issue, during our next planning meeting(s). If we later determine, that the issue has no community involvement, or it's very rare and low-impact issue, we will close it - so that the team can focus on more important and high impact issues. To learn more about what to expect next and how this issue will be handled you can read more about our triage process here.

javiercn commented 1 year ago

Initial design in the comment below.

Disclaimer alert, the explanation/design is very extensive as it goes into a lot of detail about the different set of scenarios as well as how this works in ASP.NET and why.

The actual implementation of the design is straightforward and contains examples for how it applies to different scenarios.

javiercn commented 1 year ago

Blazor apps discovery

Discovery is the process for which we identify what elements are part of an application so that we can configure the system appropriately.

Context

Discovery has been part of ASP.NET since the times of MVC.

In old ASP.NET, discovery was a concept that was "hidden" to the user, it was considered an implementation detail.

The frameworks used reflection during application startup to find the user defined components (controllers, pages, etc.) and create the appropriate handlers to deal with requests.

The entry point was the entry point assembly for the app and was not configurable.

Discovery in ASP.NET and Blazor

In ASP.NET Core, discovery is present inside MVC and Razor Pages through the concept of Application Parts, and extends to any concept/primitive that the user defines for the app.

In Blazor, discovery is limited to finding pages and is handled by the Router component internally, which is responsible for scanning the assemblies and searching for pages to collect the list of routes.

There are three characteristics about discovery that we have learnt over time are desirable:

General discovery flow

Sketch of the scenarios for discovery with Blazor united

Let's take a look at a rough sketch of the different scenarios with Blazor united in the picture. These scenarios are not meant to work as they are lacking some of the potential concepts needed to make them work.

Some goals to take into account:

For each scenario we need to ask ourselves three questions:

01 - Single Blazor united app (single project only SSR)

flowchart TD
subgraph Application
net8.0
end
app.AddRazorComponents();

app.MapRazorComponents();

02 - Single Blazor united app (single project with server components)

flowchart TD
subgraph Application
net8.0
end
app.AddRazorComponents()
    .AddServerComponents();

app.MapRazorComponents()
    .WithServerComponents();

03 - Single Blazor united app (single project with webassembly components)

flowchart TD
subgraph Application
net8.0
net8.0-browser
end
app.AddRazorComponents()
    .AddWebassemblyComponents();

app.MapRazorComponents()
    .WithWebassemblyComponents();

04 - Single Blazor united app (single project with server and webassembly components)

flowchart TD
subgraph Application
net8.0
net8.0-browser
end
app.AddRazorComponents()
    .AddServerComponents()
    .AddWebassemblyComponents();

app.MapRazorComponents()
    .WithServerComponents()
    .WithWebassemblyComponents();

05 - Single Blazor united app (from referenced project)

For the sake of conciseness this does not cover server variants as they are orthogonal.

flowchart TD
subgraph WebHost
net8.0host["net8.0"]
end
subgraph Application
net8.0app["net8.0"]
net8.0-browser
end
WebHost-->Application
app.AddRazorComponents()
    .AddWebassemblyComponents();

app.MapRazorComponents()
    .WithWebassemblyComponents();

06 - Multiple Blazor united apps (single project)

flowchart TD
subgraph Application
net8.0
net8.0-browser
end
app.MapWhen(ctx => ctx.Request.Path.StartsWithSegments("/FirstApp"), first =>
{
    first.UseRouting();

    first.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorComponents();
    });
});

app.MapWhen(ctx => ctx.Request.Path.StartsWithSegments("/SecondApp"), second =>
{
    second.UseRouting();
    second.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorComponents();
    });
});

07 - Multiple Blazor webassembly apps with prerendering

flowchart TD
subgraph WebHost
net8.0host["net8.0"]
end
subgraph FirstApp
app1net8.0["net8.0"]
app1net8.0-browser["net8.0-browser"]
end
subgraph SecondApp
app2net8.0["net8.0"]
app2net8.0-browser["net8.0-browser"]
end
WebHost-->FirstApp
WebHost-->SecondApp

Today

app.MapWhen(ctx => ctx.Request.Path.StartsWithSegments("/FirstApp"), first =>
{
    first.UseBlazorFrameworkFiles("/FirstApp");
    first.UseStaticFiles();

    first.UseRouting();
    first.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
        endpoints.MapFallbackToRazorPage("FirstApp/{*path:nonfile}", "/_Host.cshtml");
    });
});

app.MapWhen(ctx => ctx.Request.Path.StartsWithSegments("/SecondApp"), second =>
{
    second.UseBlazorFrameworkFiles("/SecondApp");
    second.UseStaticFiles();

    second.UseRouting();
    second.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
        endpoints.MapFallbackToRazorPage("SecondApp/{*path:nonfile}", "/_Host.cshtml");
    });
});

net8.0

app.MapWhen(ctx => ctx.Request.Path.StartsWithSegments("/FirstApp"), first =>
{
    first.UseStaticFiles();

    first.UseRouting();
    first.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
        endpoints.MapRazorComponents()
          .WithWebassemblyComponents()
          .WithRazorPageHost("/Host.cshtml");
    });
});

app.MapWhen(ctx => ctx.Request.Path.StartsWithSegments("/SecondApp"), second =>
{
    second.UseStaticFiles();

    second.UseRouting();
    second.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
        endpoints.MapRazorComponents()
          .WithWebassemblyComponents()            
          .WithRazorPageHost("/Host.cshtml");
    });
});

08 - Multiple Blazor server apps with prerendering

flowchart TD
subgraph Application
net8.0
end
app.MapWhen(ctx => ctx.Request.Path.StartsWithSegments("/FirstApp"), first =>
{
    first.UseRouting();

    first.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorComponents()
          .WithServerComponents()
          .WithRazorPageHost("/Host1.cshtml");
    });
});

app.MapWhen(ctx => ctx.Request.Path.StartsWithSegments("/SecondApp"), second =>
{
    second.UseRouting();
    second.UseEndpoints(endpoints =>
    {
        endpoints.MapComponentHub()
            .WithServerComponents()
            .WithRazorPageHost("/Host2.cshtml");;
    });
});

09 - Blazor Webassembly apps with blazor united app

flowchart TD
subgraph WebHost ["Application (United & Host)"]
net8.0host["net8.0"]
net8.0browserhost["net8.0-browser"]
end
subgraph Application ["Application (Wasm)"]
net8.0app["net8.0"]
net8.0-browser
end
WebHost-->Application
app.MapWhen(ctx => ctx.Request.Path.StartsWithSegments("/FirstApp"), first =>
{
    first.UseRouting();

    first.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorComponents();
    });
});

app.MapWhen(ctx => ctx.Request.Path.StartsWithSegments("/SecondApp"), second =>
{
    second.UseRouting();
    second.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorComponents()
          .WithWebassemblyComponents()            
          .WithRazorPageHost("/Host.cshtml");;
    });
});

What are the missing pieces

What are our options

Desired characteristics for discovery on Blazor apps

A brief interlude on how discovery works in MVC

To give an end to end example of how this works for controllers, the flow is as follows:

Discovery for components

A system like the one used for MVC provides a lot of flexibility and at the same time introduces a lot of complexity and is not devoid of its own problems.

As part of designing discovery for Blazor applications we want to keep the complexity to a minimum, while still retaining some of the good qualities that discovery in MVC offers and overcome some of the current limitations.

With this in mind, let's look at a concrete design proposal, by looking at how it will work on the different scenarios layed out above:

app.AddRazorComponents();

app.MapRazorComponents<Type>();

Lets look at some concrete scenarios:

Benefits

Scenarios with the proposed implementation

01 - Single Blazor united app (single project only SSR)

flowchart TD
subgraph Application
net8.0
end
app.AddRazorComponents();

app.MapRazorComponents<App>();

02 - Single Blazor united app (single project with server components)

flowchart TD
subgraph Application
net8.0
end
app.AddRazorComponents()
    .AddServerComponents();

app.MapRazorComponents()
    .WithServerComponents<App>();

03 - Single Blazor united app (single project with webassembly components)

flowchart TD
subgraph Application
net8.0
net8.0-browser
end
app.AddRazorComponents()
    .AddWebassemblyComponents();

app.MapRazorComponents<App>()
    .WithWebassemblyComponents();

04 - Single Blazor united app (single project with server and webassembly components)

flowchart TD
subgraph Application
net8.0
net8.0-browser
end
app.AddRazorComponents()
    .AddServerComponents()
    .AddWebassemblyComponents();

app.MapRazorComponents<App>()
    .WithServerComponents()
    .WithWebassemblyComponents();

05 - Single Blazor united app (from referenced project)

For the sake of conciseness this does not cover server variants as they are orthogonal.

flowchart TD
subgraph WebHost
net8.0host["net8.0"]
end
subgraph Application
net8.0app["net8.0"]
net8.0-browser
end
WebHost-->Application
app.AddRazorComponents()
    .AddWebassemblyComponents();

app.MapRazorComponents<Library.App>()
    .WithWebassemblyComponents();

06 - Multiple Blazor united apps (single project)

flowchart TD
subgraph Application
net8.0
net8.0-browser
end
app.MapWhen(ctx => ctx.Request.Path.StartsWithSegments("/FirstApp"), first =>
{
    first.UseRouting();

    first.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorComponents<App>(
            options => FilterPagesForFirstApp(options)
        );
    });
});

app.MapWhen(ctx => ctx.Request.Path.StartsWithSegments("/SecondApp"), second =>
{
    second.UseRouting();
    second.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorComponents<App>(
            options => FilterPagesForSecondApp(options)
        );
    });
});

07 - Multiple Blazor webassembly apps with prerendering

flowchart TD
subgraph WebHost
net8.0host["net8.0"]
end
subgraph FirstApp
app1net8.0["net8.0"]
app1net8.0-browser["net8.0-browser"]
end
subgraph SecondApp
app2net8.0["net8.0"]
app2net8.0-browser["net8.0-browser"]
end
WebHost-->FirstApp
WebHost-->SecondApp

net8.0

app.MapWhen(ctx => ctx.Request.Path.StartsWithSegments("/FirstApp"), first =>
{
    first.UseStaticFiles();

    first.UseRouting();
    first.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
        endpoints.MapRazorComponents<FirstApp.App>()
          .WithWebassemblyComponents()
          .WithRazorPageHost("/Host.cshtml");
    });
});

app.MapWhen(ctx => ctx.Request.Path.StartsWithSegments("/SecondApp"), second =>
{
    second.UseStaticFiles();

    second.UseRouting();
    second.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
        endpoints.MapRazorComponents<SecondApp.App>
          .WithWebassemblyComponents()            
          .WithRazorPageHost("/Host.cshtml");
    });
});

08 - Multiple Blazor server apps with prerendering

flowchart TD
subgraph Application
net8.0
end
app.MapWhen(ctx => ctx.Request.Path.StartsWithSegments("/FirstApp"), first =>
{
    first.UseRouting();

    first.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorComponents<FirstApp.App>()
          .WithServerComponents()
          .WithRazorPageHost("/Host1.cshtml");
    });
});

app.MapWhen(ctx => ctx.Request.Path.StartsWithSegments("/SecondApp"), second =>
{
    second.UseRouting();
    second.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorComponents<SecondApp.App>
            .WithServerComponents()
            .WithRazorPageHost("/Host2.cshtml");;
    });
});

09 - Blazor Webassembly apps with blazor united app

flowchart TD
subgraph WebHost ["Application (United & Host)"]
net8.0host["net8.0"]
net8.0browserhost["net8.0-browser"]
end
subgraph Application ["Application (Wasm)"]
net8.0app["net8.0"]
net8.0-browser
end
WebHost-->Application
app.MapWhen(ctx => ctx.Request.Path.StartsWithSegments("/FirstApp"), first =>
{
    first.UseRouting();

    first.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorComponents<App>(
            options => options.Pages.Except<SecondApp.App>())
        );
    });
});

app.MapWhen(ctx => ctx.Request.Path.StartsWithSegments("/SecondApp"), second =>
{
    second.UseRouting();
    second.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorComponents<SecondApp.App>()
          .WithWebassemblyComponents()            
          .WithRazorPageHost("/Host.cshtml");;
    });
});

Appendix A

Appendix B

Appendix C

All the examples use a root component, but in reality IComponentApplicationDiscoveryRoot can be implemented by any type, so this design does not force us into having a root component, we could use instead MainLayout if we choose to do so.

ghost commented 1 year ago

Thanks for contacting us.

We're moving this issue to the .NET 8 Planning milestone for future evaluation / consideration. We would like to keep this around to collect more feedback, which can help us with prioritizing this work. We will re-evaluate this issue, during our next planning meeting(s). If we later determine, that the issue has no community involvement, or it's very rare and low-impact issue, we will close it - so that the team can focus on more important and high impact issues. To learn more about what to expect next and how this issue will be handled you can read more about our triage process here.

javiercn commented 1 year ago

Expansion on Appendix B

The automatic detection works as follows:

When you have a single call to MapRazorComponents<App>();

We know what render modes your app requires as we have collected that information from the components.

Multiple calls to MapRazorComponents();

Benefits of automatically detecting the render mode.

Potential cons of detecting the render mode

Auto components triggering additional render modes?

This is not the case, unless you have: 1) Multiple apps. 2) You have already OK'ed globally via AddServerComponents or AddWebAssemblyComponents that you are ok with those endpoints being present. Otherwise, Auto simply adapts to the available render modes.

A library adding additional endpoints to the application?

javiercn commented 1 year ago
SteveSandersonMS commented 1 year ago

@javiercn Can this be closed as done? If not, can you rephrase the issue title and description to reflect what's left?

SteveSandersonMS commented 1 year ago

Discussed with @javiercn. Closing this because it's now a mixture of done and covered by more focused issues.

javiercn commented 1 year ago

Reopening this issue to target performing actual compile time discovery, since we cut it from .NET 8.0

javiercn commented 1 year ago

https://github.com/dotnet/aspnetcore/issues/52077 https://github.com/dotnet/aspnetcore/issues/51411 https://github.com/dotnet/aspnetcore/issues/51237

https://github.com/dotnet/aspnetcore/issues/48767