diff --git a/AspNetCore.sln b/AspNetCore.sln
index 0682ff1a645f..403bbfaa9977 100644
--- a/AspNetCore.sln
+++ b/AspNetCore.sln
@@ -1786,6 +1786,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Authen
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IdentitySample.ApiEndpoints", "src\Identity\samples\IdentitySample.ApiEndpoints\IdentitySample.ApiEndpoints.csproj", "{37FC77EA-AC44-4D08-B002-8EFF415C424A}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Components.WasmMinimal", "src\Components\test\testassets\Components.WasmMinimal\Components.WasmMinimal.csproj", "{87D58D50-20D1-4091-88C5-8D88DCCC2DE3}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -10739,6 +10741,22 @@ Global
{37FC77EA-AC44-4D08-B002-8EFF415C424A}.Release|x64.Build.0 = Release|Any CPU
{37FC77EA-AC44-4D08-B002-8EFF415C424A}.Release|x86.ActiveCfg = Release|Any CPU
{37FC77EA-AC44-4D08-B002-8EFF415C424A}.Release|x86.Build.0 = Release|Any CPU
+ {87D58D50-20D1-4091-88C5-8D88DCCC2DE3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {87D58D50-20D1-4091-88C5-8D88DCCC2DE3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {87D58D50-20D1-4091-88C5-8D88DCCC2DE3}.Debug|arm64.ActiveCfg = Debug|Any CPU
+ {87D58D50-20D1-4091-88C5-8D88DCCC2DE3}.Debug|arm64.Build.0 = Debug|Any CPU
+ {87D58D50-20D1-4091-88C5-8D88DCCC2DE3}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {87D58D50-20D1-4091-88C5-8D88DCCC2DE3}.Debug|x64.Build.0 = Debug|Any CPU
+ {87D58D50-20D1-4091-88C5-8D88DCCC2DE3}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {87D58D50-20D1-4091-88C5-8D88DCCC2DE3}.Debug|x86.Build.0 = Debug|Any CPU
+ {87D58D50-20D1-4091-88C5-8D88DCCC2DE3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {87D58D50-20D1-4091-88C5-8D88DCCC2DE3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {87D58D50-20D1-4091-88C5-8D88DCCC2DE3}.Release|arm64.ActiveCfg = Release|Any CPU
+ {87D58D50-20D1-4091-88C5-8D88DCCC2DE3}.Release|arm64.Build.0 = Release|Any CPU
+ {87D58D50-20D1-4091-88C5-8D88DCCC2DE3}.Release|x64.ActiveCfg = Release|Any CPU
+ {87D58D50-20D1-4091-88C5-8D88DCCC2DE3}.Release|x64.Build.0 = Release|Any CPU
+ {87D58D50-20D1-4091-88C5-8D88DCCC2DE3}.Release|x86.ActiveCfg = Release|Any CPU
+ {87D58D50-20D1-4091-88C5-8D88DCCC2DE3}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -11621,6 +11639,7 @@ Global
{56291265-B7BF-4756-92AB-FC30F09381D1} = {822D1519-77F0-484A-B9AB-F694C2CC25F1}
{66FA1041-5556-43A0-9CA3-F9937F085F6E} = {56291265-B7BF-4756-92AB-FC30F09381D1}
{37FC77EA-AC44-4D08-B002-8EFF415C424A} = {64B2A28F-6D82-4F2B-B0BB-88DE5216DD2C}
+ {87D58D50-20D1-4091-88C5-8D88DCCC2DE3} = {6126DCE4-9692-4EE2-B240-C65743572995}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F}
diff --git a/src/Components/Components.slnf b/src/Components/Components.slnf
index e0ae358526d6..005fb7b1e8b9 100644
--- a/src/Components/Components.slnf
+++ b/src/Components/Components.slnf
@@ -53,6 +53,7 @@
"src\\Components\\test\\E2ETest\\Microsoft.AspNetCore.Components.E2ETests.csproj",
"src\\Components\\test\\testassets\\BasicTestApp\\BasicTestApp.csproj",
"src\\Components\\test\\testassets\\Components.TestServer\\Components.TestServer.csproj",
+ "src\\Components\\test\\testassets\\Components.WasmMinimal\\Components.WasmMinimal.csproj",
"src\\Components\\test\\testassets\\ComponentsApp.App\\ComponentsApp.App.csproj",
"src\\Components\\test\\testassets\\ComponentsApp.Server\\ComponentsApp.Server.csproj",
"src\\Components\\test\\testassets\\GlobalizationWasmApp\\GlobalizationWasmApp.csproj",
@@ -148,4 +149,4 @@
"src\\WebEncoders\\src\\Microsoft.Extensions.WebEncoders.csproj"
]
}
-}
+}
\ No newline at end of file
diff --git a/src/Components/Endpoints/src/DependencyInjection/WebAssemblyComponentsEndpointOptions.cs b/src/Components/Endpoints/src/DependencyInjection/WebAssemblyComponentsEndpointOptions.cs
new file mode 100644
index 000000000000..69bcc72990f5
--- /dev/null
+++ b/src/Components/Endpoints/src/DependencyInjection/WebAssemblyComponentsEndpointOptions.cs
@@ -0,0 +1,18 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.Components.Endpoints;
+
+///
+/// Options to configure interactive WebAssembly components.
+///
+public sealed class WebAssemblyComponentsEndpointOptions
+{
+ ///
+ /// Gets or sets the that indicates the prefix for Blazor WebAssembly assets.
+ /// This path must correspond to a referenced Blazor WebAssembly application project.
+ ///
+ public PathString PathPrefix { get; set; }
+}
diff --git a/src/Components/Endpoints/src/PublicAPI.Unshipped.txt b/src/Components/Endpoints/src/PublicAPI.Unshipped.txt
index e4528ad1be96..f852122daa03 100644
--- a/src/Components/Endpoints/src/PublicAPI.Unshipped.txt
+++ b/src/Components/Endpoints/src/PublicAPI.Unshipped.txt
@@ -76,6 +76,10 @@ Microsoft.AspNetCore.Components.Endpoints.RenderModeEndpointProvider.RenderModeE
Microsoft.AspNetCore.Components.Endpoints.RootComponentMetadata
Microsoft.AspNetCore.Components.Endpoints.RootComponentMetadata.RootComponentMetadata(System.Type! rootComponentType) -> void
Microsoft.AspNetCore.Components.Endpoints.RootComponentMetadata.Type.get -> System.Type!
+Microsoft.AspNetCore.Components.Endpoints.WebAssemblyComponentsEndpointOptions
+Microsoft.AspNetCore.Components.Endpoints.WebAssemblyComponentsEndpointOptions.PathPrefix.get -> Microsoft.AspNetCore.Http.PathString
+Microsoft.AspNetCore.Components.Endpoints.WebAssemblyComponentsEndpointOptions.PathPrefix.set -> void
+Microsoft.AspNetCore.Components.Endpoints.WebAssemblyComponentsEndpointOptions.WebAssemblyComponentsEndpointOptions() -> void
Microsoft.AspNetCore.Components.Infrastructure.RazorComponentApplicationAttribute
Microsoft.AspNetCore.Components.Infrastructure.RazorComponentApplicationAttribute.RazorComponentApplicationAttribute() -> void
Microsoft.AspNetCore.Components.PersistedStateSerializationMode
@@ -91,4 +95,4 @@ static Microsoft.AspNetCore.Builder.RazorComponentsEndpointRouteBuilderExtension
static Microsoft.AspNetCore.Components.Discovery.ComponentApplicationBuilder.GetBuilder() -> Microsoft.AspNetCore.Components.Discovery.ComponentApplicationBuilder?
static Microsoft.Extensions.DependencyInjection.RazorComponentsServiceCollectionExtensions.AddRazorComponents(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.AspNetCore.Components.Endpoints.IRazorComponentsBuilder!
static readonly Microsoft.AspNetCore.Components.Endpoints.RazorComponentResultExecutor.DefaultContentType -> string!
-virtual Microsoft.AspNetCore.Components.Endpoints.RazorComponentResultExecutor.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext, Microsoft.AspNetCore.Components.Endpoints.RazorComponentResult! result) -> System.Threading.Tasks.Task!
\ No newline at end of file
+virtual Microsoft.AspNetCore.Components.Endpoints.RazorComponentResultExecutor.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext, Microsoft.AspNetCore.Components.Endpoints.RazorComponentResult! result) -> System.Threading.Tasks.Task!
diff --git a/src/Components/WebAssembly/Server/src/Microsoft.AspNetCore.Components.WebAssembly.Server.csproj b/src/Components/WebAssembly/Server/src/Microsoft.AspNetCore.Components.WebAssembly.Server.csproj
index ba032dcf8f79..2bd9c32a565a 100644
--- a/src/Components/WebAssembly/Server/src/Microsoft.AspNetCore.Components.WebAssembly.Server.csproj
+++ b/src/Components/WebAssembly/Server/src/Microsoft.AspNetCore.Components.WebAssembly.Server.csproj
@@ -12,6 +12,7 @@
+
diff --git a/src/Components/WebAssembly/Server/src/PublicAPI.Unshipped.txt b/src/Components/WebAssembly/Server/src/PublicAPI.Unshipped.txt
index bbfd1c31db86..814dd116adf0 100644
--- a/src/Components/WebAssembly/Server/src/PublicAPI.Unshipped.txt
+++ b/src/Components/WebAssembly/Server/src/PublicAPI.Unshipped.txt
@@ -1,2 +1,4 @@
#nullable enable
Microsoft.AspNetCore.Components.WebAssembly.Server.TargetPickerUi.DisplayFirefox(Microsoft.AspNetCore.Http.HttpContext! context) -> System.Threading.Tasks.Task!
+Microsoft.Extensions.DependencyInjection.RazorComponentsBuilderExtensions
+static Microsoft.Extensions.DependencyInjection.RazorComponentsBuilderExtensions.AddWebAssemblyComponents(this Microsoft.AspNetCore.Components.Endpoints.IRazorComponentsBuilder! builder, System.Action? configure = null) -> Microsoft.AspNetCore.Components.Endpoints.IRazorComponentsBuilder!
diff --git a/src/Components/WebAssembly/Server/src/RazorComponentsBuilderExtensions.cs b/src/Components/WebAssembly/Server/src/RazorComponentsBuilderExtensions.cs
new file mode 100644
index 000000000000..9d1a7004c435
--- /dev/null
+++ b/src/Components/WebAssembly/Server/src/RazorComponentsBuilderExtensions.cs
@@ -0,0 +1,112 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Components;
+using Microsoft.AspNetCore.Components.Endpoints;
+using Microsoft.AspNetCore.Components.Web;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Extensions.DependencyInjection;
+
+///
+/// Extension methods to configure an for WebAssembly components.
+///
+public static class RazorComponentsBuilderExtensions
+{
+ ///
+ /// Adds services to support rendering interactive WebAssembly components.
+ ///
+ /// The .
+ /// A callback to configure .
+ /// An that can be used to further customize the configuration.
+ public static IRazorComponentsBuilder AddWebAssemblyComponents(this IRazorComponentsBuilder builder, Action? configure = null)
+ {
+ ArgumentNullException.ThrowIfNull(builder, nameof(builder));
+
+ builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton());
+
+ if (configure is not null)
+ {
+ builder.Services.Configure(configure);
+ }
+
+ return builder;
+ }
+
+ private class WebAssemblyEndpointProvider : RenderModeEndpointProvider
+ {
+ private readonly IServiceProvider _services;
+ private readonly WebAssemblyComponentsEndpointOptions _options;
+
+ public WebAssemblyEndpointProvider(IServiceProvider services, IOptions options)
+ {
+ _services = services;
+ _options = options.Value;
+ }
+
+ public override IEnumerable GetEndpointBuilders(IComponentRenderMode renderMode, IApplicationBuilder applicationBuilder)
+ {
+ var endpointRouteBuilder = new EndpointRouteBuilder(_services, applicationBuilder);
+ var pathPrefix = _options.PathPrefix;
+
+ applicationBuilder.UseBlazorFrameworkFiles(pathPrefix);
+ var app = applicationBuilder.Build();
+
+ endpointRouteBuilder.Map($"{pathPrefix}/_framework/{{*path}}", context =>
+ {
+ // Set endpoint to null so the static files middleware will handle the request.
+ context.SetEndpoint(null);
+
+ return app(context);
+ });
+
+ return endpointRouteBuilder.GetEndpoints();
+ }
+
+ public override bool Supports(IComponentRenderMode renderMode)
+ => renderMode is WebAssemblyRenderMode or AutoRenderMode;
+
+ private class EndpointRouteBuilder : IEndpointRouteBuilder
+ {
+ private readonly IApplicationBuilder _applicationBuilder;
+
+ public EndpointRouteBuilder(IServiceProvider serviceProvider, IApplicationBuilder applicationBuilder)
+ {
+ ServiceProvider = serviceProvider;
+ _applicationBuilder = applicationBuilder;
+ }
+
+ public IServiceProvider ServiceProvider { get; }
+
+ public ICollection DataSources { get; } = new List() { };
+
+ public IApplicationBuilder CreateApplicationBuilder()
+ {
+ return _applicationBuilder.New();
+ }
+
+ internal IEnumerable GetEndpoints()
+ {
+ foreach (var ds in DataSources)
+ {
+ foreach (var endpoint in ds.Endpoints)
+ {
+ var routeEndpoint = (RouteEndpoint)endpoint;
+ var builder = new RouteEndpointBuilder(endpoint.RequestDelegate, routeEndpoint.RoutePattern, routeEndpoint.Order);
+ for (var i = 0; i < routeEndpoint.Metadata.Count; i++)
+ {
+ var metadata = routeEndpoint.Metadata[i];
+ builder.Metadata.Add(metadata);
+ }
+
+ yield return builder;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/Components/test/E2ETest/ServerRenderingTests/InteractivityTest.cs b/src/Components/test/E2ETest/ServerRenderingTests/InteractivityTest.cs
new file mode 100644
index 000000000000..b1735e538f2a
--- /dev/null
+++ b/src/Components/test/E2ETest/ServerRenderingTests/InteractivityTest.cs
@@ -0,0 +1,86 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Components.TestServer.RazorComponents;
+using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
+using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
+using Microsoft.AspNetCore.E2ETesting;
+using OpenQA.Selenium;
+using TestServer;
+using Xunit.Abstractions;
+
+namespace Microsoft.AspNetCore.Components.E2ETests.ServerRenderingTests;
+
+public class InteractivityTest : ServerTestBase>>
+{
+ public InteractivityTest(
+ BrowserFixture browserFixture,
+ BasicTestAppServerSiteFixture> serverFixture,
+ ITestOutputHelper output)
+ : base(browserFixture, serverFixture, output)
+ {
+ }
+
+ public override Task InitializeAsync()
+ => InitializeAsync(BrowserFixture.StreamingContext);
+
+ [Fact]
+ public void CanRenderInteractiveServerComponent()
+ {
+ // '2' configures the increment amount.
+ Navigate($"{ServerPathBase}/interactive?server=2");
+
+ Browser.Equal("0", () => Browser.FindElement(By.Id("count-server")).Text);
+ Browser.Equal("True", () => Browser.FindElement(By.Id("is-interactive-server")).Text);
+
+ Browser.Click(By.Id("increment-server"));
+
+ Browser.Equal("2", () => Browser.FindElement(By.Id("count-server")).Text);
+ }
+
+ [Fact]
+ public void CanRenderInteractiveServerComponentFromRazorClassLibrary()
+ {
+ // '3' configures the increment amount.
+ Navigate($"{ServerPathBase}/interactive?server-shared=3");
+
+ Browser.Equal("0", () => Browser.FindElement(By.Id("count-server-shared")).Text);
+ Browser.Equal("True", () => Browser.FindElement(By.Id("is-interactive-server-shared")).Text);
+
+ Browser.Click(By.Id("increment-server-shared"));
+
+ Browser.Equal("3", () => Browser.FindElement(By.Id("count-server-shared")).Text);
+ }
+
+ [Fact]
+ public void CanRenderInteractiveWebAssemblyComponentFromRazorClassLibrary()
+ {
+ // '4' configures the increment amount.
+ Navigate($"{ServerPathBase}/interactive?wasm-shared=4");
+
+ Browser.Equal("0", () => Browser.FindElement(By.Id("count-wasm-shared")).Text);
+ Browser.Equal("True", () => Browser.FindElement(By.Id("is-interactive-wasm-shared")).Text);
+
+ Browser.Click(By.Id("increment-wasm-shared"));
+
+ Browser.Equal("4", () => Browser.FindElement(By.Id("count-wasm-shared")).Text);
+ }
+
+ [Fact]
+ public void CanRenderInteractiveServerAndWebAssemblyComponentsAtTheSameTime()
+ {
+ // '3' and '5' configure the increment amounts.
+ Navigate($"{ServerPathBase}/interactive?server-shared=3&wasm-shared=5");
+
+ Browser.Equal("0", () => Browser.FindElement(By.Id("count-server-shared")).Text);
+ Browser.Equal("0", () => Browser.FindElement(By.Id("count-wasm-shared")).Text);
+ Browser.Equal("True", () => Browser.FindElement(By.Id("is-interactive-server-shared")).Text);
+ Browser.Equal("True", () => Browser.FindElement(By.Id("is-interactive-wasm-shared")).Text);
+
+ Browser.Click(By.Id("increment-server-shared"));
+ Browser.Click(By.Id("increment-wasm-shared"));
+
+ Browser.Equal("3", () => Browser.FindElement(By.Id("count-server-shared")).Text);
+ Browser.Equal("5", () => Browser.FindElement(By.Id("count-wasm-shared")).Text);
+ }
+}
diff --git a/src/Components/test/testassets/Components.TestServer/Components.TestServer.csproj b/src/Components/test/testassets/Components.TestServer/Components.TestServer.csproj
index c0d29d8eae4d..fd1ad17595e6 100644
--- a/src/Components/test/testassets/Components.TestServer/Components.TestServer.csproj
+++ b/src/Components/test/testassets/Components.TestServer/Components.TestServer.csproj
@@ -27,6 +27,7 @@
+
diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs
index 87aa55f5d1f3..0da1eb13c0cd 100644
--- a/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs
+++ b/src/Components/test/testassets/Components.TestServer/RazorComponentEndpointsStartup.cs
@@ -22,7 +22,12 @@ public RazorComponentEndpointsStartup(IConfiguration configuration)
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
- services.AddRazorComponents();
+ services.AddRazorComponents()
+ .AddServerComponents()
+ .AddWebAssemblyComponents(options =>
+ {
+ options.PathPrefix = "/WasmMinimal";
+ });
services.AddHttpContextAccessor();
services.AddSingleton();
}
@@ -41,10 +46,13 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
app.Map("/subdir", app =>
{
+ app.UseStaticFiles();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
- endpoints.MapRazorComponents();
+ endpoints.MapRazorComponents()
+ .AddServerRenderMode()
+ .AddWebAssemblyRenderMode();
StreamingRendering.MapEndpoints(endpoints);
StreamingRenderingForm.MapEndpoints(endpoints);
diff --git a/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor b/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor
index 142eca4b7fea..bbef6b531009 100644
--- a/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor
+++ b/src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor
@@ -20,6 +20,15 @@
-
+
+