Skip to content
Merged
19 changes: 19 additions & 0 deletions AspNetCore.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand Down
3 changes: 2 additions & 1 deletion src/Components/Components.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -148,4 +149,4 @@
"src\\WebEncoders\\src\\Microsoft.Extensions.WebEncoders.csproj"
]
}
}
}
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Options to configure interactive WebAssembly components.
/// </summary>
public sealed class WebAssemblyComponentsEndpointOptions
{
/// <summary>
/// Gets or sets the <see cref="PathString"/> that indicates the prefix for Blazor WebAssembly assets.
/// This path must correspond to a referenced Blazor WebAssembly application project.
/// </summary>
public PathString PathPrefix { get; set; }
}
6 changes: 5 additions & 1 deletion src/Components/Endpoints/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -91,4 +95,4 @@ static Microsoft.AspNetCore.Builder.RazorComponentsEndpointRouteBuilderExtension
static Microsoft.AspNetCore.Components.Discovery.ComponentApplicationBuilder.GetBuilder<TComponent>() -> 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!
virtual Microsoft.AspNetCore.Components.Endpoints.RazorComponentResultExecutor.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext, Microsoft.AspNetCore.Components.Endpoints.RazorComponentResult! result) -> System.Threading.Tasks.Task!
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
</PropertyGroup>

<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Components.Endpoints" />
<Reference Include="Microsoft.AspNetCore.StaticFiles" />
<Reference Include="Microsoft.NETCore.BrowserDebugHost.Transport" GeneratePathProperty="true" PrivateAssets="All" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Microsoft.AspNetCore.Components.Endpoints.WebAssemblyComponentsEndpointOptions!>? configure = null) -> Microsoft.AspNetCore.Components.Endpoints.IRazorComponentsBuilder!
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Extension methods to configure an <see cref="IServiceCollection"/> for WebAssembly components.
/// </summary>
public static class RazorComponentsBuilderExtensions
{
/// <summary>
/// Adds services to support rendering interactive WebAssembly components.
/// </summary>
/// <param name="builder">The <see cref="IRazorComponentsBuilder"/>.</param>
/// <param name="configure">A callback to configure <see cref="WebAssemblyComponentsEndpointOptions"/>.</param>
/// <returns>An <see cref="IRazorComponentsBuilder"/> that can be used to further customize the configuration.</returns>
public static IRazorComponentsBuilder AddWebAssemblyComponents(this IRazorComponentsBuilder builder, Action<WebAssemblyComponentsEndpointOptions>? configure = null)
{
ArgumentNullException.ThrowIfNull(builder, nameof(builder));

builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<RenderModeEndpointProvider, WebAssemblyEndpointProvider>());

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<WebAssemblyComponentsEndpointOptions> options)
{
_services = services;
_options = options.Value;
}

public override IEnumerable<RouteEndpointBuilder> 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<EndpointDataSource> DataSources { get; } = new List<EndpointDataSource>() { };

public IApplicationBuilder CreateApplicationBuilder()
{
return _applicationBuilder.New();
}

internal IEnumerable<RouteEndpointBuilder> 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;
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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<BasicTestAppServerSiteFixture<RazorComponentEndpointsStartup<App>>>
{
public InteractivityTest(
BrowserFixture browserFixture,
BasicTestAppServerSiteFixture<RazorComponentEndpointsStartup<App>> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

<ItemGroup>
<ProjectReference Include="..\BasicTestApp\BasicTestApp.csproj" />
<ProjectReference Include="..\Components.WasmMinimal\Components.WasmMinimal.csproj" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<AsyncOperationService>();
}
Expand All @@ -41,10 +46,13 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)

app.Map("/subdir", app =>
{
app.UseStaticFiles();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorComponents<TRootComponent>();
endpoints.MapRazorComponents<TRootComponent>()
.AddServerRenderMode()
.AddWebAssemblyRenderMode();

StreamingRendering.MapEndpoints(endpoints);
StreamingRenderingForm.MapEndpoints(endpoints);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@
</LayoutView>
</NotFound>
</Router>
<script src="_framework/blazor.web.js" suppress-error="BL9992"></script>
<script src="_framework/blazor.web.js" autostart="false" suppress-error="BL9992"></script>
<script suppress-error="BL9992">
Blazor.start({
webAssembly: {
loadBootResource: function (type, name, defaultUri, integrity) {
return `WasmMinimal/_framework/${name}`;
},
},
});
</script>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@attribute [RenderModeServer]

<strong>Server counter</strong>
<TestContentPackage.Counter IncrementAmount="IncrementAmount" IdSuffix="server" />

@code {
[Parameter]
public int IncrementAmount { get; set; } = 1;
}
Loading