Skip to content

Commit 56f2ba4

Browse files
[Blazor] Add IRazorComponentsBuilder.AddWebAssemblyComponents() (#48664)
1 parent f8c1bb3 commit 56f2ba4

19 files changed

+385
-6
lines changed

AspNetCore.sln

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1786,6 +1786,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Authen
17861786
EndProject
17871787
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IdentitySample.ApiEndpoints", "src\Identity\samples\IdentitySample.ApiEndpoints\IdentitySample.ApiEndpoints.csproj", "{37FC77EA-AC44-4D08-B002-8EFF415C424A}"
17881788
EndProject
1789+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Components.WasmMinimal", "src\Components\test\testassets\Components.WasmMinimal\Components.WasmMinimal.csproj", "{87D58D50-20D1-4091-88C5-8D88DCCC2DE3}"
1790+
EndProject
17891791
Global
17901792
GlobalSection(SolutionConfigurationPlatforms) = preSolution
17911793
Debug|Any CPU = Debug|Any CPU
@@ -10739,6 +10741,22 @@ Global
1073910741
{37FC77EA-AC44-4D08-B002-8EFF415C424A}.Release|x64.Build.0 = Release|Any CPU
1074010742
{37FC77EA-AC44-4D08-B002-8EFF415C424A}.Release|x86.ActiveCfg = Release|Any CPU
1074110743
{37FC77EA-AC44-4D08-B002-8EFF415C424A}.Release|x86.Build.0 = Release|Any CPU
10744+
{87D58D50-20D1-4091-88C5-8D88DCCC2DE3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
10745+
{87D58D50-20D1-4091-88C5-8D88DCCC2DE3}.Debug|Any CPU.Build.0 = Debug|Any CPU
10746+
{87D58D50-20D1-4091-88C5-8D88DCCC2DE3}.Debug|arm64.ActiveCfg = Debug|Any CPU
10747+
{87D58D50-20D1-4091-88C5-8D88DCCC2DE3}.Debug|arm64.Build.0 = Debug|Any CPU
10748+
{87D58D50-20D1-4091-88C5-8D88DCCC2DE3}.Debug|x64.ActiveCfg = Debug|Any CPU
10749+
{87D58D50-20D1-4091-88C5-8D88DCCC2DE3}.Debug|x64.Build.0 = Debug|Any CPU
10750+
{87D58D50-20D1-4091-88C5-8D88DCCC2DE3}.Debug|x86.ActiveCfg = Debug|Any CPU
10751+
{87D58D50-20D1-4091-88C5-8D88DCCC2DE3}.Debug|x86.Build.0 = Debug|Any CPU
10752+
{87D58D50-20D1-4091-88C5-8D88DCCC2DE3}.Release|Any CPU.ActiveCfg = Release|Any CPU
10753+
{87D58D50-20D1-4091-88C5-8D88DCCC2DE3}.Release|Any CPU.Build.0 = Release|Any CPU
10754+
{87D58D50-20D1-4091-88C5-8D88DCCC2DE3}.Release|arm64.ActiveCfg = Release|Any CPU
10755+
{87D58D50-20D1-4091-88C5-8D88DCCC2DE3}.Release|arm64.Build.0 = Release|Any CPU
10756+
{87D58D50-20D1-4091-88C5-8D88DCCC2DE3}.Release|x64.ActiveCfg = Release|Any CPU
10757+
{87D58D50-20D1-4091-88C5-8D88DCCC2DE3}.Release|x64.Build.0 = Release|Any CPU
10758+
{87D58D50-20D1-4091-88C5-8D88DCCC2DE3}.Release|x86.ActiveCfg = Release|Any CPU
10759+
{87D58D50-20D1-4091-88C5-8D88DCCC2DE3}.Release|x86.Build.0 = Release|Any CPU
1074210760
EndGlobalSection
1074310761
GlobalSection(SolutionProperties) = preSolution
1074410762
HideSolutionNode = FALSE
@@ -11621,6 +11639,7 @@ Global
1162111639
{56291265-B7BF-4756-92AB-FC30F09381D1} = {822D1519-77F0-484A-B9AB-F694C2CC25F1}
1162211640
{66FA1041-5556-43A0-9CA3-F9937F085F6E} = {56291265-B7BF-4756-92AB-FC30F09381D1}
1162311641
{37FC77EA-AC44-4D08-B002-8EFF415C424A} = {64B2A28F-6D82-4F2B-B0BB-88DE5216DD2C}
11642+
{87D58D50-20D1-4091-88C5-8D88DCCC2DE3} = {6126DCE4-9692-4EE2-B240-C65743572995}
1162411643
EndGlobalSection
1162511644
GlobalSection(ExtensibilityGlobals) = postSolution
1162611645
SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F}

src/Components/Components.slnf

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
"src\\Components\\test\\E2ETest\\Microsoft.AspNetCore.Components.E2ETests.csproj",
5454
"src\\Components\\test\\testassets\\BasicTestApp\\BasicTestApp.csproj",
5555
"src\\Components\\test\\testassets\\Components.TestServer\\Components.TestServer.csproj",
56+
"src\\Components\\test\\testassets\\Components.WasmMinimal\\Components.WasmMinimal.csproj",
5657
"src\\Components\\test\\testassets\\ComponentsApp.App\\ComponentsApp.App.csproj",
5758
"src\\Components\\test\\testassets\\ComponentsApp.Server\\ComponentsApp.Server.csproj",
5859
"src\\Components\\test\\testassets\\GlobalizationWasmApp\\GlobalizationWasmApp.csproj",
@@ -148,4 +149,4 @@
148149
"src\\WebEncoders\\src\\Microsoft.Extensions.WebEncoders.csproj"
149150
]
150151
}
151-
}
152+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.AspNetCore.Http;
5+
6+
namespace Microsoft.AspNetCore.Components.Endpoints;
7+
8+
/// <summary>
9+
/// Options to configure interactive WebAssembly components.
10+
/// </summary>
11+
public sealed class WebAssemblyComponentsEndpointOptions
12+
{
13+
/// <summary>
14+
/// Gets or sets the <see cref="PathString"/> that indicates the prefix for Blazor WebAssembly assets.
15+
/// This path must correspond to a referenced Blazor WebAssembly application project.
16+
/// </summary>
17+
public PathString PathPrefix { get; set; }
18+
}

