diff --git a/src/Microsoft.AspNetCore.Blazor.Server/Circuits/BlazorHub.cs b/src/Microsoft.AspNetCore.Blazor.Server/BlazorHub.cs similarity index 72% rename from src/Microsoft.AspNetCore.Blazor.Server/Circuits/BlazorHub.cs rename to src/Microsoft.AspNetCore.Blazor.Server/BlazorHub.cs index 17989ec62b..848d04afeb 100644 --- a/src/Microsoft.AspNetCore.Blazor.Server/Circuits/BlazorHub.cs +++ b/src/Microsoft.AspNetCore.Blazor.Server/BlazorHub.cs @@ -3,40 +3,59 @@ using System; using System.Threading.Tasks; +using Microsoft.AspNetCore.Blazor.Server.Circuits; using Microsoft.AspNetCore.Blazor.Services; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace Microsoft.AspNetCore.Blazor.Server.Circuits +namespace Microsoft.AspNetCore.Blazor.Server { - internal class BlazorHub : Hub + /// + /// A SignalR hub that accepts connections to a Server-Side Blazor app. + /// + public sealed class BlazorHub : Hub { private static readonly object CircuitKey = new object(); - private readonly CircuitFactory _circuitFactory; private readonly ILogger _logger; + /// + /// Intended for framework use only. Applications should not instantiate + /// this class directly. + /// public BlazorHub( ILogger logger, - CircuitFactory circuitFactory) + IServiceProvider services) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _circuitFactory = circuitFactory ?? throw new ArgumentNullException(nameof(circuitFactory)); + _circuitFactory = services.GetRequiredService(); } - public CircuitHost CircuitHost + /// + /// Gets the default endpoint path for incoming connections. + /// + public static PathString DefaultPath => "/_blazor"; + + private CircuitHost CircuitHost { get => (CircuitHost)Context.Items[CircuitKey]; set => Context.Items[CircuitKey] = value; } + /// + /// Intended for framework use only. Applications should not call this method directly. + /// public override Task OnDisconnectedAsync(Exception exception) { CircuitHost.Dispose(); return base.OnDisconnectedAsync(exception); } + /// + /// Intended for framework use only. Applications should not call this method directly. + /// public async Task StartCircuit(string uriAbsolute, string baseUriAbsolute) { var circuitHost = _circuitFactory.CreateCircuitHost(Context.GetHttpContext(), Clients.Caller); @@ -50,7 +69,10 @@ namespace Microsoft.AspNetCore.Blazor.Server.Circuits await circuitHost.InitializeAsync(); CircuitHost = circuitHost; } - + + /// + /// Intended for framework use only. Applications should not call this method directly. + /// public void BeginInvokeDotNetFromJS(string callId, string assemblyName, string methodIdentifier, long dotNetObjectId, string argsJson) { EnsureCircuitHost().BeginInvokeDotNetFromJS(callId, assemblyName, methodIdentifier, dotNetObjectId, argsJson); diff --git a/src/Microsoft.AspNetCore.Blazor.Server/Builder/BlazorApplicationBuilderExtensions.cs b/src/Microsoft.AspNetCore.Blazor.Server/Builder/BlazorApplicationBuilderExtensions.cs index 21d0a87aab..dbe8cebb67 100644 --- a/src/Microsoft.AspNetCore.Blazor.Server/Builder/BlazorApplicationBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Blazor.Server/Builder/BlazorApplicationBuilderExtensions.cs @@ -24,12 +24,13 @@ namespace Microsoft.AspNetCore.Builder /// Configures the middleware pipeline to work with Blazor. /// /// Any type from the client app project. This is used to identify the client app assembly. - /// - public static void UseBlazor( + /// The . + /// The . + public static IApplicationBuilder UseBlazor( this IApplicationBuilder app) { var clientAssemblyInServerBinDir = typeof(TProgram).Assembly; - app.UseBlazor(new BlazorOptions + return app.UseBlazor(new BlazorOptions { ClientAssemblyPath = clientAssemblyInServerBinDir.Location, }); @@ -38,9 +39,10 @@ namespace Microsoft.AspNetCore.Builder /// /// Configures the middleware pipeline to work with Blazor. /// - /// - /// - public static void UseBlazor( + /// The . + /// Options to configure the middleware. + /// The . + public static IApplicationBuilder UseBlazor( this IApplicationBuilder app, BlazorOptions options) { @@ -108,6 +110,8 @@ namespace Microsoft.AspNetCore.Builder spa.Options.DefaultPageStaticFileOptions = indexHtmlStaticFileOptions; }); }); + + return app; } private static string FindIndexHtmlFile(BlazorConfig config) diff --git a/src/Microsoft.AspNetCore.Blazor.Server/Builder/ServerSideBlazorApplicationBuilderExtensions.cs b/src/Microsoft.AspNetCore.Blazor.Server/Builder/ServerSideBlazorApplicationBuilderExtensions.cs index 8d58a02c92..86d1167d36 100644 --- a/src/Microsoft.AspNetCore.Blazor.Server/Builder/ServerSideBlazorApplicationBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Blazor.Server/Builder/ServerSideBlazorApplicationBuilderExtensions.cs @@ -2,15 +2,14 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using Microsoft.AspNetCore.Blazor.Builder; -using Microsoft.AspNetCore.Blazor.Hosting; -using Microsoft.AspNetCore.Blazor.Server.Circuits; -using Microsoft.Extensions.DependencyInjection; +using Microsoft.AspNetCore.Blazor.Server; +using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.Builder { /// /// Extension methods to configure an for Server-Side Blazor. + /// These are just shorthand for combining UseSignalR with UseBlazor. /// public static class ServerSideBlazorApplicationBuilderExtensions { @@ -20,94 +19,58 @@ namespace Microsoft.AspNetCore.Builder /// The . /// A Blazor startup type. /// The . - public static IApplicationBuilder UseServerSideBlazor(this IApplicationBuilder builder) + public static IApplicationBuilder UseServerSideBlazor( + this IApplicationBuilder builder) { if (builder == null) { throw new ArgumentNullException(nameof(builder)); } - return UseServerSideBlazor(builder, typeof(TStartup)); + // WARNING: Don't add extra setup logic here. It's important for + // UseServerSideBlazor just to be shorthand for UseSignalR+UseBlazor, + // so that people who want to call those two manually instead can + // also do so. That's needed for people using Azure SignalR. + + // TODO: Also allow configuring the endpoint path. + return UseSignalRWithBlazorHub(builder, BlazorHub.DefaultPath) + .UseBlazor(); } /// /// Registers Server-Side Blazor in the pipeline. /// /// The . - /// A Blazor startup type. - /// The . - public static IApplicationBuilder UseServerSideBlazor( - this IApplicationBuilder builder, - Type startupType) - { - if (builder == null) - { - throw new ArgumentNullException(nameof(builder)); - } - - if (startupType == null) - { - throw new ArgumentNullException(nameof(startupType)); - } - - var startup = builder.ApplicationServices.GetRequiredService(startupType); - var wrapper = new ConventionBasedStartup(startup); - Action configure = (b) => - { - wrapper.Configure(b, b.Services); - }; - - UseServerSideBlazorCore(builder, configure); - - builder.UseBlazor(new BlazorOptions() - { - ClientAssemblyPath = startupType.Assembly.Location, - }); - - return builder; - } - - /// - /// Registers middleware for Server-Side Blazor. - /// - /// The . /// A instance used to configure the Blazor file provider. - /// A delegate used to configure the renderer. /// The . public static IApplicationBuilder UseServerSideBlazor( this IApplicationBuilder builder, - BlazorOptions options, - Action startupAction) + BlazorOptions options) { if (builder == null) { throw new ArgumentNullException(nameof(builder)); } - if (startupAction == null) + if (options == null) { - throw new ArgumentNullException(nameof(startupAction)); + throw new ArgumentNullException(nameof(options)); } - UseServerSideBlazorCore(builder, startupAction); + // WARNING: Don't add extra setup logic here. It's important for + // UseServerSideBlazor just to be shorthand for UseSignalR+UseBlazor, + // so that people who want to call those two manually instead can + // also do so. That's needed for people using Azure SignalR. - builder.UseBlazor(options); - - return builder; + // TODO: Also allow configuring the endpoint path. + return UseSignalRWithBlazorHub(builder, BlazorHub.DefaultPath) + .UseBlazor(options); } - private static IApplicationBuilder UseServerSideBlazorCore( - IApplicationBuilder builder, - Action configure) + private static IApplicationBuilder UseSignalRWithBlazorHub( + IApplicationBuilder builder, PathString path) { - var endpoint = "/_blazor"; - - var factory = (DefaultCircuitFactory)builder.ApplicationServices.GetRequiredService(); - factory.StartupActions.Add(endpoint, configure); - - builder.UseSignalR(route => route.MapHub(endpoint)); - - return builder; + return builder.UseSignalR(route => route.MapHub(BlazorHub.DefaultPath)); } } } diff --git a/src/Microsoft.AspNetCore.Blazor.Server/Circuits/DefaultCircuitFactory.cs b/src/Microsoft.AspNetCore.Blazor.Server/Circuits/DefaultCircuitFactory.cs index 2a01b77b3d..885f4637bc 100644 --- a/src/Microsoft.AspNetCore.Blazor.Server/Circuits/DefaultCircuitFactory.cs +++ b/src/Microsoft.AspNetCore.Blazor.Server/Circuits/DefaultCircuitFactory.cs @@ -2,32 +2,36 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Collections.Generic; using Microsoft.AspNetCore.Blazor.Browser.Rendering; -using Microsoft.AspNetCore.Blazor.Builder; using Microsoft.AspNetCore.Blazor.Rendering; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Blazor.Server.Circuits { internal class DefaultCircuitFactory : CircuitFactory { private readonly IServiceScopeFactory _scopeFactory; + private readonly DefaultCircuitFactoryOptions _options; - public DefaultCircuitFactory(IServiceScopeFactory scopeFactory) + public DefaultCircuitFactory( + IServiceScopeFactory scopeFactory, + IOptions options) { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + _scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory)); - - StartupActions = new Dictionary>(); + _options = options.Value; } - public Dictionary> StartupActions { get; } - public override CircuitHost CreateCircuitHost(HttpContext httpContext, IClientProxy client) { - if (!StartupActions.TryGetValue(httpContext.Request.Path, out var config)) + if (!_options.StartupActions.TryGetValue(httpContext.Request.Path, out var config)) { var message = $"Could not find a Blazor startup action for request path {httpContext.Request.Path}"; throw new InvalidOperationException(message); diff --git a/src/Microsoft.AspNetCore.Blazor.Server/Circuits/DefaultCircuitFactoryOptions.cs b/src/Microsoft.AspNetCore.Blazor.Server/Circuits/DefaultCircuitFactoryOptions.cs new file mode 100644 index 0000000000..d12072c69c --- /dev/null +++ b/src/Microsoft.AspNetCore.Blazor.Server/Circuits/DefaultCircuitFactoryOptions.cs @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Blazor.Builder; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Blazor.Server.Circuits +{ + internal class DefaultCircuitFactoryOptions + { + // During the DI configuration phase, we use Configure(...) + // callbacks to build up this dictionary mapping paths to startup actions + public Dictionary> StartupActions { get; } + + public DefaultCircuitFactoryOptions() + { + StartupActions = new Dictionary>(); + } + } +} diff --git a/src/Microsoft.AspNetCore.Blazor.Server/DependencyInjection/ServerSideBlazorServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.Blazor.Server/DependencyInjection/ServerSideBlazorServiceCollectionExtensions.cs index 5579345972..bb0b32b5f0 100644 --- a/src/Microsoft.AspNetCore.Blazor.Server/DependencyInjection/ServerSideBlazorServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNetCore.Blazor.Server/DependencyInjection/ServerSideBlazorServiceCollectionExtensions.cs @@ -2,10 +2,11 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using Microsoft.AspNetCore.Blazor; using Microsoft.AspNetCore.Blazor.Hosting; +using Microsoft.AspNetCore.Blazor.Server; using Microsoft.AspNetCore.Blazor.Server.Circuits; using Microsoft.AspNetCore.Blazor.Services; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.JSInterop; namespace Microsoft.Extensions.DependencyInjection @@ -20,32 +21,15 @@ namespace Microsoft.Extensions.DependencyInjection /// /// The . /// The . - public static IServiceCollection AddServerSideBlazor(this IServiceCollection services) - { - if (services == null) - { - throw new ArgumentNullException(nameof(services)); - } - - return AddServerSideBlazor(services, null, null); - } - - /// - /// Adds Server-Side Blazor services to the service collection. - /// - /// The . - /// A delegate to configure the . - /// The . public static IServiceCollection AddServerSideBlazor( - this IServiceCollection services, - Action configure) + this IServiceCollection services) { if (services == null) { throw new ArgumentNullException(nameof(services)); } - return AddServerSideBlazorCore(services, null, configure); + return AddServerSideBlazor(services, null); } /// @@ -54,7 +38,9 @@ namespace Microsoft.Extensions.DependencyInjection /// The . /// A Blazor startup type. /// The . - public static IServiceCollection AddServerSideBlazor(this IServiceCollection services, Type startupType) + public static IServiceCollection AddServerSideBlazor( + this IServiceCollection services, + Type startupType) { if (services == null) { @@ -66,7 +52,7 @@ namespace Microsoft.Extensions.DependencyInjection throw new ArgumentNullException(nameof(startupType)); } - return AddServerSideBlazorCore(services, startupType, null); + return AddServerSideBlazorCore(services, startupType); } /// @@ -75,101 +61,75 @@ namespace Microsoft.Extensions.DependencyInjection /// The . /// A Blazor startup type. /// The . - public static IServiceCollection AddServerSideBlazor(this IServiceCollection services) - { - if (services == null) - { - throw new ArgumentNullException(nameof(services)); - } - - return AddServerSideBlazorCore(services, typeof(TStartup), null); - } - - /// - /// Adds Server-Side Blazor services to the service collection. - /// - /// The . - /// A Blazor startup type. - /// A delegate to configure the . - /// The . - public static IServiceCollection AddServerSideBlazor( - this IServiceCollection services, - Type startupType, - Action configure) - { - if (services == null) - { - throw new ArgumentNullException(nameof(services)); - } - - if (startupType == null) - { - throw new ArgumentNullException(nameof(startupType)); - } - - return AddServerSideBlazorCore(services, startupType, configure); - } - - /// - /// Adds Server-Side Blazor services to the service collection. - /// - /// The . - /// A delegate to configure the . - /// A Blazor startup type. - /// The . public static IServiceCollection AddServerSideBlazor( - this IServiceCollection services, - Action configure) + this IServiceCollection services) { if (services == null) { throw new ArgumentNullException(nameof(services)); } - return AddServerSideBlazorCore(services, typeof(TStartup), configure); + return AddServerSideBlazorCore(services, typeof(TStartup)); } private static IServiceCollection AddServerSideBlazorCore( IServiceCollection services, - Type startupType, - Action configure) + Type startupType) { - services.AddSingleton(); - services.AddScoped(); - services.AddScoped(s => s.GetRequiredService().Circuit); - - services.AddScoped(); - services.AddScoped(s => s.GetRequiredService().JSRuntime); - - services.AddScoped(); - - services.AddSignalR().AddMessagePackProtocol(options => - { - // TODO: Enable compression, either by having SignalR use - // LZ4MessagePackSerializer instead of MessagePackSerializer, - // or perhaps by compressing the RenderBatch bytes ourselves - // and then using https://github.com/nodeca/pako in JS to decompress. - options.FormatterResolvers.Insert(0, new RenderBatchFormatterResolver()); - }); + AddStandardServerSideBlazorServices(services); if (startupType != null) { - // Make sure we only create a single instance of the startup type. We can register - // it in the services so we can retrieve it later when creating the middlware. + // Call TStartup's ConfigureServices method immediately var startup = Activator.CreateInstance(startupType); - services.AddSingleton(startupType, startup); - - // We don't need to reuse the wrapper, it's not stateful. var wrapper = new ConventionBasedStartup(startup); wrapper.ConfigureServices(services); - } - if (configure != null) - { - services.Configure(configure); + // Configure the circuit factory to call a startup action when each + // incoming connection is established. The startup action is "call + // TStartup's Configure method". + services.Configure(circuitFactoryOptions => + { + var endpoint = BlazorHub.DefaultPath; // TODO: allow configuring this + if (circuitFactoryOptions.StartupActions.ContainsKey(endpoint)) + { + throw new InvalidOperationException( + "Multiple Server Side Blazor entries are configured to use " + + $"the same endpoint '{endpoint}'."); + } + + circuitFactoryOptions.StartupActions.Add(endpoint, builder => + { + wrapper.Configure(builder, builder.Services); + }); + }); } return services; } + + private static void AddStandardServerSideBlazorServices(IServiceCollection services) + { + // Here we add a bunch of services that don't vary in any way based on the + // user's configuration. So even if the user has multiple independent server-side + // Blazor entrypoints, this lot is the same and repeated registrations are a no-op. + services.TryAddSingleton(); + services.TryAddScoped(); + services.TryAddScoped(s => s.GetRequiredService().Circuit); + services.TryAddScoped(); + services.TryAddScoped(s => s.GetRequiredService().JSRuntime); + services.TryAddScoped(); + + // We've discussed with the SignalR team and believe it's OK to have repeated + // calls to AddSignalR (making the nonfirst ones no-ops). If we want to change + // this in the future, we could change AddServerSideBlazor to be an extension + // method on ISignalRServerBuilder so the developer always has to chain it onto + // their own AddSignalR call. For now we're keeping it like this because it's + // simpler for developers in common cases. + services.AddSignalR().AddMessagePackProtocol(options => + { + options.FormatterResolvers.Insert(0, new RenderBatchFormatterResolver()); + }); + } } } diff --git a/src/Microsoft.AspNetCore.Blazor.Server/ServerSideBlazorOptions.cs b/src/Microsoft.AspNetCore.Blazor.Server/ServerSideBlazorOptions.cs deleted file mode 100644 index 32ac61f884..0000000000 --- a/src/Microsoft.AspNetCore.Blazor.Server/ServerSideBlazorOptions.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -namespace Microsoft.AspNetCore.Blazor -{ - /// - /// Options for Server-Side Blazor. - /// - public class ServerSideBlazorOptions - { - } -} diff --git a/test/testapps/TestServer/Startup.cs b/test/testapps/TestServer/Startup.cs index 62af5e4f89..7e93160f28 100644 --- a/test/testapps/TestServer/Startup.cs +++ b/test/testapps/TestServer/Startup.cs @@ -1,3 +1,4 @@ +using Microsoft.AspNetCore.Blazor.Server; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; @@ -39,7 +40,14 @@ namespace TestServer // Mount the server-side Blazor app on /subdir app.Map("/subdir", subdirApp => { - subdirApp.UseServerSideBlazor(); + // The following two lines are equivalent to: + // subdirApp.UseServerSideBlazor(); + // However it's expressed using UseSignalR+UseBlazor as a way of checking that + // we're not relying on any extra magic inside UseServerSideBlazor, since it's + // important that people can set up these bits of middleware manually (e.g., to + // swap in UseAzureSignalR instead of UseSignalR). + subdirApp.UseSignalR(route => route.MapHub(BlazorHub.DefaultPath)); + subdirApp.UseBlazor(); }); }