|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics.CodeAnalysis;
using System.Net.WebSockets;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Endpoints.Infrastructure;
using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Connections;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace Microsoft.Extensions.DependencyInjection;
/// <summary>
/// Extension methods to configure an <see cref="IServiceCollection"/> for components.
/// </summary>
public static class ServerRazorComponentsBuilderExtensions
{
/// <summary>
/// Adds services to support rendering interactive server components in a razor components
/// application.
/// </summary>
/// <param name="builder">The <see cref="IRazorComponentsBuilder"/>.</param>
/// <param name="configure">A callback to configure <see cref="CircuitOptions"/>.</param>
/// <returns>An <see cref="IRazorComponentsBuilder"/> that can be used to further customize the configuration.</returns>
[RequiresUnreferencedCode("Server-side Blazor does not currently support native AOT.", Url = "https://aka.ms/aspnet/nativeaot")]
public static IServerSideBlazorBuilder AddInteractiveServerComponents(this IRazorComponentsBuilder builder, Action<CircuitOptions>? configure = null)
{
ArgumentNullException.ThrowIfNull(builder, nameof(builder));
builder.Services.AddServerSideBlazor(configure);
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<RenderModeEndpointProvider, CircuitEndpointProvider>());
return new DefaultServerSideBlazorBuilder(builder.Services);
}
private sealed class DefaultServerSideBlazorBuilder : IServerSideBlazorBuilder
{
public DefaultServerSideBlazorBuilder(IServiceCollection services)
{
Services = services;
}
public IServiceCollection Services { get; }
}
private class CircuitEndpointProvider(IServiceProvider services) : RenderModeEndpointProvider
{
public IServiceProvider Services { get; } = services;
public override IEnumerable<RouteEndpointBuilder> GetEndpointBuilders(
IComponentRenderMode renderMode,
IApplicationBuilder applicationBuilder)
{
if (renderMode is not InternalServerRenderMode)
{
if (renderMode is InteractiveServerRenderMode)
{
throw new InvalidOperationException("Invalid render mode. Use AddInteractiveServerRenderMode() to configure the Server render mode.");
}
return [];
}
var endpointRouteBuilder = new EndpointRouteBuilder(Services, applicationBuilder);
var hub = endpointRouteBuilder.MapBlazorHub("/_blazor");
if (renderMode is InternalServerRenderMode { Options.ConfigureWebSocketAcceptContext: var configureConnection, Options.DisableWebSocketCompression: var disableCompression } &&
(configureConnection is not null || !disableCompression))
{
hub.Finally(c =>
{
for (var i = 0; i < c.Metadata.Count; i++)
{
var metadata = c.Metadata[i];
if (metadata is NegotiateMetadata)
{
return;
}
if (metadata is HubMetadata)
{
var originalDelegate = c.RequestDelegate;
var builder = endpointRouteBuilder.CreateApplicationBuilder();
builder.UseWebSockets();
builder.Use((ctx, nxt) =>
{
if (ctx.WebSockets.IsWebSocketRequest)
{
var currentFeature = ctx.Features.Get<IHttpWebSocketFeature>();
ctx.Features.Set<IHttpWebSocketFeature>(new ServerComponentsSocketFeature(currentFeature!, ctx, configureConnection, disableCompression));
}
return nxt(ctx);
});
builder.Run(originalDelegate);
c.RequestDelegate = builder.Build();
return;
}
}
});
}
return endpointRouteBuilder.GetEndpoints();
}
public override bool Supports(IComponentRenderMode renderMode)
{
return renderMode switch
{
InteractiveServerRenderMode _ or InteractiveAutoRenderMode _ => true,
_ => false,
};
}
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;
}
}
}
}
private sealed class ServerComponentsSocketFeature(
IHttpWebSocketFeature originalFeature,
HttpContext httpContext,
Func<HttpContext, WebSocketAcceptContext, Task>? configureConnection,
bool compressionDisabled)
: IHttpWebSocketFeature
{
public bool IsWebSocketRequest => originalFeature.IsWebSocketRequest;
public Task<WebSocket> AcceptAsync(WebSocketAcceptContext context)
{
context.DangerousEnableCompression = !compressionDisabled;
if (configureConnection is null)
{
return originalFeature.AcceptAsync(context);
}
else
{
var result = configureConnection.Invoke(httpContext, context);
if (!result.IsCompletedSuccessfully)
{
return ReturnAwaited(result, context);
}
else
{
return originalFeature.AcceptAsync(context);
}
}
}
private async Task<WebSocket> ReturnAwaited(Task result, WebSocketAcceptContext context)
{
await result;
return await originalFeature.AcceptAsync(context);
}
}
}
}
|