src/Components/Endpoints/src/PublicAPI.Unshipped.txt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ Microsoft.AspNetCore.Components.Endpoints.RenderModeEndpointProvider.RenderModeE
7676
Microsoft.AspNetCore.Components.Endpoints.RootComponentMetadata
7777
Microsoft.AspNetCore.Components.Endpoints.RootComponentMetadata.RootComponentMetadata(System.Type! rootComponentType) -> void
7878
Microsoft.AspNetCore.Components.Endpoints.RootComponentMetadata.Type.get -> System.Type!
79+
Microsoft.AspNetCore.Components.Endpoints.WebAssemblyComponentsEndpointOptions
80+
Microsoft.AspNetCore.Components.Endpoints.WebAssemblyComponentsEndpointOptions.PathPrefix.get -> Microsoft.AspNetCore.Http.PathString
81+
Microsoft.AspNetCore.Components.Endpoints.WebAssemblyComponentsEndpointOptions.PathPrefix.set -> void
82+
Microsoft.AspNetCore.Components.Endpoints.WebAssemblyComponentsEndpointOptions.WebAssemblyComponentsEndpointOptions() -> void
7983
Microsoft.AspNetCore.Components.Infrastructure.RazorComponentApplicationAttribute
8084
Microsoft.AspNetCore.Components.Infrastructure.RazorComponentApplicationAttribute.RazorComponentApplicationAttribute() -> void
8185
Microsoft.AspNetCore.Components.PersistedStateSerializationMode
@@ -91,4 +95,4 @@ static Microsoft.AspNetCore.Builder.RazorComponentsEndpointRouteBuilderExtension
9195
static Microsoft.AspNetCore.Components.Discovery.ComponentApplicationBuilder.GetBuilder<TComponent>() -> Microsoft.AspNetCore.Components.Discovery.ComponentApplicationBuilder?
9296
static Microsoft.Extensions.DependencyInjection.RazorComponentsServiceCollectionExtensions.AddRazorComponents(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.AspNetCore.Components.Endpoints.IRazorComponentsBuilder!
9397
static readonly Microsoft.AspNetCore.Components.Endpoints.RazorComponentResultExecutor.DefaultContentType -> string!
94-
virtual Microsoft.AspNetCore.Components.Endpoints.RazorComponentResultExecutor.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext, Microsoft.AspNetCore.Components.Endpoints.RazorComponentResult! result) -> System.Threading.Tasks.Task!
98+
virtual Microsoft.AspNetCore.Components.Endpoints.RazorComponentResultExecutor.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext, Microsoft.AspNetCore.Components.Endpoints.RazorComponentResult! result) -> System.Threading.Tasks.Task!

