Open javiercn opened 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.
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.
Discovery is the process for which we identify what elements are part of an application so that we can configure the system appropriately.
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.
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:
Define the entry point for discovery
Define a mechanism to walk though other parts of the app:
Define a mechanism to identify the primitives that you want to get from that assembly
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:
flowchart TD
subgraph Application
net8.0
end
app.AddRazorComponents();
app.MapRazorComponents();
flowchart TD
subgraph Application
net8.0
end
app.AddRazorComponents()
.AddServerComponents();
app.MapRazorComponents()
.WithServerComponents();
flowchart TD
subgraph Application
net8.0
net8.0-browser
end
app.AddRazorComponents()
.AddWebassemblyComponents();
app.MapRazorComponents()
.WithWebassemblyComponents();
flowchart TD
subgraph Application
net8.0
net8.0-browser
end
app.AddRazorComponents()
.AddServerComponents()
.AddWebassemblyComponents();
app.MapRazorComponents()
.WithServerComponents()
.WithWebassemblyComponents();
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();
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();
});
});
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
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");
});
});
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");
});
});
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");;
});
});
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");;
});
});
RelatedPartAttribute
) to identify all the assemblies it needs to scan for "parts" of the application.AssemblyPart
implements IApplicationPartTypeProvider
to provide a list of types.CompiledRazorAssemblyPart
implements IRazorCompiledItemProvider
to provide a list of RazorCompiledItem
that are used to find views.To give an end to end example of how this works for controllers, the flow is as follows:
ControllerEndpointFactory
-> ControllerActionDescriptorProvider
-> DefaultApplicationModelProvider
-> ApplicationPartManager.GetFeature<ControllerFeature>
-> ControllerFeatureProvider
.ApplicationPartManager.GetFeature<ControllerFeature>
is responsible for populating a Controller
feature with a list of controllers.ControllerFeatureProvider
is responsible for examining the list of application parts that implement IApplicationPartTypeProvider
and selecting controller types out of them. It is the piece responsible for the definition of "what a controller is".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>();
App
component that Webassembly and Server apps already use.AddControllersAsServices
)) which we don't want to replicate in Blazor.Type implements an interface IComponentApplicationDiscoveryRoot
interface IComponentApplicationDiscoveryRoot
{
static Dictionary<string, PageGroup> GetPages();
}
public class PageGroup
{
public string Name { get; set; }
public List<ComponentPage> Pages { get; set; }
}
public class ComponentPage
{
public string Template { get; }
public Type PageType { get; }
}
ComponentFeatures
so that we can extend it in the future if we want to include other aspects like JsonSerializationContexts or other source generated elements.GetComponentFeatures
always produces a new "graph" that can be mutated to tweak the set of inputs to the app.ComponentFeatures
to perform concrete tasks.IComponentApplicationDiscoveryRoot
is expected to be compile time generated via a source generator or the Razor Compiler. Although initially we will ship a default implementation that does reflection. For example:
class App : ComponentBase, IComponentApplicationDiscoveryRoot
{
ComponentFeatures IComponentApplicationDiscoveryRoot.GetComponentFeatures(){
return new ComponentFeatures() {
ComponentPages = new() {
new PageFeatureGroup(){
Name = "App",
Pages = new(){
new ComponentPage
{
Template = "/",
PageType = typeof(Index)
}
}
},
new PageFeatureGroup(){
Name = "Library",
Pages = new(){
new ComponentPage
{
Template = "/About",
PageType = typeof(About)
}
}
}
}
}
}
}
Lets look at some concrete scenarios:
app.MapRazorComponents<App>(options => options.Pages.Remove("Library"));
app.MapRazorComponents<App>(options => options.Pages.Except<WebAssembly.App>())
IComponentApplicationDiscoveryRoot
and we provide a default implementation for the new member.flowchart TD
subgraph Application
net8.0
end
app.AddRazorComponents();
app.MapRazorComponents<App>();
flowchart TD
subgraph Application
net8.0
end
app.AddRazorComponents()
.AddServerComponents();
app.MapRazorComponents()
.WithServerComponents<App>();
flowchart TD
subgraph Application
net8.0
net8.0-browser
end
app.AddRazorComponents()
.AddWebassemblyComponents();
app.MapRazorComponents<App>()
.WithWebassemblyComponents();
flowchart TD
subgraph Application
net8.0
net8.0-browser
end
app.AddRazorComponents()
.AddServerComponents()
.AddWebassemblyComponents();
app.MapRazorComponents<App>()
.WithServerComponents()
.WithWebassemblyComponents();
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();
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)
);
});
});
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
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");
});
});
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");;
});
});
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");;
});
});
IComponentApplicationDiscoveryRoot
, get the data from there and filter that against the list of assemblies we are given.IComponentApplicationDiscoveryRoot
can contain another method or set of properties that indicates whether the app needs to use webassembly or blazor server (if there are components that requested running on wasm or server).
WithWebassemblyComponents
or WithServerComponents
.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.
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.
The automatic detection works as follows:
MapRazorComponents<App>();
We know what render modes your app requires as we have collected that information from the components.
AddWebAssemblyComponents
and AddServerComponents
establish what render modes are available to the app.
AddWebAssemblyComponents
or AddServerComponents
.SetWebAssemblyRenderingMode
or SetServerRenderingMode
.
AddWebAssemblyComponents
and AddServerComponents
establish what render modes are available to the app.MapRazorComponents
decides what endpoints are needed based on the required component render modes defined for the specific app. Examples:
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.
@javiercn Can this be closed as done? If not, can you rephrase the issue title and description to reflect what's left?
Discussed with @javiercn. Closing this because it's now a mixture of done and covered by more focused issues.
Reopening this issue to target performing actual compile time discovery, since we cut it from .NET 8.0