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();
});
}