src/Components/WebAssembly/Server/src/Microsoft.AspNetCore.Components.WebAssembly.Server.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
</PropertyGroup>
1313

1414
<ItemGroup>
15+
<Reference Include="Microsoft.AspNetCore.Components.Endpoints" />
1516
<Reference Include="Microsoft.AspNetCore.StaticFiles" />
1617
<Reference Include="Microsoft.NETCore.BrowserDebugHost.Transport" GeneratePathProperty="true" PrivateAssets="All" />
1718
</ItemGroup>
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
#nullable enable
22
Microsoft.AspNetCore.Components.WebAssembly.Server.TargetPickerUi.DisplayFirefox(Microsoft.AspNetCore.Http.HttpContext! context) -> System.Threading.Tasks.Task!
3+
Microsoft.Extensions.DependencyInjection.RazorComponentsBuilderExtensions
4+
static Microsoft.Extensions.DependencyInjection.RazorComponentsBuilderExtensions.AddWebAssemblyComponents(this Microsoft.AspNetCore.Components.Endpoints.IRazorComponentsBuilder! builder, System.Action<Microsoft.AspNetCore.Components.Endpoints.WebAssemblyComponentsEndpointOptions!>? configure = null) -> Microsoft.AspNetCore.Components.Endpoints.IRazorComponentsBuilder!
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.AspNetCore.Builder;
5+
using Microsoft.AspNetCore.Components;
6+
using Microsoft.AspNetCore.Components.Endpoints;
7+
using Microsoft.AspNetCore.Components.Web;
8+
using Microsoft.AspNetCore.Http;
9+
using Microsoft.AspNetCore.Routing;
10+
using Microsoft.Extensions.DependencyInjection.Extensions;
11+
using Microsoft.Extensions.Options;
12+
13+
namespace Microsoft.Extensions.DependencyInjection;
14+
15+
/// <summary>
16+
/// Extension methods to configure an <see cref="IServiceCollection"/> for WebAssembly components.
17+
/// </summary>
18+
public static class RazorComponentsBuilderExtensions
19+
{
20+
/// <summary>
21+
/// Adds services to support rendering interactive WebAssembly components.
22+
/// </summary>
23+
/// <param name="builder">The <see cref="IRazorComponentsBuilder"/>.</param>
24+
/// <param name="configure">A callback to configure <see cref="WebAssemblyComponentsEndpointOptions"/>.</param>
25+
/// <returns>An <see cref="IRazorComponentsBuilder"/> that can be used to further customize the configuration.</returns>
26+
public static IRazorComponentsBuilder AddWebAssemblyComponents(this IRazorComponentsBuilder builder, Action<WebAssemblyComponentsEndpointOptions>? configure = null)
27+
{
28+
ArgumentNullException.ThrowIfNull(builder, nameof(builder));
29+
30+
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<RenderModeEndpointProvider, WebAssemblyEndpointProvider>());
31+
32+
if (configure is not null)
33+
{
34+
builder.Services.Configure(configure);
35+
}
36+
37+
return builder;
38+
}
39+
40+
private class WebAssemblyEndpointProvider : RenderModeEndpointProvider
41+
{
42+
private readonly IServiceProvider _services;
43+
private readonly WebAssemblyComponentsEndpointOptions _options;
44+
45+
public WebAssemblyEndpointProvider(IServiceProvider services, IOptions<WebAssemblyComponentsEndpointOptions> options)
46+
{
47+
_services = services;
48+
_options = options.Value;
49+
}
50+
51+
public override IEnumerable<RouteEndpointBuilder> GetEndpointBuilders(IComponentRenderMode renderMode, IApplicationBuilder applicationBuilder)
52+
{
53+
var endpointRouteBuilder = new EndpointRouteBuilder(_services, applicationBuilder);
54+
var pathPrefix = _options.PathPrefix;
55+
56+
applicationBuilder.UseBlazorFrameworkFiles(pathPrefix);
57+
var app = applicationBuilder.Build();
58+
59+
endpointRouteBuilder.Map($"{pathPrefix}/_framework/{{*path}}", context =>
60+
{
61+
// Set endpoint to null so the static files middleware will handle the request.
62+
context.SetEndpoint(null);
63+
64+
return app(context);
65+
});
66+
67+
return endpointRouteBuilder.GetEndpoints();
68+
}
69+
70+
public override bool Supports(IComponentRenderMode renderMode)
71+
=> renderMode is WebAssemblyRenderMode or AutoRenderMode;
72+
73+
private class EndpointRouteBuilder : IEndpointRouteBuilder
74+
{
75+
private readonly IApplicationBuilder _applicationBuilder;
76+
77+
public EndpointRouteBuilder(IServiceProvider serviceProvider, IApplicationBuilder applicationBuilder)
78+
{
79+
ServiceProvider = serviceProvider;
80+
_applicationBuilder = applicationBuilder;
81+
}
82+
83+
public IServiceProvider ServiceProvider { get; }
84+
85+
public ICollection<EndpointDataSource> DataSources { get; } = new List<EndpointDataSource>() { };
86+
87+
public IApplicationBuilder CreateApplicationBuilder()
88+
{
89+
return _applicationBuilder.New();
90+
}
91+
92+
internal IEnumerable<RouteEndpointBuilder> GetEndpoints()
93+
{
94+
foreach (var ds in DataSources)
95+
{
96+
foreach (var endpoint in ds.Endpoints)
97+
{
98+
var routeEndpoint = (RouteEndpoint)endpoint;
99+
var builder = new RouteEndpointBuilder(endpoint.RequestDelegate, routeEndpoint.RoutePattern, routeEndpoint.Order);
100+
for (var i = 0; i < routeEndpoint.Metadata.Count; i++)
101+
{
102+
var metadata = routeEndpoint.Metadata[i];
103+
builder.Metadata.Add(metadata);
104+
}
105+
106+
yield return builder;
107+
}
108+
}
109+
}
110+
}
111+
}
112+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Components.TestServer.RazorComponents;
5+
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
6+
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
7+
using Microsoft.AspNetCore.E2ETesting;
8+
using OpenQA.Selenium;
9+
using TestServer;
10+
using Xunit.Abstractions;
11+
12+
namespace Microsoft.AspNetCore.Components.E2ETests.ServerRenderingTests;
13+
14+
public class InteractivityTest : ServerTestBase<BasicTestAppServerSiteFixture<RazorComponentEndpointsStartup<App>>>
15+
{
16+
public InteractivityTest(
17+
BrowserFixture browserFixture,
18+
BasicTestAppServerSiteFixture<RazorComponentEndpointsStartup<App>> serverFixture,
19+
ITestOutputHelper output)
20+
: base(browserFixture, serverFixture, output)
21+
{
22+
}
23+
24+
public override Task InitializeAsync()
25+
=> InitializeAsync(BrowserFixture.StreamingContext);
26+
27+
[Fact]
28+
public void CanRenderInteractiveServerComponent()
29+
{
30+
// '2' configures the increment amount.
31+
Navigate($"{ServerPathBase}/interactive?server=2");
32+
33+
Browser.Equal("0", () => Browser.FindElement(By.Id("count-server")).Text);
34+
Browser.Equal("True", () => Browser.FindElement(By.Id("is-interactive-server")).Text);
35+
36+
Browser.Click(By.Id("increment-server"));
37+
38+
Browser.Equal("2", () => Browser.FindElement(By.Id("count-server")).Text);
39+
}
40+
41+
[Fact]
42+
public void CanRenderInteractiveServerComponentFromRazorClassLibrary()
43+
{
44+
// '3' configures the increment amount.
45+
Navigate($"{ServerPathBase}/interactive?server-shared=3");
46+
47+
Browser.Equal("0", () => Browser.FindElement(By.Id("count-server-shared")).Text);
48+
Browser.Equal("True", () => Browser.FindElement(By.Id("is-interactive-server-shared")).Text);
49+
50+
Browser.Click(By.Id("increment-server-shared"));
51+
52+
Browser.Equal("3", () => Browser.FindElement(By.Id("count-server-shared")).Text);
53+
}
54+
55+
[Fact]
56+
public void CanRenderInteractiveWebAssemblyComponentFromRazorClassLibrary()
57+
{
58+
// '4' configures the increment amount.
59+
Navigate($"{ServerPathBase}/interactive?wasm-shared=4");
60+
61+
Browser.Equal("0", () => Browser.FindElement(By.Id("count-wasm-shared")).Text);
62+
Browser.Equal("True", () => Browser.FindElement(By.Id("is-interactive-wasm-shared")).Text);
63+
64+
Browser.Click(By.Id("increment-wasm-shared"));
65+
66+
Browser.Equal("4", () => Browser.FindElement(By.Id("count-wasm-shared")).Text);
67+
}
68+
69+
[Fact]
70+
public void CanRenderInteractiveServerAndWebAssemblyComponentsAtTheSameTime()
71+
{
72+
// '3' and '5' configure the increment amounts.
73+
Navigate($"{ServerPathBase}/interactive?server-shared=3&wasm-shared=5");
74+
75+
Browser.Equal("0", () => Browser.FindElement(By.Id("count-server-shared")).Text);
76+
Browser.Equal("0", () => Browser.FindElement(By.Id("count-wasm-shared")).Text);
77+
Browser.Equal("True", () => Browser.FindElement(By.Id("is-interactive-server-shared")).Text);
78+
Browser.Equal("True", () => Browser.FindElement(By.Id("is-interactive-wasm-shared")).Text);
79+
80+
Browser.Click(By.Id("increment-server-shared"));
81+
Browser.Click(By.Id("increment-wasm-shared"));
82+
83+
Browser.Equal("3", () => Browser.FindElement(By.Id("count-server-shared")).Text);
84+
Browser.Equal("5", () => Browser.FindElement(By.Id("count-wasm-shared")).Text);
85+
}
86+
}

src/Components/test/testassets/Components.TestServer/Components.TestServer.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727

2828
<ItemGroup>
2929
<ProjectReference Include="..\BasicTestApp\BasicTestApp.csproj" />
30+
<ProjectReference Include="..\Components.WasmMinimal\Components.WasmMinimal.csproj" />
3031
</ItemGroup>
3132

3233
<ItemGroup>

src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,12 @@ public RazorComponentEndpointsStartup(IConfiguration configuration)
2222
// This method gets called by the runtime. Use this method to add services to the container.
2323
public void ConfigureServices(IServiceCollection services)
2424
{
25-
services.AddRazorComponents();
25+
services.AddRazorComponents()
26+
.AddServerComponents()
27+
.AddWebAssemblyComponents(options =>
28+
{
29+
options.PathPrefix = "/WasmMinimal";
30+
});
2631
services.AddHttpContextAccessor();
2732
services.AddSingleton<AsyncOperationService>();
2833
}
@@ -41,10 +46,13 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
4146

4247
app.Map("/subdir", app =>
4348
{
49+
app.UseStaticFiles();
4450
app.UseRouting();
4551
app.UseEndpoints(endpoints =>
4652
{
47-
endpoints.MapRazorComponents<TRootComponent>();
53+
endpoints.MapRazorComponents<TRootComponent>()
54+
.AddServerRenderMode()
55+
.AddWebAssemblyRenderMode();
4856

4957
StreamingRendering.MapEndpoints(endpoints);
5058
StreamingRenderingForm.MapEndpoints(endpoints);

src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@
2020
</LayoutView>
2121
</NotFound>
2222
</Router>
23-
<script src="_framework/blazor.web.js" suppress-error="BL9992"></script>
23+
<script src="_framework/blazor.web.js" autostart="false" suppress-error="BL9992"></script>
24+
<script suppress-error="BL9992">
25+
Blazor.start({
26+
webAssembly: {
27+
loadBootResource: function (type, name, defaultUri, integrity) {
28+
return `WasmMinimal/_framework/${name}`;
29+
},
30+
},
31+
});
32+
</script>
2433
</body>
2534
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
@attribute [RenderModeServer]
2+
3+
<strong>Server counter</strong>
4+
<TestContentPackage.Counter IncrementAmount="IncrementAmount" IdSuffix="server" />
5+
6+
@code {
7+
[Parameter]
8+
public int IncrementAmount { get; set; } = 1;
9+
}

0 commit comments

Comments
 (0)