[Blazor] Enables the client to initiate blazor server-side renders (#13147)

* [Blazor] Allows multiple components as entry points
* Removes all overloads that register a component statically with aborts
  selector.
* Updates render component to have a RenderMode parameter that indicates
  how the component must render. Valid values are Static, Server, and
  ServerPrerendered.
* When using Server or ServerPrerendered we emit marker comments into
  the page that are later used by blazor.server.js to bootrstrap a
  blazor server-side application.
This commit is contained in:
Javier Calvarro Nelson 2019-08-17 20:44:59 +02:00 committed by GitHub
parent 8283e6ac2b
commit 74b801506b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
66 changed files with 1974 additions and 641 deletions

View File

@ -13,7 +13,7 @@ namespace Microsoft.AspNetCore.Analyzers.TestFiles.CompilationFeatureDetectorTes
app.UseEndpoints(endpoints =>
{
endpoints.MapBlazorHub<App>("app");
endpoints.MapBlazorHub();
});
}

View File

@ -7,7 +7,7 @@
<Compile Include="Microsoft.AspNetCore.Components.Server.netcoreapp3.0.cs" />
<Reference Include="Microsoft.AspNetCore.Components.Authorization" />
<Reference Include="Microsoft.AspNetCore.Components.Web" />
<Reference Include="Microsoft.AspNetCore.DataProtection" />
<Reference Include="Microsoft.AspNetCore.DataProtection.Extensions" />
<Reference Include="Microsoft.AspNetCore.SignalR" />
<Reference Include="Microsoft.AspNetCore.StaticFiles" />
<Reference Include="Microsoft.Extensions.Caching.Memory" />

View File

@ -8,20 +8,12 @@ namespace Microsoft.AspNetCore.Builder
internal ComponentEndpointConventionBuilder() { }
public void Add(System.Action<Microsoft.AspNetCore.Builder.EndpointBuilder> convention) { }
}
public static partial class ComponentEndpointConventionBuilderExtensions
{
public static Microsoft.AspNetCore.Builder.ComponentEndpointConventionBuilder AddComponent(this Microsoft.AspNetCore.Builder.ComponentEndpointConventionBuilder builder, System.Type componentType, string selector) { throw null; }
}
public static partial class ComponentEndpointRouteBuilderExtensions
{
public static Microsoft.AspNetCore.Builder.ComponentEndpointConventionBuilder MapBlazorHub(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, System.Type type, string selector) { throw null; }
public static Microsoft.AspNetCore.Builder.ComponentEndpointConventionBuilder MapBlazorHub(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, System.Type type, string selector, System.Action<Microsoft.AspNetCore.Http.Connections.HttpConnectionDispatcherOptions> configureOptions) { throw null; }
public static Microsoft.AspNetCore.Builder.ComponentEndpointConventionBuilder MapBlazorHub(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, System.Type componentType, string selector, string path) { throw null; }
public static Microsoft.AspNetCore.Builder.ComponentEndpointConventionBuilder MapBlazorHub(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, System.Type componentType, string selector, string path, System.Action<Microsoft.AspNetCore.Http.Connections.HttpConnectionDispatcherOptions> configureOptions) { throw null; }
public static Microsoft.AspNetCore.Builder.ComponentEndpointConventionBuilder MapBlazorHub<TComponent>(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, string selector) where TComponent : Microsoft.AspNetCore.Components.IComponent { throw null; }
public static Microsoft.AspNetCore.Builder.ComponentEndpointConventionBuilder MapBlazorHub<TComponent>(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, string selector, System.Action<Microsoft.AspNetCore.Http.Connections.HttpConnectionDispatcherOptions> configureOptions) where TComponent : Microsoft.AspNetCore.Components.IComponent { throw null; }
public static Microsoft.AspNetCore.Builder.ComponentEndpointConventionBuilder MapBlazorHub<TComponent>(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, string selector, string path) where TComponent : Microsoft.AspNetCore.Components.IComponent { throw null; }
public static Microsoft.AspNetCore.Builder.ComponentEndpointConventionBuilder MapBlazorHub<TComponent>(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, string selector, string path, System.Action<Microsoft.AspNetCore.Http.Connections.HttpConnectionDispatcherOptions> configureOptions) where TComponent : Microsoft.AspNetCore.Components.IComponent { throw null; }
public static Microsoft.AspNetCore.Builder.ComponentEndpointConventionBuilder MapBlazorHub(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints) { throw null; }
public static Microsoft.AspNetCore.Builder.ComponentEndpointConventionBuilder MapBlazorHub(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, System.Action<Microsoft.AspNetCore.Http.Connections.HttpConnectionDispatcherOptions> configureOptions) { throw null; }
public static Microsoft.AspNetCore.Builder.ComponentEndpointConventionBuilder MapBlazorHub(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, string path) { throw null; }
public static Microsoft.AspNetCore.Builder.ComponentEndpointConventionBuilder MapBlazorHub(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, string path, System.Action<Microsoft.AspNetCore.Http.Connections.HttpConnectionDispatcherOptions> configureOptions) { throw null; }
}
}
namespace Microsoft.AspNetCore.Components.Server

View File

@ -1,52 +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.
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Components.Server;
namespace Microsoft.AspNetCore.Builder
{
/// <summary>
/// Extensions for <see cref="ComponentEndpointConventionBuilder"/>.
/// </summary>
public static class ComponentEndpointConventionBuilderExtensions
{
/// <summary>
/// Adds <paramref name="componentType"/> to the list of components registered with this hub instance.
/// </summary>
/// <param name="builder">The <see cref="ComponentEndpointConventionBuilder"/>.</param>
/// <param name="componentType">The component type.</param>
/// <param name="selector">The component selector in the DOM for the <paramref name="componentType"/>.</param>
/// <returns>The <paramref name="builder"/>.</returns>
public static ComponentEndpointConventionBuilder AddComponent(this ComponentEndpointConventionBuilder builder, Type componentType, string selector)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
if (componentType == null)
{
throw new ArgumentNullException(nameof(componentType));
}
if (selector == null)
{
throw new ArgumentNullException(nameof(selector));
}
builder.Add(endpointBuilder => AddComponent(endpointBuilder.Metadata, componentType, selector));
return builder;
}
private static void AddComponent(IList<object> metadata, Type type, string selector)
{
metadata.Add(new ComponentDescriptor
{
ComponentType = type,
Selector = selector
});
}
}
}

View File

@ -2,7 +2,6 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Http.Connections;
using Microsoft.AspNetCore.Routing;
@ -16,215 +15,28 @@ namespace Microsoft.AspNetCore.Builder
public static class ComponentEndpointRouteBuilderExtensions
{
/// <summary>
///Maps the Blazor <see cref="Hub" /> to the default path and associates
/// the component <typeparamref name="TComponent"/> to this hub instance as the given DOM <paramref name="selector"/>.
/// Maps the Blazor <see cref="Hub" /> to the default path.
/// </summary>
/// <typeparam name="TComponent">The first <see cref="IComponent"/> associated with this Blazor <see cref="Hub" />.</typeparam>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/>.</param>
/// <param name="selector">The selector for the <typeparamref name="TComponent"/>.</param>
/// <returns>The <see cref="ComponentEndpointConventionBuilder"/>.</returns>
public static ComponentEndpointConventionBuilder MapBlazorHub<TComponent>(
this IEndpointRouteBuilder endpoints,
string selector) where TComponent : IComponent
public static ComponentEndpointConventionBuilder MapBlazorHub(this IEndpointRouteBuilder endpoints)
{
if (endpoints == null)
{
throw new ArgumentNullException(nameof(endpoints));
}
if (selector == null)
{
throw new ArgumentNullException(nameof(selector));
}
return endpoints.MapBlazorHub(typeof(TComponent), selector, ComponentHub.DefaultPath);
return endpoints.MapBlazorHub(ComponentHub.DefaultPath);
}
/// <summary>
///Maps the Blazor <see cref="Hub" /> to the default path and associates
/// the component <typeparamref name="TComponent"/> to this hub instance as the given DOM <paramref name="selector"/>.
/// </summary>
/// <typeparam name="TComponent">The first <see cref="IComponent"/> associated with this Blazor <see cref="Hub" />.</typeparam>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/>.</param>
/// <param name="selector">The selector for the <typeparamref name="TComponent"/>.</param>
/// <param name="configureOptions">A callback to configure dispatcher options.</param>
/// <returns>The <see cref="ComponentEndpointConventionBuilder"/>.</returns>
public static ComponentEndpointConventionBuilder MapBlazorHub<TComponent>(
this IEndpointRouteBuilder endpoints,
string selector,
Action<HttpConnectionDispatcherOptions> configureOptions) where TComponent : IComponent
{
if (endpoints == null)
{
throw new ArgumentNullException(nameof(endpoints));
}
if (selector == null)
{
throw new ArgumentNullException(nameof(selector));
}
if (configureOptions == null)
{
throw new ArgumentNullException(nameof(configureOptions));
}
return endpoints.MapBlazorHub(typeof(TComponent), selector, ComponentHub.DefaultPath, configureOptions);
}
/// <summary>
///Maps the Blazor <see cref="Hub" /> to the default path and associates
/// the component <paramref name="type"/> to this hub instance as the given DOM <paramref name="selector"/>.
/// Maps the Blazor <see cref="Hub" /> to the path <paramref name="path"/>.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/>.</param>
/// <param name="type">The first <see cref="IComponent"/> associated with this Blazor <see cref="Hub" />.</param>
/// <param name="selector">The selector for the component.</param>
/// <returns>The <see cref="ComponentEndpointConventionBuilder"/>.</returns>
public static ComponentEndpointConventionBuilder MapBlazorHub(
this IEndpointRouteBuilder endpoints,
Type type,
string selector)
{
if (endpoints == null)
{
throw new ArgumentNullException(nameof(endpoints));
}
if (type == null)
{
throw new ArgumentNullException(nameof(type));
}
if (selector == null)
{
throw new ArgumentNullException(nameof(selector));
}
return endpoints.MapBlazorHub(type, selector, ComponentHub.DefaultPath);
}
/// <summary>
///Maps the Blazor <see cref="Hub" /> to the default path and associates
/// the component <paramref name="type"/> to this hub instance as the given DOM <paramref name="selector"/>.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/>.</param>
/// <param name="type">The first <see cref="IComponent"/> associated with this Blazor <see cref="Hub" />.</param>
/// <param name="selector">The selector for the component.</param>
/// <param name="configureOptions">A callback to configure dispatcher options.</param>
/// <returns>The <see cref="ComponentEndpointConventionBuilder"/>.</returns>
public static ComponentEndpointConventionBuilder MapBlazorHub(
this IEndpointRouteBuilder endpoints,
Type type,
string selector,
Action<HttpConnectionDispatcherOptions> configureOptions)
{
if (endpoints == null)
{
throw new ArgumentNullException(nameof(endpoints));
}
if (type == null)
{
throw new ArgumentNullException(nameof(type));
}
if (selector == null)
{
throw new ArgumentNullException(nameof(selector));
}
if (configureOptions == null)
{
throw new ArgumentNullException(nameof(configureOptions));
}
return endpoints.MapBlazorHub(type, selector, ComponentHub.DefaultPath, configureOptions);
}
/// <summary>
/// Maps the Blazor <see cref="Hub" /> to the path <paramref name="path"/> and associates
/// the component <typeparamref name="TComponent"/> to this hub instance as the given DOM <paramref name="selector"/>.
/// </summary>
/// <typeparam name="TComponent">The first <see cref="IComponent"/> associated with this Blazor <see cref="Hub" />.</typeparam>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/>.</param>
/// <param name="selector">The selector for the <typeparamref name="TComponent"/>.</param>
/// <param name="path">The path to map the Blazor <see cref="Hub" />.</param>
/// <returns>The <see cref="ComponentEndpointConventionBuilder"/>.</returns>
public static ComponentEndpointConventionBuilder MapBlazorHub<TComponent>(
this IEndpointRouteBuilder endpoints,
string selector,
string path) where TComponent : IComponent
{
if (endpoints == null)
{
throw new ArgumentNullException(nameof(endpoints));
}
if (path == null)
{
throw new ArgumentNullException(nameof(path));
}
if (selector == null)
{
throw new ArgumentNullException(nameof(selector));
}
return endpoints.MapBlazorHub(typeof(TComponent), selector, path);
}
/// <summary>
/// Maps the Blazor <see cref="Hub" /> to the path <paramref name="path"/> and associates
/// the component <typeparamref name="TComponent"/> to this hub instance as the given DOM <paramref name="selector"/>.
/// </summary>
/// <typeparam name="TComponent">The first <see cref="IComponent"/> associated with this Blazor <see cref="Hub" />.</typeparam>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/>.</param>
/// <param name="selector">The selector for the <typeparamref name="TComponent"/>.</param>
/// <param name="path">The path to map the Blazor <see cref="Hub" />.</param>
/// <param name="configureOptions">A callback to configure dispatcher options.</param>
/// <returns>The <see cref="ComponentEndpointConventionBuilder"/>.</returns>
public static ComponentEndpointConventionBuilder MapBlazorHub<TComponent>(
this IEndpointRouteBuilder endpoints,
string selector,
string path,
Action<HttpConnectionDispatcherOptions> configureOptions) where TComponent : IComponent
{
if (endpoints == null)
{
throw new ArgumentNullException(nameof(endpoints));
}
if (path == null)
{
throw new ArgumentNullException(nameof(path));
}
if (selector == null)
{
throw new ArgumentNullException(nameof(selector));
}
if (configureOptions == null)
{
throw new ArgumentNullException(nameof(configureOptions));
}
return endpoints.MapBlazorHub(typeof(TComponent), selector, path, configureOptions);
}
/// <summary>
/// Maps the Blazor <see cref="Hub" /> to the path <paramref name="path"/> and associates
/// the component <paramref name="componentType"/> to this hub instance as the given DOM <paramref name="selector"/>.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/>.</param>
/// <param name="componentType">The first <see cref="IComponent"/> associated with this Blazor <see cref="Hub" />.</param>
/// <param name="selector">The selector for the <paramref name="componentType"/>.</param>
/// <param name="path">The path to map the Blazor <see cref="Hub" />.</param>
/// <returns>The <see cref="ComponentEndpointConventionBuilder"/>.</returns>
public static ComponentEndpointConventionBuilder MapBlazorHub(
this IEndpointRouteBuilder endpoints,
Type componentType,
string selector,
string path)
{
if (endpoints == null)
@ -237,33 +49,41 @@ namespace Microsoft.AspNetCore.Builder
throw new ArgumentNullException(nameof(path));
}
if (componentType == null)
{
throw new ArgumentNullException(nameof(componentType));
}
if (selector == null)
{
throw new ArgumentNullException(nameof(selector));
}
return endpoints.MapBlazorHub(componentType, selector, path, configureOptions: _ => { });
return endpoints.MapBlazorHub(path, configureOptions: _ => { });
}
/// <summary>
/// Maps the Blazor <see cref="Hub" /> to the path <paramref name="path"/> and associates
/// the component <paramref name="componentType"/> to this hub instance as the given DOM <paramref name="selector"/>.
///Maps the Blazor <see cref="Hub" /> to the default path.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/>.</param>
/// <param name="componentType">The first <see cref="IComponent"/> associated with this Blazor <see cref="Hub" />.</param>
/// <param name="selector">The selector for the <paramref name="componentType"/>.</param>
/// <param name="configureOptions">A callback to configure dispatcher options.</param>
/// <param name="path">The path to map the Blazor <see cref="Hub" />.</param>
/// <returns>The <see cref="ComponentEndpointConventionBuilder"/>.</returns>
public static ComponentEndpointConventionBuilder MapBlazorHub(
this IEndpointRouteBuilder endpoints,
Type componentType,
string selector,
Action<HttpConnectionDispatcherOptions> configureOptions)
{
if (endpoints == null)
{
throw new ArgumentNullException(nameof(endpoints));
}
if (configureOptions == null)
{
throw new ArgumentNullException(nameof(configureOptions));
}
return endpoints.MapBlazorHub(ComponentHub.DefaultPath, configureOptions);
}
/// <summary>
/// Maps the Blazor <see cref="Hub" /> to the path <paramref name="path"/>.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/>.</param>
/// <param name="path">The path to map the Blazor <see cref="Hub" />.</param>
/// <param name="configureOptions">A callback to configure dispatcher options.</param>
/// <returns>The <see cref="ComponentEndpointConventionBuilder"/>.</returns>
public static ComponentEndpointConventionBuilder MapBlazorHub(
this IEndpointRouteBuilder endpoints,
string path,
Action<HttpConnectionDispatcherOptions> configureOptions)
{
@ -277,16 +97,6 @@ namespace Microsoft.AspNetCore.Builder
throw new ArgumentNullException(nameof(path));
}
if (componentType == null)
{
throw new ArgumentNullException(nameof(componentType));
}
if (selector == null)
{
throw new ArgumentNullException(nameof(selector));
}
if (configureOptions == null)
{
throw new ArgumentNullException(nameof(configureOptions));
@ -299,10 +109,7 @@ namespace Microsoft.AspNetCore.Builder
endpoints.CreateApplicationBuilder().UseMiddleware<CircuitDisconnectMiddleware>().Build())
.WithDisplayName("Blazor disconnect");
return new ComponentEndpointConventionBuilder(
hubEndpoint,
disconnectEndpoint)
.AddComponent(componentType, selector);
return new ComponentEndpointConventionBuilder(hubEndpoint, disconnectEndpoint);
}
}
}

View File

@ -1,18 +1,101 @@
// 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 System.Linq;
using System.Security.Claims;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.JSInterop;
namespace Microsoft.AspNetCore.Components.Server.Circuits
{
internal abstract class CircuitFactory
internal class CircuitFactory
{
public abstract CircuitHost CreateCircuitHost(
HttpContext httpContext,
private readonly IServiceScopeFactory _scopeFactory;
private readonly ILoggerFactory _loggerFactory;
private readonly CircuitIdFactory _circuitIdFactory;
private readonly CircuitOptions _options;
private readonly ILogger _logger;
public CircuitFactory(
IServiceScopeFactory scopeFactory,
ILoggerFactory loggerFactory,
CircuitIdFactory circuitIdFactory,
IOptions<CircuitOptions> options)
{
_scopeFactory = scopeFactory;
_loggerFactory = loggerFactory;
_circuitIdFactory = circuitIdFactory;
_options = options.Value;
_logger = _loggerFactory.CreateLogger<CircuitFactory>();
}
public CircuitHost CreateCircuitHost(
IReadOnlyList<ComponentDescriptor> components,
CircuitClientProxy client,
string baseUri,
string uri,
ClaimsPrincipal user);
ClaimsPrincipal user)
{
var scope = _scopeFactory.CreateScope();
var jsRuntime = (RemoteJSRuntime)scope.ServiceProvider.GetRequiredService<IJSRuntime>();
jsRuntime.Initialize(client);
var navigationManager = (RemoteNavigationManager)scope.ServiceProvider.GetRequiredService<NavigationManager>();
var navigationInterception = (RemoteNavigationInterception)scope.ServiceProvider.GetRequiredService<INavigationInterception>();
if (client.Connected)
{
navigationManager.AttachJsRuntime(jsRuntime);
navigationManager.Initialize(baseUri, uri);
navigationInterception.AttachJSRuntime(jsRuntime);
}
else
{
navigationManager.Initialize(baseUri, uri);
}
var renderer = new RemoteRenderer(
scope.ServiceProvider,
_loggerFactory,
_options,
client,
_loggerFactory.CreateLogger<RemoteRenderer>());
var circuitHandlers = scope.ServiceProvider.GetServices<CircuitHandler>()
.OrderBy(h => h.Order)
.ToArray();
var circuitHost = new CircuitHost(
_circuitIdFactory.CreateCircuitId(),
scope,
_options,
client,
renderer,
components,
jsRuntime,
circuitHandlers,
_loggerFactory.CreateLogger<CircuitHost>());
Log.CreatedCircuit(_logger, circuitHost);
// Initialize per - circuit data that services need
(circuitHost.Services.GetRequiredService<ICircuitAccessor>() as DefaultCircuitAccessor).Circuit = circuitHost.Circuit;
circuitHost.SetCircuitUser(user);
return circuitHost;
}
private static class Log
{
private static readonly Action<ILogger, string, string, Exception> _createdCircuit =
LoggerMessage.Define<string, string>(LogLevel.Debug, new EventId(1, "CreatedCircuit"), "Created circuit {CircuitId} for connection {ConnectionId}");
internal static void CreatedCircuit(ILogger logger, CircuitHost circuitHost) =>
_createdCircuit(logger, circuitHost.CircuitId.Id, circuitHost.Client.ConnectionId, null);
}
}
}

View File

@ -111,8 +111,8 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
var count = Descriptors.Count;
for (var i = 0; i < count; i++)
{
var (componentType, domElementSelector) = Descriptors[i];
await Renderer.AddComponentAsync(componentType, domElementSelector);
var (componentType, sequence) = Descriptors[i];
await Renderer.AddComponentAsync(componentType, sequence.ToString());
}
Log.InitializationSucceeded(_logger);
@ -544,8 +544,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
}
else
{
return
$"There was an unhandled exception on the current circuit, so this circuit will be terminated. For more details turn on " +
return $"There was an unhandled exception on the current circuit, so this circuit will be terminated. For more details turn on " +
$"detailed exceptions in '{typeof(CircuitOptions).Name}.{nameof(CircuitOptions.DetailedErrors)}'. {additionalInformation}";
}
}

View File

@ -9,12 +9,12 @@ namespace Microsoft.AspNetCore.Components.Server
{
public Type ComponentType { get; set; }
public string Selector { get; set; }
public int Sequence { get; set; }
public void Deconstruct(out Type componentType, out string selector)
public void Deconstruct(out Type componentType, out int sequence)
{
componentType = ComponentType;
selector = Selector;
sequence = Sequence;
}
}
}

View File

@ -1,133 +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.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.JSInterop;
namespace Microsoft.AspNetCore.Components.Server.Circuits
{
internal class DefaultCircuitFactory : CircuitFactory
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly ILoggerFactory _loggerFactory;
private readonly CircuitIdFactory _circuitIdFactory;
private readonly CircuitOptions _options;
private readonly ILogger _logger;
public DefaultCircuitFactory(
IServiceScopeFactory scopeFactory,
ILoggerFactory loggerFactory,
CircuitIdFactory circuitIdFactory,
IOptions<CircuitOptions> options)
{
_scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory));
_loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
_circuitIdFactory = circuitIdFactory ?? throw new ArgumentNullException(nameof(circuitIdFactory));
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
_logger = _loggerFactory.CreateLogger<DefaultCircuitFactory>();
}
public override CircuitHost CreateCircuitHost(
HttpContext httpContext,
CircuitClientProxy client,
string baseUri,
string uri,
ClaimsPrincipal user)
{
// We do as much intialization as possible eagerly in this method, which makes the error handling
// story much simpler. If we throw from here, it's handled inside the initial hub method.
var components = ResolveComponentMetadata(httpContext);
var scope = _scopeFactory.CreateScope();
var jsRuntime = (RemoteJSRuntime)scope.ServiceProvider.GetRequiredService<IJSRuntime>();
jsRuntime.Initialize(client);
var navigationManager = (RemoteNavigationManager)scope.ServiceProvider.GetRequiredService<NavigationManager>();
var navigationInterception = (RemoteNavigationInterception)scope.ServiceProvider.GetRequiredService<INavigationInterception>();
if (client.Connected)
{
navigationManager.AttachJsRuntime(jsRuntime);
navigationManager.Initialize(baseUri, uri);
navigationInterception.AttachJSRuntime(jsRuntime);
}
else
{
navigationManager.Initialize(baseUri, uri);
}
var renderer = new RemoteRenderer(
scope.ServiceProvider,
_loggerFactory,
_options,
jsRuntime,
client,
_loggerFactory.CreateLogger<RemoteRenderer>());
var circuitHandlers = scope.ServiceProvider.GetServices<CircuitHandler>()
.OrderBy(h => h.Order)
.ToArray();
var circuitHost = new CircuitHost(
_circuitIdFactory.CreateCircuitId(),
scope,
_options,
client,
renderer,
components,
jsRuntime,
circuitHandlers,
_loggerFactory.CreateLogger<CircuitHost>());
Log.CreatedCircuit(_logger, circuitHost);
// Initialize per - circuit data that services need
(circuitHost.Services.GetRequiredService<ICircuitAccessor>() as DefaultCircuitAccessor).Circuit = circuitHost.Circuit;
circuitHost.SetCircuitUser(user);
return circuitHost;
}
public static IReadOnlyList<ComponentDescriptor> ResolveComponentMetadata(HttpContext httpContext)
{
var endpoint = httpContext.GetEndpoint();
if (endpoint == null)
{
throw new InvalidOperationException(
$"{nameof(ComponentHub)} doesn't have an associated endpoint. " +
"Use 'app.UseEndpoints(endpoints => endpoints.MapBlazorHub<App>(\"app\"))' to register your hub.");
}
return endpoint.Metadata.GetOrderedMetadata<ComponentDescriptor>();
}
private static class Log
{
private static readonly Action<ILogger, CircuitId, string, Exception> _createdConnectedCircuit =
LoggerMessage.Define<CircuitId, string>(LogLevel.Debug, new EventId(1, "CreatedConnectedCircuit"), "Created circuit {CircuitId} for connection {ConnectionId}");
private static readonly Action<ILogger, CircuitId, Exception> _createdDisconnectedCircuit =
LoggerMessage.Define<CircuitId>(LogLevel.Debug, new EventId(2, "CreatedDisconnectedCircuit"), "Created circuit {CircuitId} for disconnected client");
internal static void CreatedCircuit(ILogger logger, CircuitHost circuitHost)
{
if (circuitHost.Client.Connected)
{
_createdConnectedCircuit(logger, circuitHost.CircuitId, circuitHost.Client.ConnectionId, null);
}
else
{
_createdDisconnectedCircuit(logger, circuitHost.CircuitId, null);
}
}
}
}
}

View File

@ -17,7 +17,6 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
{
private static readonly Task CanceledTask = Task.FromCanceled(new CancellationToken(canceled: true));
private readonly IJSRuntime _jsRuntime;
private readonly CircuitClientProxy _client;
private readonly CircuitOptions _options;
private readonly ILogger _logger;
@ -37,12 +36,10 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
IServiceProvider serviceProvider,
ILoggerFactory loggerFactory,
CircuitOptions options,
IJSRuntime jsRuntime,
CircuitClientProxy client,
ILogger logger)
: base(serviceProvider, loggerFactory)
{
_jsRuntime = jsRuntime;
_client = client;
_options = options;
_logger = logger;
@ -61,11 +58,8 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
var component = InstantiateComponent(componentType);
var componentId = AssignRootComponentId(component);
var attachComponentTask = _jsRuntime.InvokeVoidAsync(
"Blazor._internal.attachRootComponentToElement",
domElementSelector,
componentId);
CaptureAsyncExceptions(attachComponentTask.AsTask());
var attachComponentTask = _client.SendAsync("JS.AttachComponent", componentId, domElementSelector);
CaptureAsyncExceptions(attachComponentTask);
return RenderRootComponentAsync(componentId);
}

View File

@ -0,0 +1,263 @@
// 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 System.Text.Json;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Components.Server
{
// **Component descriptor protocol**
// MVC serializes one or more components as comments in HTML.
// Each comment is in the form <!-- Blazor:<<Json>>--> for example { "type": "server", "sequence": 0, descriptor: "base64(dataprotected(<<ServerComponent>>))" }
// Where <<Json>> has the following properties:
// 'type' indicates the marker type. For now it's limited to server.
// 'sequence' indicates the order in which this component got rendered on the server.
// 'descriptor' a data-protected payload that allows the server to validate the legitimacy of the rendered component.
// 'prerenderId' a unique identifier that uniquely identifies the marker to match start and end markers.
//
// descriptor holds the information to validate a component render request. It prevents an infinite number of components
// from being rendered by a given client.
//
// descriptor is a data protected json payload that holds the following information
// 'sequence' indicates the order in which this component got rendered on the server.
// 'assemblyName' the assembly name for the rendered component.
// 'type' the full type name for the rendered component.
// 'invocationId' a random string that matches all components rendered by as part of a single HTTP response.
// For example: base64(dataprotection({ "sequence": 1, "assemblyName": "Microsoft.AspNetCore.Components", "type":"Microsoft.AspNetCore.Components.Routing.Router", "invocationId": "<<guid>>"}))
// Serialization:
// For a given response, MVC renders one or more markers in sequence, including a descriptor for each rendered
// component containing the information described above.
// Deserialization:
// To prevent a client from rendering an infinite amount of components, we require clients to send all component
// markers in order. They can do so thanks to the sequence included in the marker.
// When we process a marker we do the following.
// * We unprotect the data-protected information.
// * We validate that the sequence number for the descriptor goes after the previous descriptor.
// * We compare the invocationId for the previous descriptor against the invocationId for the current descriptor to make sure they match.
// By doing this we achieve three things:
// * We ensure that the descriptor came from the server.
// * We ensure that a client can't just send an infinite amount of components to render.
// * We ensure that we do the minimal amount of work in the case of an invalid sequence of descriptors.
//
// For example:
// A client can't just send 100 component markers and force us to process them if the server didn't generate those 100 markers.
// * If a marker is out of sequence we will fail early, so we process at most n-1 markers.
// * If a marker has the right sequence but the invocation ID is different we will fail at that point. We know for sure that the
// component wasn't render as part of the same response.
// * If a marker can't be unprotected we will fail early. We know that the marker was tampered with and can't be trusted.
internal class ServerComponentDeserializer
{
private readonly IDataProtector _dataProtector;
private readonly ILogger<ServerComponentDeserializer> _logger;
private readonly ServerComponentTypeCache _rootComponentTypeCache;
public ServerComponentDeserializer(
IDataProtectionProvider dataProtectionProvider,
ILogger<ServerComponentDeserializer> logger,
ServerComponentTypeCache rootComponentTypeCache)
{
// When we protect the data we use a time-limited data protector with the
// limits established in 'ServerComponentSerializationSettings.DataExpiration'
// We don't use any of the additional methods provided by ITimeLimitedDataProtector
// in this class, but we need to create one for the unprotect operations to work
// even though we simply call '_dataProtector.Unprotect'.
// See the comment in ServerComponentSerializationSettings.DataExpiration to understand
// why we limit the validity of the protected payloads.
_dataProtector = dataProtectionProvider
.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
.ToTimeLimitedDataProtector();
_logger = logger;
_rootComponentTypeCache = rootComponentTypeCache;
}
public bool TryDeserializeComponentDescriptorCollection(string serializedComponentRecords, out List<ComponentDescriptor> descriptors)
{
var markers = JsonSerializer.Deserialize<IEnumerable<ServerComponentMarker>>(serializedComponentRecords, ServerComponentSerializationSettings.JsonSerializationOptions);
descriptors = new List<ComponentDescriptor>();
int lastSequence = -1;
var previousInstance = new ServerComponent();
foreach (var marker in markers)
{
if (marker.Type != ServerComponentMarker.ServerMarkerType)
{
Log.InvalidMarkerType(_logger, marker.Type);
descriptors.Clear();
return false;
}
if (marker.Descriptor == null)
{
Log.MissingMarkerDescriptor(_logger);
descriptors.Clear();
return false;
}
var (descriptor, serverComponent) = DeserializeServerComponent(marker);
if (descriptor == null)
{
// We failed to deserialize the component descriptor for some reason.
descriptors.Clear();
return false;
}
// We force our client to send the descriptors in order so that we do minimal work.
// The list of descriptors starts with 0 and lastSequence is initialized to -1 so this
// check covers that the sequence starts by 0.
if (lastSequence != serverComponent.Sequence - 1)
{
if (lastSequence == -1)
{
Log.DescriptorSequenceMustStartAtZero(_logger, serverComponent.Sequence);
}
else
{
Log.OutOfSequenceDescriptor(_logger, lastSequence, serverComponent.Sequence);
}
descriptors.Clear();
return false;
}
if (lastSequence != -1 && !previousInstance.InvocationId.Equals(serverComponent.InvocationId))
{
Log.MismatchedInvocationId(_logger, previousInstance.InvocationId.ToString("N"), serverComponent.InvocationId.ToString("N"));
descriptors.Clear();
return false;
}
// As described below, we build a chain of descriptors to prevent being flooded by
// descriptors from a client not behaving properly.
lastSequence = serverComponent.Sequence;
previousInstance = serverComponent;
descriptors.Add(descriptor);
}
return true;
}
private (ComponentDescriptor, ServerComponent) DeserializeServerComponent(ServerComponentMarker record)
{
string unprotected;
try
{
unprotected = _dataProtector.Unprotect(record.Descriptor);
}
catch (Exception e)
{
Log.FailedToUnprotectDescriptor(_logger, e);
return default;
}
ServerComponent serverComponent;
try
{
serverComponent = JsonSerializer.Deserialize<ServerComponent>(
unprotected,
ServerComponentSerializationSettings.JsonSerializationOptions);
}
catch (Exception e)
{
Log.FailedToDeserializeDescriptor(_logger, e);
return default;
}
var componentType = _rootComponentTypeCache
.GetRootComponent(serverComponent.AssemblyName, serverComponent.TypeName);
if (componentType == null)
{
Log.FailedToFindComponent(_logger, serverComponent.TypeName, serverComponent.AssemblyName);
return default;
}
var componentDescriptor = new ComponentDescriptor
{
ComponentType = componentType,
Sequence = serverComponent.Sequence
};
return (componentDescriptor, serverComponent);
}
private static class Log
{
private static readonly Action<ILogger, Exception> _failedToDeserializeDescriptor =
LoggerMessage.Define(
LogLevel.Debug,
new EventId(1, "FailedToDeserializeDescriptor"),
"Failed to deserialize the component descriptor.");
private static readonly Action<ILogger, string, string, Exception> _failedToFindComponent =
LoggerMessage.Define<string, string>(
LogLevel.Debug,
new EventId(2, "FailedToFindComponent"),
"Failed to find component '{ComponentName}' in assembly '{Assembly}'.");
private static readonly Action<ILogger, Exception> _failedToUnprotectDescriptor =
LoggerMessage.Define(
LogLevel.Debug,
new EventId(3, "FailedToUnprotectDescriptor"),
"Failed to unprotect the component descriptor.");
private static readonly Action<ILogger, string, Exception> _invalidMarkerType =
LoggerMessage.Define<string>(
LogLevel.Debug,
new EventId(4, "InvalidMarkerType"),
"Invalid component marker type '{}'.");
private static readonly Action<ILogger, Exception> _missingMarkerDescriptor =
LoggerMessage.Define(
LogLevel.Debug,
new EventId(5, "MissingMarkerDescriptor"),
"The component marker is missing the descriptor.");
private static readonly Action<ILogger, string, string, Exception> _mismatchedInvocationId =
LoggerMessage.Define<string, string>(
LogLevel.Debug,
new EventId(6, "MismatchedInvocationId"),
"The descriptor invocationId is '{invocationId}' and got a descriptor with invocationId '{currentInvocationId}'.");
private static readonly Action<ILogger, int, int, Exception> _outOfSequenceDescriptor =
LoggerMessage.Define<int, int>(
LogLevel.Debug,
new EventId(7, "OutOfSequenceDescriptor"),
"The last descriptor sequence was '{lastSequence}' and got a descriptor with sequence '{receivedSequence}'.");
private static readonly Action<ILogger, int, Exception> _descriptorSequenceMustStartAtZero =
LoggerMessage.Define<int>(
LogLevel.Debug,
new EventId(8, "DescriptorSequenceMustStartAtZero"),
"The descriptor sequence '{sequence}' is an invalid start sequence.");
public static void FailedToDeserializeDescriptor(ILogger<ServerComponentDeserializer> logger, Exception e) =>
_failedToDeserializeDescriptor(logger, e);
public static void FailedToFindComponent(ILogger<ServerComponentDeserializer> logger, string assemblyName, string typeName) =>
_failedToFindComponent(logger, assemblyName, typeName, null);
public static void FailedToUnprotectDescriptor(ILogger<ServerComponentDeserializer> logger, Exception e) =>
_failedToUnprotectDescriptor(logger, e);
public static void InvalidMarkerType(ILogger<ServerComponentDeserializer> logger, string markerType) =>
_invalidMarkerType(logger, markerType, null);
public static void MissingMarkerDescriptor(ILogger<ServerComponentDeserializer> logger) =>
_missingMarkerDescriptor(logger, null);
public static void MismatchedInvocationId(ILogger<ServerComponentDeserializer> logger, string invocationId, string currentInvocationId) =>
_mismatchedInvocationId(logger, invocationId, currentInvocationId, null);
public static void OutOfSequenceDescriptor(ILogger<ServerComponentDeserializer> logger, int lastSequence, int sequence) =>
_outOfSequenceDescriptor(logger, lastSequence, sequence, null);
public static void DescriptorSequenceMustStartAtZero(ILogger<ServerComponentDeserializer> logger, int sequence) =>
_descriptorSequenceMustStartAtZero(logger, sequence, null);
}
}
}

View File

@ -0,0 +1,59 @@
// 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.Concurrent;
using System.Linq;
using System.Reflection;
namespace Microsoft.AspNetCore.Components
{
// A cache for root component types
internal class ServerComponentTypeCache
{
private readonly ConcurrentDictionary<Key, Type> _typeToKeyLookUp = new ConcurrentDictionary<Key, Type>();
public Type GetRootComponent(string assembly, string type)
{
var key = new Key(assembly, type);
if (_typeToKeyLookUp.TryGetValue(key, out var resolvedType))
{
return resolvedType;
}
else
{
return _typeToKeyLookUp.GetOrAdd(key, ResolveType, AppDomain.CurrentDomain.GetAssemblies());
}
}
private static Type ResolveType(Key key, Assembly[] assemblies)
{
var assembly = assemblies
.FirstOrDefault(a => string.Equals(a.GetName().Name, key.Assembly, StringComparison.Ordinal));
if (assembly == null)
{
return null;
}
return assembly.GetType(key.Type, throwOnError: false, ignoreCase: false);
}
private struct Key : IEquatable<Key>
{
public Key(string assembly, string type) =>
(Assembly, Type) = (assembly, type);
public string Assembly { get; set; }
public string Type { get; set; }
public override bool Equals(object obj) => Equals((Key)obj);
public bool Equals(Key other) => string.Equals(Assembly, other.Assembly, StringComparison.Ordinal) &&
string.Equals(Type, other.Type, StringComparison.Ordinal);
public override int GetHashCode() => HashCode.Combine(Assembly, Type);
}
}
}

View File

@ -8,7 +8,6 @@ using Microsoft.AspNetCore.Components.Server.Circuits;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Components.Server
{
@ -37,23 +36,23 @@ namespace Microsoft.AspNetCore.Components.Server
internal sealed class ComponentHub : Hub
{
private static readonly object CircuitKey = new object();
private readonly ServerComponentDeserializer _serverComponentSerializer;
private readonly CircuitFactory _circuitFactory;
private readonly CircuitIdFactory _circuitIdFactory;
private readonly CircuitRegistry _circuitRegistry;
private readonly CircuitOptions _options;
private readonly ILogger _logger;
public ComponentHub(
ServerComponentDeserializer serializer,
CircuitFactory circuitFactory,
CircuitIdFactory circuitIdFactory,
CircuitRegistry circuitRegistry,
ILogger<ComponentHub> logger,
IOptions<CircuitOptions> options)
ILogger<ComponentHub> logger)
{
_serverComponentSerializer = serializer;
_circuitFactory = circuitFactory;
_circuitIdFactory = circuitIdFactory;
_circuitRegistry = circuitRegistry;
_options = options.Value;
_logger = logger;
}
@ -75,7 +74,7 @@ namespace Microsoft.AspNetCore.Components.Server
return _circuitRegistry.DisconnectAsync(circuitHost, Context.ConnectionId);
}
public async ValueTask<string> StartCircuit(string baseUri, string uri)
public async ValueTask<string> StartCircuit(string baseUri, string uri, string serializedComponentRecords)
{
var circuitHost = GetCircuit();
if (circuitHost != null)
@ -104,11 +103,11 @@ namespace Microsoft.AspNetCore.Components.Server
return null;
}
// From this point, we can try to actually initialize the circuit.
if (DefaultCircuitFactory.ResolveComponentMetadata(Context.GetHttpContext()).Count == 0)
if (!_serverComponentSerializer.TryDeserializeComponentDescriptorCollection(serializedComponentRecords, out var components))
{
// No components preregistered so return. This is totally normal if the components were prerendered.
Log.NoComponentsRegisteredInEndpoint(_logger, Context.GetHttpContext().GetEndpoint()?.DisplayName);
Log.InvalidInputData(_logger);
await NotifyClientError(Clients.Caller, $"The list of component records is not valid.");
Context.Abort();
return null;
}
@ -116,7 +115,7 @@ namespace Microsoft.AspNetCore.Components.Server
{
var circuitClient = new CircuitClientProxy(Clients.Caller, Context.ConnectionId);
circuitHost = _circuitFactory.CreateCircuitHost(
Context.GetHttpContext(),
components,
circuitClient,
baseUri,
uri,
@ -187,7 +186,7 @@ namespace Microsoft.AspNetCore.Components.Server
return;
}
_ = circuitHost.BeginInvokeDotNetFromJS(callId, assemblyName, methodIdentifier, dotNetObjectId, argsJson);
_ = circuitHost.BeginInvokeDotNetFromJS(callId, assemblyName, methodIdentifier, dotNetObjectId, argsJson);
}
public async ValueTask EndInvokeJSFromDotNet(long asyncHandle, bool succeeded, string arguments)
@ -198,7 +197,7 @@ namespace Microsoft.AspNetCore.Components.Server
return;
}
_ = circuitHost.EndInvokeJSFromDotNet(asyncHandle, succeeded, arguments);
_ = circuitHost.EndInvokeJSFromDotNet(asyncHandle, succeeded, arguments);
}
public async ValueTask DispatchBrowserEvent(string eventDescriptor, string eventArgs)
@ -209,7 +208,7 @@ namespace Microsoft.AspNetCore.Components.Server
return;
}
_ = circuitHost.DispatchEvent(eventDescriptor, eventArgs);
_ = circuitHost.DispatchEvent(eventDescriptor, eventArgs);
}
public async ValueTask OnRenderCompleted(long renderId, string errorMessageOrNull)
@ -232,7 +231,7 @@ namespace Microsoft.AspNetCore.Components.Server
return;
}
_ = circuitHost.OnLocationChangedAsync(uri, intercepted);
_ = circuitHost.OnLocationChangedAsync(uri, intercepted);
}
// We store the CircuitHost through a *handle* here because Context.Items is tied to the lifetime
@ -281,37 +280,29 @@ namespace Microsoft.AspNetCore.Components.Server
private static class Log
{
private static readonly Action<ILogger, string, Exception> _noComponentsRegisteredInEndpoint =
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(1, "NoComponentsRegisteredInEndpoint"), "No components registered in the current endpoint '{Endpoint}'");
private static readonly Action<ILogger, long, Exception> _receivedConfirmationForBatch =
LoggerMessage.Define<long>(LogLevel.Debug, new EventId(2, "ReceivedConfirmationForBatch"), "Received confirmation for batch {BatchId}");
private static readonly Action<ILogger, string, Exception> _unhandledExceptionInCircuit =
LoggerMessage.Define<string>(LogLevel.Warning, new EventId(3, "UnhandledExceptionInCircuit"), "Unhandled exception in circuit {CircuitId}");
LoggerMessage.Define<long>(LogLevel.Debug, new EventId(1, "ReceivedConfirmationForBatch"), "Received confirmation for batch {BatchId}");
private static readonly Action<ILogger, CircuitId, Exception> _circuitAlreadyInitialized =
LoggerMessage.Define<CircuitId>(LogLevel.Debug, new EventId(4, "CircuitAlreadyInitialized"), "The circuit host '{CircuitId}' has already been initialized");
LoggerMessage.Define<CircuitId>(LogLevel.Debug, new EventId(2, "CircuitAlreadyInitialized"), "The circuit host '{CircuitId}' has already been initialized");
private static readonly Action<ILogger, string, Exception> _circuitHostNotInitialized =
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(5, "CircuitHostNotInitialized"), "Call to '{CallSite}' received before the circuit host initialization");
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(3, "CircuitHostNotInitialized"), "Call to '{CallSite}' received before the circuit host initialization");
private static readonly Action<ILogger, string, Exception> _circuitHostShutdown =
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(6, "CircuitHostShutdown"), "Call to '{CallSite}' received after the circuit was shut down");
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(4, "CircuitHostShutdown"), "Call to '{CallSite}' received after the circuit was shut down");
private static readonly Action<ILogger, string, Exception> _invalidInputData =
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(7, "InvalidInputData"), "Call to '{CallSite}' received invalid input data");
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(5, "InvalidInputData"), "Call to '{CallSite}' received invalid input data");
private static readonly Action<ILogger, Exception> _circuitInitializationFailed =
LoggerMessage.Define(LogLevel.Debug, new EventId(8, "CircuitInitializationFailed"), "Circuit initialization failed");
LoggerMessage.Define(LogLevel.Debug, new EventId(6, "CircuitInitializationFailed"), "Circuit initialization failed");
private static readonly Action<ILogger, CircuitId, string, string, Exception> _createdCircuit =
LoggerMessage.Define<CircuitId, string, string>(LogLevel.Debug, new EventId(8, "CreatedCircuit"), "Created circuit '{CircuitId}' with secret '{CircuitIdSecret}' for '{ConnectionId}'");
LoggerMessage.Define<CircuitId, string, string>(LogLevel.Debug, new EventId(7, "CreatedCircuit"), "Created circuit '{CircuitId}' with secret '{CircuitIdSecret}' for '{ConnectionId}'");
private static readonly Action<ILogger, string, Exception> _invalidCircuitId =
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(9, "InvalidCircuitId"), "ConnectAsync recieved an invalid circuit id '{CircuitIdSecret}'");
public static void NoComponentsRegisteredInEndpoint(ILogger logger, string endpointDisplayName) => _noComponentsRegisteredInEndpoint(logger, endpointDisplayName, null);
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(8, "InvalidCircuitId"), "ConnectAsync recieved an invalid circuit id '{CircuitIdSecret}'");
public static void ReceivedConfirmationForBatch(ILogger logger, long batchId) => _receivedConfirmationForBatch(logger, batchId, null);

View File

@ -55,8 +55,9 @@ namespace Microsoft.Extensions.DependencyInjection
// user's configuration. So even if the user has multiple independent server-side
// Components entrypoints, this lot is the same and repeated registrations are a no-op.
services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<StaticFileOptions>, ConfigureStaticFilesOptions>());
services.TryAddSingleton<CircuitFactory, DefaultCircuitFactory>();
services.TryAddSingleton<CircuitFactory>();
services.TryAddSingleton<ServerComponentDeserializer>();
services.TryAddSingleton<ServerComponentTypeCache>();
services.TryAddSingleton<CircuitIdFactory>();
services.TryAddScoped(s => s.GetRequiredService<ICircuitAccessor>().Circuit);

View File

@ -14,7 +14,7 @@
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Components.Authorization" />
<Reference Include="Microsoft.AspNetCore.Components.Web" />
<Reference Include="Microsoft.AspNetCore.DataProtection" />
<Reference Include="Microsoft.AspNetCore.DataProtection.Extensions" />
<Reference Include="Microsoft.AspNetCore.SignalR" />
<Reference Include="Microsoft.AspNetCore.StaticFiles" />
<Reference Include="Microsoft.Extensions.Caching.Memory" />
@ -66,6 +66,11 @@
<Compile Include="$(MessagePackRoot)SequencePool.cs" LinkBase="BlazorPack\MessagePack" />
<Compile Include="$(MessagePackRoot)SequenceReader.cs" LinkBase="BlazorPack\MessagePack" />
<Compile Include="$(MessagePackRoot)SequenceReaderExtensions.cs" LinkBase="BlazorPack\MessagePack" />
<!-- Shared descriptor infrastructure with MVC -->
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponent.cs" />
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponentSerializationSettings.cs" />
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponentMarker.cs" />
</ItemGroup>
<PropertyGroup>

View File

@ -252,14 +252,13 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
{
return new TestRemoteRenderer(
Mock.Of<IServiceProvider>(),
Mock.Of<IJSRuntime>(),
Mock.Of<IClientProxy>());
}
private class TestRemoteRenderer : RemoteRenderer
{
public TestRemoteRenderer(IServiceProvider serviceProvider, IJSRuntime jsRuntime, IClientProxy client)
: base(serviceProvider, NullLoggerFactory.Instance, new CircuitOptions(), jsRuntime, new CircuitClientProxy(client, "connection"), NullLogger.Instance)
public TestRemoteRenderer(IServiceProvider serviceProvider, IClientProxy client)
: base(serviceProvider, NullLoggerFactory.Instance, new CircuitOptions(), new CircuitClientProxy(client, "connection"), NullLogger.Instance)
{
}

View File

@ -426,23 +426,18 @@ namespace Microsoft.AspNetCore.Components.Web.Rendering
private TestRemoteRenderer GetRemoteRenderer(IServiceProvider serviceProvider, CircuitClientProxy circuitClient = null)
{
var jsRuntime = new Mock<IJSRuntime>();
jsRuntime.Setup(r => r.InvokeAsync<object>("Blazor._internal.attachRootComponentToElement", It.IsAny<object[]>()))
.ReturnsAsync(new ValueTask<object>((object)null));
return new TestRemoteRenderer(
serviceProvider,
NullLoggerFactory.Instance,
new CircuitOptions(),
jsRuntime.Object,
circuitClient ?? new CircuitClientProxy(),
NullLogger.Instance);
}
private class TestRemoteRenderer : RemoteRenderer
{
public TestRemoteRenderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory, CircuitOptions options, IJSRuntime jsRuntime, CircuitClientProxy client, ILogger logger)
: base(serviceProvider, loggerFactory, options, jsRuntime, client, logger)
public TestRemoteRenderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory, CircuitOptions options, CircuitClientProxy client, ILogger logger)
: base(serviceProvider, loggerFactory, options, client, logger)
{
}

View File

@ -0,0 +1,261 @@
// 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.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
namespace Microsoft.AspNetCore.Components.Server.Circuits
{
public class ServerComponentDeserializerTest
{
private readonly IDataProtectionProvider _ephemeralDataProtectionProvider;
private readonly ITimeLimitedDataProtector _protector;
private readonly ServerComponentInvocationSequence _invocationSequence = new ServerComponentInvocationSequence();
public ServerComponentDeserializerTest()
{
_ephemeralDataProtectionProvider = new EphemeralDataProtectionProvider();
_protector = _ephemeralDataProtectionProvider
.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
.ToTimeLimitedDataProtector();
}
[Fact]
public void CanParseSingleMarker()
{
// Arrange
var markers = SerializeMarkers(CreateMarkers(typeof(TestComponent)));
var serverComponentDeserializer = CreateServerComponentDeserializer();
// Act & assert
Assert.True(serverComponentDeserializer.TryDeserializeComponentDescriptorCollection(markers, out var descriptors));
var deserializedDescriptor = Assert.Single(descriptors);
Assert.Equal(typeof(TestComponent).FullName, deserializedDescriptor.ComponentType.FullName);
Assert.Equal(0, deserializedDescriptor.Sequence);
}
[Fact]
public void CanParseMultipleMarkers()
{
// Arrange
var markers = SerializeMarkers(CreateMarkers(typeof(TestComponent), typeof(TestComponent)));
var serverComponentDeserializer = CreateServerComponentDeserializer();
// Act & assert
Assert.True(serverComponentDeserializer.TryDeserializeComponentDescriptorCollection(markers, out var descriptors));
Assert.Equal(2, descriptors.Count);
var firstDescriptor = descriptors[0];
Assert.Equal(typeof(TestComponent).FullName, firstDescriptor.ComponentType.FullName);
Assert.Equal(0, firstDescriptor.Sequence);
var secondDescriptor = descriptors[1];
Assert.Equal(typeof(TestComponent).FullName, secondDescriptor.ComponentType.FullName);
Assert.Equal(1, secondDescriptor.Sequence);
}
[Fact]
public void DoesNotParseOutOfOrderMarkers()
{
// Arrange
var markers = SerializeMarkers(CreateMarkers(typeof(TestComponent), typeof(TestComponent)).Reverse().ToArray());
var serverComponentDeserializer = CreateServerComponentDeserializer();
// Act & assert
Assert.False(serverComponentDeserializer.TryDeserializeComponentDescriptorCollection(markers, out var descriptors));
Assert.Empty(descriptors);
}
[Fact]
public void DoesNotParseMarkersFromDifferentInvocationSequences()
{
// Arrange
var firstChain = CreateMarkers(typeof(TestComponent));
var secondChain = CreateMarkers(new ServerComponentInvocationSequence(), typeof(TestComponent), typeof(TestComponent)).Skip(1);
var markers = SerializeMarkers(firstChain.Concat(secondChain).ToArray());
var serverComponentDeserializer = CreateServerComponentDeserializer();
// Act & assert
Assert.False(serverComponentDeserializer.TryDeserializeComponentDescriptorCollection(markers, out var descriptors));
Assert.Empty(descriptors);
}
[Fact]
public void DoesNotParseMarkersWhoseSequenceDoesNotStartAtZero()
{
// Arrange
var markers = SerializeMarkers(CreateMarkers(typeof(TestComponent), typeof(TestComponent)).Skip(1).ToArray());
var serverComponentDeserializer = CreateServerComponentDeserializer();
// Act & assert
Assert.False(serverComponentDeserializer.TryDeserializeComponentDescriptorCollection(markers, out var descriptors));
Assert.Empty(descriptors);
}
[Fact]
public void DoesNotParseMarkersWithGapsInTheSequence()
{
// Arrange
var brokenChain = CreateMarkers(typeof(TestComponent), typeof(TestComponent), typeof(TestComponent))
.Where(m => m.Sequence != 1)
.ToArray();
var markers = SerializeMarkers(brokenChain);
var serverComponentDeserializer = CreateServerComponentDeserializer();
// Act & assert
Assert.False(serverComponentDeserializer.TryDeserializeComponentDescriptorCollection(markers, out var descriptors));
Assert.Empty(descriptors);
}
[Fact]
public void DoesNotParseMarkersWithMissingDescriptor()
{
// Arrange
var missingDescriptorMarker = CreateMarkers(typeof(TestComponent));
missingDescriptorMarker[0].Descriptor = null;
var markers = SerializeMarkers(missingDescriptorMarker);
var serverComponentDeserializer = CreateServerComponentDeserializer();
// Act & assert
Assert.False(serverComponentDeserializer.TryDeserializeComponentDescriptorCollection(markers, out var descriptors));
Assert.Empty(descriptors);
}
[Fact]
public void DoesNotParseMarkersWithMissingType()
{
// Arrange
var missingTypeMarker = CreateMarkers(typeof(TestComponent));
missingTypeMarker[0].Type = null;
var markers = SerializeMarkers(missingTypeMarker);
var serverComponentDeserializer = CreateServerComponentDeserializer();
// Act & assert
Assert.False(serverComponentDeserializer.TryDeserializeComponentDescriptorCollection(markers, out var descriptors));
Assert.Empty(descriptors);
}
// Ensures we don't use untrusted data for validation.
[Fact]
public void AllowsMarkersWithMissingSequence()
{
// Arrange
var missingSequenceMarker = CreateMarkers(typeof(TestComponent), typeof(TestComponent));
missingSequenceMarker[0].Sequence = null;
missingSequenceMarker[1].Sequence = null;
var markers = SerializeMarkers(missingSequenceMarker);
var serverComponentDeserializer = CreateServerComponentDeserializer();
// Act & assert
Assert.True(serverComponentDeserializer.TryDeserializeComponentDescriptorCollection(markers, out var descriptors));
Assert.Equal(2, descriptors.Count);
}
// Ensures that we don't try to load assemblies
[Fact]
public void DoesNotParseMarkersWithUnknownComponentTypeAssembly()
{
// Arrange
var missingUnknownComponentTypeMarker = CreateMarkers(typeof(TestComponent));
missingUnknownComponentTypeMarker[0].Descriptor = _protector.Protect(
SerializeComponent("UnknownAssembly", "System.String"),
TimeSpan.FromSeconds(30));
var markers = SerializeMarkers(missingUnknownComponentTypeMarker);
var serverComponentDeserializer = CreateServerComponentDeserializer();
// Act & assert
Assert.False(serverComponentDeserializer.TryDeserializeComponentDescriptorCollection(markers, out var descriptors));
Assert.Empty(descriptors);
}
[Fact]
public void DoesNotParseMarkersWithUnknownComponentTypeName()
{
// Arrange
var missingUnknownComponentTypeMarker = CreateMarkers(typeof(TestComponent));
missingUnknownComponentTypeMarker[0].Descriptor = _protector.Protect(
SerializeComponent(typeof(TestComponent).Assembly.GetName().Name, "Unknown.Type"),
TimeSpan.FromSeconds(30));
var markers = SerializeMarkers(missingUnknownComponentTypeMarker);
var serverComponentDeserializer = CreateServerComponentDeserializer();
// Act & assert
Assert.False(serverComponentDeserializer.TryDeserializeComponentDescriptorCollection(markers, out var descriptors));
Assert.Empty(descriptors);
}
[Fact]
public void DoesNotParseMarkersWithInvalidDescriptorPayloads()
{
// Arrange
var invalidDescriptorMarker = CreateMarkers(typeof(TestComponent));
invalidDescriptorMarker[0].Descriptor = "nondataprotecteddata";
var markers = SerializeMarkers(invalidDescriptorMarker);
var serverComponentDeserializer = CreateServerComponentDeserializer();
// Act & assert
Assert.False(serverComponentDeserializer.TryDeserializeComponentDescriptorCollection(markers, out var descriptors));
Assert.Empty(descriptors);
}
private string SerializeComponent(string assembly, string type) =>
JsonSerializer.Serialize(
new ServerComponent(0, assembly, type, Guid.NewGuid()),
ServerComponentSerializationSettings.JsonSerializationOptions);
private ServerComponentDeserializer CreateServerComponentDeserializer()
{
return new ServerComponentDeserializer(
_ephemeralDataProtectionProvider,
NullLogger<ServerComponentDeserializer>.Instance,
new ServerComponentTypeCache());
}
private string SerializeMarkers(ServerComponentMarker[] markers) =>
JsonSerializer.Serialize(markers, ServerComponentSerializationSettings.JsonSerializationOptions);
private ServerComponentMarker[] CreateMarkers(params Type[] types)
{
var serializer = new ServerComponentSerializer(_ephemeralDataProtectionProvider);
var markers = new ServerComponentMarker[types.Length];
for (var i = 0; i < types.Length; i++)
{
markers[i] = serializer.SerializeInvocation(_invocationSequence, types[i], false);
}
return markers;
}
private ServerComponentMarker[] CreateMarkers(ServerComponentInvocationSequence sequence, params Type[] types)
{
var serializer = new ServerComponentSerializer(_ephemeralDataProtectionProvider);
var markers = new ServerComponentMarker[types.Length];
for (var i = 0; i < types.Length; i++)
{
markers[i] = serializer.SerializeInvocation(sequence, types[i], false);
}
return markers;
}
private class TestComponent : IComponent
{
public void Attach(RenderHandle renderHandle) => throw new NotImplementedException();
public Task SetParametersAsync(ParameterView parameters) => throw new NotImplementedException();
}
}
}

View File

@ -37,7 +37,6 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
serviceScope.ServiceProvider ?? Mock.Of<IServiceProvider>(),
NullLoggerFactory.Instance,
new CircuitOptions(),
jsRuntime,
clientProxy,
NullLogger.Instance);
}

View File

@ -26,7 +26,7 @@ namespace Microsoft.AspNetCore.Components.Server.Tests
.UseRouting()
.UseEndpoints(endpoints =>
{
endpoints.MapBlazorHub<MyComponent>("app", dispatchOptions => called = true);
endpoints.MapBlazorHub(dispatchOptions => called = true);
}).Build();
// Assert
@ -45,7 +45,7 @@ namespace Microsoft.AspNetCore.Components.Server.Tests
.UseRouting()
.UseEndpoints(endpoints =>
{
endpoints.MapBlazorHub(Mock.Of<IComponent>().GetType(),"app", "_blazor", dispatchOptions => called = true);
endpoints.MapBlazorHub("_blazor", dispatchOptions => called = true);
}).Build();
// Assert

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
@ -18,6 +18,9 @@
<Compile Include="$(SignalRTestBase)MessagePackHubProtocolTestBase.cs" LinkBase="BlazorPack" />
<Compile Include="$(SignalRTestBase)TestBinder.cs" LinkBase="BlazorPack" />
<Compile Include="$(SignalRTestBase)TestHubMessageEqualityComparer.cs" LinkBase="BlazorPack" />
<Compile Include="$(RepoRoot)src\Mvc\Mvc.ViewFeatures\src\ServerComponentSerializer.cs" LinkBase="Mvc" />
<Compile Include="$(RepoRoot)src\Mvc\Mvc.ViewFeatures\src\ServerComponentInvocationSequence.cs" LinkBase="Mvc" />
</ItemGroup>
</Project>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -6,10 +6,11 @@ import { shouldAutoStart } from './BootCommon';
import { RenderQueue } from './Platform/Circuits/RenderQueue';
import { ConsoleLogger } from './Platform/Logging/Loggers';
import { LogLevel, Logger } from './Platform/Logging/Logger';
import { startCircuit } from './Platform/Circuits/CircuitManager';
import { discoverComponents, CircuitDescriptor } from './Platform/Circuits/CircuitManager';
import { setEventDispatcher } from './Rendering/RendererEventDispatcher';
import { resolveOptions, BlazorOptions } from './Platform/Circuits/BlazorOptions';
import { DefaultReconnectionHandler } from './Platform/Circuits/DefaultReconnectionHandler';
import { attachRootComponentToLogicalElement } from './Rendering/Renderer';
let renderingFailed = false;
let started = false;
@ -27,8 +28,15 @@ async function boot(userOptions?: Partial<BlazorOptions>): Promise<void> {
options.reconnectionHandler = options.reconnectionHandler || window['Blazor'].defaultReconnectionHandler;
logger.log(LogLevel.Information, 'Starting up blazor server-side application.');
const initialConnection = await initializeConnection(options, logger);
const circuit = await startCircuit(initialConnection);
const components = discoverComponents(document);
const circuit = new CircuitDescriptor(components);
const initialConnection = await initializeConnection(options, logger, circuit);
const circuitStarted = await circuit.startCircuit(initialConnection);
if (!circuitStarted) {
logger.log(LogLevel.Error, 'Failed to start the circuit.');
return;
}
const reconnect = async (existingConnection?: signalR.HubConnection): Promise<boolean> => {
if (renderingFailed) {
@ -36,7 +44,7 @@ async function boot(userOptions?: Partial<BlazorOptions>): Promise<void> {
return false;
}
const reconnection = existingConnection || await initializeConnection(options, logger);
const reconnection = existingConnection || await initializeConnection(options, logger, circuit);
if (!(await circuit.reconnect(reconnection))) {
logger.log(LogLevel.Information, 'Reconnection attempt to the circuit was rejected by the server. This may indicate that the associated state is no longer available on the server.');
return false;
@ -51,7 +59,8 @@ async function boot(userOptions?: Partial<BlazorOptions>): Promise<void> {
'unload',
() => {
const data = new FormData();
data.set('circuitId', circuit.circuitId);
const circuitId = circuit.circuitId!;
data.append('circuitId', circuitId);
navigator.sendBeacon('_blazor/disconnect', data);
},
false
@ -62,7 +71,7 @@ async function boot(userOptions?: Partial<BlazorOptions>): Promise<void> {
logger.log(LogLevel.Information, 'Blazor server-side application started.');
}
async function initializeConnection(options: BlazorOptions, logger: Logger): Promise<signalR.HubConnection> {
async function initializeConnection(options: BlazorOptions, logger: Logger, circuit: CircuitDescriptor): Promise<signalR.HubConnection> {
const hubProtocol = new MessagePackHubProtocol();
(hubProtocol as unknown as { name: string }).name = 'blazorpack';
@ -83,10 +92,11 @@ async function initializeConnection(options: BlazorOptions, logger: Logger): Pro
return connection.send('OnLocationChanged', uri, intercepted);
});
connection.on('JS.AttachComponent', (componentId, selector) => attachRootComponentToLogicalElement(0, circuit.resolveElement(selector), componentId));
connection.on('JS.BeginInvokeJS', DotNet.jsCallDispatcher.beginInvokeJSFromDotNet);
connection.on('JS.EndInvokeDotNet', (args: string) => DotNet.jsCallDispatcher.endInvokeDotNetFromJS(...(JSON.parse(args) as [string, boolean, unknown])));
const renderQueue = new RenderQueue(/* renderer ID unused with remote renderer */ 0, logger);
const renderQueue = RenderQueue.getOrCreate(logger);
connection.on('JS.RenderBatch', (batchId: number, batchData: Uint8Array) => {
logger.log(LogLevel.Debug, `Received render batch with id ${batchId} and ${batchData.byteLength} bytes.`);
renderQueue.processBatch(batchId, batchData, connection);

View File

@ -1,22 +1,274 @@
import { internalFunctions as navigationManagerFunctions } from '../../Services/NavigationManager';
import { toLogicalRootCommentElement, LogicalElement } from '../../Rendering/LogicalElements';
export class CircuitDescriptor {
public circuitId: string;
public circuitId?: string;
public constructor(circuitId: string) {
this.circuitId = circuitId;
public components: ComponentDescriptor[];
public constructor(components: ComponentDescriptor[]) {
this.circuitId = undefined;
this.components = components;
}
public reconnect(reconnection: signalR.HubConnection): Promise<boolean> {
if (!this.circuitId) {
throw new Error('Circuit host not initialized.');
}
return reconnection.invoke<boolean>('ConnectCircuit', this.circuitId);
}
public initialize(circuitId: string): void {
if (this.circuitId) {
throw new Error(`Circuit host '${this.circuitId}' already initialized.`);
}
this.circuitId = circuitId;
}
public async startCircuit(connection: signalR.HubConnection): Promise<boolean> {
const result = await connection.invoke<string>(
'StartCircuit',
navigationManagerFunctions.getBaseURI(),
navigationManagerFunctions.getLocationHref(),
JSON.stringify(this.components.map(c => c.toRecord()))
);
if (result) {
this.initialize(result);
return true;
} else {
return false;
}
}
public resolveElement(sequence: string): LogicalElement {
const parsedSequence = Number.parseInt(sequence);
if (!Number.isNaN(parsedSequence)) {
return toLogicalRootCommentElement(this.components[parsedSequence].start as Comment, this.components[parsedSequence].end as Comment);
} else {
throw new Error(`Invalid sequence number '${sequence}'.`);
}
}
}
export async function startCircuit(connection: signalR.HubConnection): Promise<CircuitDescriptor> {
const result = await connection.invoke<string>('StartCircuit', navigationManagerFunctions.getBaseURI(), navigationManagerFunctions.getLocationHref());
if (result) {
return new CircuitDescriptor(result);
} else {
throw new Error('Circuit failed to start');
interface ComponentMarker {
type: string;
sequence: number;
descriptor: string;
}
export class ComponentDescriptor {
public type: string;
public start: Node;
public end?: Node;
public sequence: number;
public descriptor: string;
public constructor(type: string, start: Node, end: Node | undefined, sequence: number, descriptor: string) {
this.type = type;
this.start = start;
this.end = end;
this.sequence = sequence;
this.descriptor = descriptor;
}
public toRecord(): ComponentMarker {
const result = { type: this.type, sequence: this.sequence, descriptor: this.descriptor };
return result;
}
}
export function discoverComponents(document: Document): ComponentDescriptor[] {
const componentComments = resolveComponentComments(document);
const discoveredComponents: ComponentDescriptor[] = [];
for (let i = 0; i < componentComments.length; i++) {
const componentComment = componentComments[i];
const entry = new ComponentDescriptor(
componentComment.type,
componentComment.start,
componentComment.end,
componentComment.sequence,
componentComment.descriptor,
);
discoveredComponents.push(entry);
}
return discoveredComponents;
}
interface ComponentComment {
type: 'server';
sequence: number;
descriptor: string;
start: Node;
end?: Node;
prerenderId?: string;
}
function resolveComponentComments(node: Node): ComponentComment[] {
if (!node.hasChildNodes()) {
return [];
}
const result: ComponentComment[] = [];
const childNodeIterator = new ComponentCommentIterator(node.childNodes);
while (childNodeIterator.next() && childNodeIterator.currentElement) {
const componentComment = getComponentComment(childNodeIterator);
if (componentComment) {
result.push(componentComment);
} else {
const childResults = resolveComponentComments(childNodeIterator.currentElement);
for (let j = 0; j < childResults.length; j++) {
const childResult = childResults[j];
result.push(childResult);
}
}
}
return result;
}
const blazorCommentRegularExpression = /\W*Blazor:[^{]*(.*)$/;
function getComponentComment(commentNodeIterator: ComponentCommentIterator): ComponentComment | undefined {
const candidateStart = commentNodeIterator.currentElement;
if (!candidateStart || candidateStart.nodeType !== Node.COMMENT_NODE) {
return;
}
if (candidateStart.textContent) {
const componentStartComment = new RegExp(blazorCommentRegularExpression);
const definition = componentStartComment.exec(candidateStart.textContent);
const json = definition && definition[1];
if (json) {
try {
return createComponentComment(json, candidateStart, commentNodeIterator);
} catch (error) {
throw new Error(`Found malformed component comment at ${candidateStart.textContent}`);
}
} else {
return;
}
}
}
function createComponentComment(json: string, start: Node, iterator: ComponentCommentIterator): ComponentComment {
const payload = JSON.parse(json) as ComponentComment;
const { type, sequence, descriptor, prerenderId } = payload;
if (type !== 'server') {
throw new Error(`Invalid component type '${type}'.`);
}
if (!descriptor) {
throw new Error('descriptor must be defined when using a descriptor.');
}
if (sequence === undefined) {
throw new Error('sequence must be defined when using a descriptor.');
}
if (!Number.isInteger(sequence)) {
throw new Error(`Error parsing the sequence '${sequence}' for component '${json}'`);
}
if (!prerenderId) {
return {
type,
sequence: sequence,
descriptor,
start,
};
} else {
const end = getComponentEndComment(prerenderId, iterator);
if (!end) {
throw new Error(`Could not find an end component comment for '${start}'`);
}
return {
type,
sequence,
descriptor,
start,
prerenderId,
end,
};
}
}
function getComponentEndComment(prerenderedId: string, iterator: ComponentCommentIterator): ChildNode | undefined {
while (iterator.next() && iterator.currentElement) {
const node = iterator.currentElement;
if (node.nodeType !== Node.COMMENT_NODE) {
continue;
}
if (!node.textContent) {
continue;
}
const definition = new RegExp(blazorCommentRegularExpression).exec(node.textContent);
const json = definition && definition[1];
if (!json) {
continue;
}
validateEndComponentPayload(json, prerenderedId);
return node;
}
return undefined;
}
function validateEndComponentPayload(json: string, prerenderedId: string): void {
const payload = JSON.parse(json) as ComponentComment;
if (Object.keys(payload).length !== 1) {
throw new Error(`Invalid end of component comment: '${json}'`);
}
const prerenderedEndId = payload.prerenderId;
if (!prerenderedEndId) {
throw new Error(`End of component comment must have a value for the prerendered property: '${json}'`);
}
if (prerenderedEndId !== prerenderedId) {
throw new Error(`End of component comment prerendered property must match the start comment prerender id: '${prerenderedId}', '${prerenderedEndId}'`);
}
}
class ComponentCommentIterator {
private childNodes: NodeListOf<ChildNode>;
private currentIndex: number;
private length: number;
public currentElement: ChildNode | undefined;
public constructor(childNodes: NodeListOf<ChildNode>) {
this.childNodes = childNodes;
this.currentIndex = -1;
this.length = childNodes.length;
}
public next(): boolean {
this.currentIndex++;
if (this.currentIndex < this.length) {
this.currentElement = this.childNodes[this.currentIndex];
return true;
} else {
this.currentElement = undefined;
return false;
}
}
}

View File

@ -4,6 +4,8 @@ import { Logger, LogLevel } from '../Logging/Logger';
import { HubConnection } from '@aspnet/signalr';
export class RenderQueue {
private static instance: RenderQueue;
private nextBatchId = 2;
private fatalError?: string;
@ -17,6 +19,14 @@ export class RenderQueue {
this.logger = logger;
}
public static getOrCreate(logger: Logger): RenderQueue {
if (!RenderQueue.instance) {
RenderQueue.instance = new RenderQueue(0, logger);
}
return this.instance;
}
public async processBatch(receivedBatchId: number, batchData: Uint8Array, connection: HubConnection): Promise<void> {
if (receivedBatchId < this.nextBatchId) {
// SignalR delivers messages in order, but it does not guarantee that the message gets delivered.

View File

@ -46,7 +46,7 @@ export function toLogicalRootCommentElement(start: Comment, end: Comment): Logic
// |- *div
// |- *component
// |- *footer
if (!start.parentNode){
if (!start.parentNode) {
throw new Error(`Comment not connected to the DOM ${start.textContent}`);
}
@ -55,8 +55,11 @@ export function toLogicalRootCommentElement(start: Comment, end: Comment): Logic
const children = getLogicalChildrenArray(parentLogicalElement);
Array.from(parent.childNodes).forEach(n => children.push(n as unknown as LogicalElement));
start[logicalParentPropname] = parentLogicalElement;
start[logicalEndSiblingPropname] = end;
toLogicalElement(end, /* allowExistingcontents */ true);
// We might not have an end comment in the case of non-prerendered components.
if (end) {
start[logicalEndSiblingPropname] = end;
toLogicalElement(end, /* allowExistingcontents */ true);
}
return toLogicalElement(start, /* allowExistingContents */ true);
}

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<!-- Shared testing infrastructure for running E2E tests using selenium -->
<Import Project="$(SharedSourceRoot)E2ETesting\E2ETesting.props" />
@ -51,4 +51,11 @@
<!-- Shared testing infrastructure for running E2E tests using selenium -->
<Import Project="$(SharedSourceRoot)E2ETesting\E2ETesting.targets" />
<ItemGroup>
<!-- Shared descriptor infrastructure with MVC -->
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponent.cs" />
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponentSerializationSettings.cs" />
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponentMarker.cs" />
</ItemGroup>
</Project>

View File

@ -4,13 +4,13 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Data;
using System.Text.RegularExpressions;
using System.Diagnostics;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Ignitor;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.DependencyInjection;
@ -18,13 +18,12 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Testing;
using Xunit;
using Xunit.Abstractions;
using Microsoft.AspNetCore.Components.RenderTree;
namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
{
public class ComponentHubReliabilityTest : IClassFixture<AspNetSiteServerFixture>, IDisposable
{
private static readonly TimeSpan DefaultLatencyTimeout = TimeSpan.FromSeconds(Debugger.IsAttached ? 60 : 10);
private static readonly TimeSpan DefaultLatencyTimeout = Debugger.IsAttached ? TimeSpan.MaxValue : TimeSpan.FromSeconds(10);
private readonly AspNetSiteServerFixture _serverFixture;
public ComponentHubReliabilityTest(AspNetSiteServerFixture serverFixture, ITestOutputHelper output)
@ -75,14 +74,16 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
var expectedError = "The circuit host '.*?' has already been initialized.";
var rootUri = _serverFixture.RootUri;
var baseUri = new Uri(rootUri, "/subdir");
Assert.True(await Client.ConnectAsync(baseUri, prerendered: false), "Couldn't connect to the app");
Assert.True(await Client.ConnectAsync(baseUri), "Couldn't connect to the app");
Assert.Single(Batches);
var descriptors = await Client.GetPrerenderDescriptors(baseUri);
// Act
await Client.ExpectCircuitErrorAndDisconnect(() => Client.HubConnection.SendAsync(
"StartCircuit",
baseUri,
baseUri + "/home"));
baseUri + "/home",
descriptors));
// Assert
var actualError = Assert.Single(Errors);
@ -97,10 +98,11 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
var expectedError = "The uris provided are invalid.";
var rootUri = _serverFixture.RootUri;
var uri = new Uri(rootUri, "/subdir");
Assert.True(await Client.ConnectAsync(uri, prerendered: false, connectAutomatically: false), "Couldn't connect to the app");
Assert.True(await Client.ConnectAsync(uri, connectAutomatically: false), "Couldn't connect to the app");
var descriptors = await Client.GetPrerenderDescriptors(uri);
// Act
await Client.ExpectCircuitErrorAndDisconnect(() => Client.HubConnection.SendAsync("StartCircuit", null, null));
await Client.ExpectCircuitErrorAndDisconnect(() => Client.HubConnection.SendAsync("StartCircuit", null, null, descriptors));
// Assert
var actualError = Assert.Single(Errors);
@ -117,12 +119,13 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
var expectedError = "The circuit failed to initialize.";
var rootUri = _serverFixture.RootUri;
var uri = new Uri(rootUri, "/subdir");
Assert.True(await Client.ConnectAsync(uri, prerendered: false, connectAutomatically: false), "Couldn't connect to the app");
Assert.True(await Client.ConnectAsync(uri, connectAutomatically: false), "Couldn't connect to the app");
var descriptors = await Client.GetPrerenderDescriptors(uri);
// Act
//
// These are valid URIs by the BaseUri doesn't contain the Uri - so it fails to initialize.
await Client.ExpectCircuitErrorAndDisconnect(() => Client.HubConnection.SendAsync("StartCircuit", uri, "http://example.com"));
await Client.ExpectCircuitErrorAndDisconnect(() => Client.HubConnection.SendAsync("StartCircuit", uri, "http://example.com", descriptors));
// Assert
var actualError = Assert.Single(Errors);
@ -137,7 +140,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
var expectedError = "Circuit not initialized.";
var rootUri = _serverFixture.RootUri;
var baseUri = new Uri(rootUri, "/subdir");
Assert.True(await Client.ConnectAsync(baseUri, prerendered: false, connectAutomatically: false));
Assert.True(await Client.ConnectAsync(baseUri, connectAutomatically: false));
Assert.Empty(Batches);
// Act
@ -163,7 +166,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
var expectedError = "Circuit not initialized.";
var rootUri = _serverFixture.RootUri;
var baseUri = new Uri(rootUri, "/subdir");
Assert.True(await Client.ConnectAsync(baseUri, prerendered: false, connectAutomatically: false));
Assert.True(await Client.ConnectAsync(baseUri, connectAutomatically: false));
Assert.Empty(Batches);
// Act
@ -187,7 +190,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
var expectedError = "Circuit not initialized.";
var rootUri = _serverFixture.RootUri;
var baseUri = new Uri(rootUri, "/subdir");
Assert.True(await Client.ConnectAsync(baseUri, prerendered: false, connectAutomatically: false));
Assert.True(await Client.ConnectAsync(baseUri, connectAutomatically: false));
Assert.Empty(Batches);
// Act
@ -206,7 +209,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
private async Task GoToTestComponent(IList<Batch> batches)
{
var rootUri = _serverFixture.RootUri;
Assert.True(await Client.ConnectAsync(new Uri(rootUri, "/subdir"), prerendered: false), "Couldn't connect to the app");
Assert.True(await Client.ConnectAsync(new Uri(rootUri, "/subdir")), "Couldn't connect to the app");
Assert.Single(batches);
await Client.SelectAsync("test-selector-select", "BasicTestApp.CounterComponent");
@ -323,7 +326,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
var expectedError = "Circuit not initialized.";
var rootUri = _serverFixture.RootUri;
var baseUri = new Uri(rootUri, "/subdir");
Assert.True(await Client.ConnectAsync(baseUri, prerendered: false, connectAutomatically: false));
Assert.True(await Client.ConnectAsync(baseUri, connectAutomatically: false));
Assert.Empty(Batches);
// Act
@ -346,7 +349,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
var expectedError = "Circuit not initialized.";
var rootUri = _serverFixture.RootUri;
var baseUri = new Uri(rootUri, "/subdir");
Assert.True(await Client.ConnectAsync(baseUri, prerendered: false, connectAutomatically: false));
Assert.True(await Client.ConnectAsync(baseUri, connectAutomatically: false));
Assert.Empty(Batches);
// Act
@ -372,7 +375,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
var rootUri = _serverFixture.RootUri;
var baseUri = new Uri(rootUri, "/subdir");
Assert.True(await Client.ConnectAsync(baseUri, prerendered: false), "Couldn't connect to the app");
Assert.True(await Client.ConnectAsync(baseUri), "Couldn't connect to the app");
Assert.Single(Batches);
// Act
@ -401,7 +404,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
var rootUri = _serverFixture.RootUri;
var baseUri = new Uri(rootUri, "/subdir");
Assert.True(await Client.ConnectAsync(baseUri, prerendered: false), "Couldn't connect to the app");
Assert.True(await Client.ConnectAsync(baseUri), "Couldn't connect to the app");
Assert.Single(Batches);
await Client.SelectAsync("test-selector-select", "BasicTestApp.NavigationFailureComponent");
@ -435,7 +438,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
var expectedError = "Unhandled exception in circuit .*";
var rootUri = _serverFixture.RootUri;
var baseUri = new Uri(rootUri, "/subdir");
Assert.True(await Client.ConnectAsync(baseUri, prerendered: false), "Couldn't connect to the app");
Assert.True(await Client.ConnectAsync(baseUri), "Couldn't connect to the app");
Assert.Single(Batches);
await Client.SelectAsync("test-selector-select", "BasicTestApp.ReliabilityComponent");
@ -466,7 +469,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
var expectedError = "Unhandled exception in circuit .*";
var rootUri = _serverFixture.RootUri;
var baseUri = new Uri(rootUri, "/subdir");
Assert.True(await Client.ConnectAsync(baseUri, prerendered: false), "Couldn't connect to the app");
Assert.True(await Client.ConnectAsync(baseUri), "Couldn't connect to the app");
Assert.Single(Batches);
await Client.SelectAsync("test-selector-select", "BasicTestApp.ReliabilityComponent");

View File

@ -9,7 +9,6 @@ using System.Text.Json;
using System.Threading.Tasks;
using Ignitor;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.SignalR.Client;
@ -590,6 +589,8 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
// Act
await Client.ClickAsync("event-handler-throw-sync", expectRenderBatch: false);
await Task.Delay(1000);
Assert.Contains(
logEvents,
e => LogLevel.Error == e.logLevel &&
@ -618,7 +619,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
private async Task GoToTestComponent(IList<Batch> batches)
{
var rootUri = _serverFixture.RootUri;
Assert.True(await Client.ConnectAsync(new Uri(rootUri, "/subdir"), prerendered: false), "Couldn't connect to the app");
Assert.True(await Client.ConnectAsync(new Uri(rootUri, "/subdir")), "Couldn't connect to the app");
Assert.Single(batches);
await Client.SelectAsync("test-selector-select", "BasicTestApp.ReliabilityComponent");

View File

@ -0,0 +1,82 @@
// 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.Linq;
using System.Text.Json;
using System.Text.RegularExpressions;
using BasicTestApp;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
using Microsoft.AspNetCore.E2ETesting;
using OpenQA.Selenium;
using Xunit;
using Xunit.Abstractions;
namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
{
public class MultipleComponentsTest : BasicTestAppTestBase
{
private const string MarkerPattern = ".*?<!--Blazor:(.*?)-->.*?";
public MultipleComponentsTest(
BrowserFixture browserFixture,
ToggleExecutionModeServerFixture<Program> serverFixture,
ITestOutputHelper output)
: base(browserFixture, serverFixture.WithServerExecution(), output)
{
}
[Fact]
public void CanRenderMultipleRootComponents()
{
Navigate("/prerendered/multiple-components");
var greets = Browser.FindElements(By.CssSelector(".greet-wrapper .greet")).Select(e => e.Text).ToArray();
Assert.Equal(4, greets.Length); // 1 statically rendered + 3 prerendered
Assert.Single(greets, "Hello John");
Assert.Equal(3, greets.Where(g => string.Equals("Hello", g)).Count()); // 3 prerendered
var content = Browser.FindElement(By.Id("test-container")).GetAttribute("innerHTML");
var markers = ReadMarkers(content);
var componentSequence = markers.Select(m => m.Item1.PrerenderId != null).ToArray();
var expectedComponentSequence = new bool[]
{
// true means it was a prerendered component.
true,
false,
false,
false,
true,
false,
true,
};
Assert.Equal(expectedComponentSequence, componentSequence);
// Once connected, output changes
BeginInteractivity();
Browser.Exists(By.CssSelector("h3.interactive"));
var updatedGreets = Browser.FindElements(By.CssSelector(".greet-wrapper .greet")).Select(e => e.Text).ToArray();
Assert.Equal(7, updatedGreets.Where(g => string.Equals("Hello Alfred", g)).Count());
}
private (ServerComponentMarker, ServerComponentMarker)[] ReadMarkers(string content)
{
content = content.Replace("\r\n", "");
var matches = Regex.Matches(content, MarkerPattern);
var markers = matches.Select(s => JsonSerializer.Deserialize<ServerComponentMarker>(
s.Groups[1].Value,
ServerComponentSerializationSettings.JsonSerializationOptions));
var prerenderMarkers = markers.Where(m => m.PrerenderId != null).GroupBy(p => p.PrerenderId).Select(g => (g.First(), g.Skip(1).First())).ToArray();
var nonPrerenderMarkers = markers.Where(m => m.PrerenderId == null).Select(g => (g, (ServerComponentMarker)default)).ToArray();
return prerenderMarkers.Concat(nonPrerenderMarkers).OrderBy(m => m.Item1.Sequence).ToArray();
}
private void BeginInteractivity()
{
Browser.FindElement(By.Id("load-boot-script")).Click();
}
}
}

View File

@ -51,7 +51,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
{
// Arrange
var baseUri = new Uri(_serverFixture.RootUri, "/subdir");
Assert.True(await Client.ConnectAsync(baseUri, prerendered: false), "Couldn't connect to the app");
Assert.True(await Client.ConnectAsync(baseUri), "Couldn't connect to the app");
Assert.Single(Batches);
await Client.SelectAsync("test-selector-select", "BasicTestApp.LimitCounterComponent");

View File

@ -20,7 +20,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
{
}
[Theory(Skip = "https://github.com/aspnet/AspNetCore/issues/12788")]
[Theory]
[InlineData(null, null)]
[InlineData(null, "Someone")]
[InlineData("Someone", null)]

View File

@ -138,7 +138,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
}
}
[Fact(Skip = "https://github.com/aspnet/AspNetCore/issues/12788")]
[Fact]
public void ReconnectUI()
{
Browser.FindElement(By.LinkText("Counter")).Click();
@ -155,7 +155,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
.Until(driver => reconnectionDialog.GetCssValue("display") == "none");
}
[Fact(Skip = "https://github.com/aspnet/AspNetCore/issues/12788")]
[Fact]
public void RendersContinueAfterReconnect()
{
Browser.FindElement(By.LinkText("Ticker")).Click();

View File

@ -0,0 +1,19 @@
<div class="greet-wrapper">
<h3 class="@interactive">Greeter component</h3>
<p class="greet">Hello @Name</p>
</div>
@code{
[Parameter] public string Name { get; set; }
private string interactive = "";
protected override void OnAfterRender(bool firstRender)
{
if (firstRender)
{
Name = "Alfred";
interactive = "interactive";
StateHasChanged();
}
}
}

View File

@ -30,6 +30,11 @@
// Load either blazor.webassembly.js or blazor.server.js depending
// on the hash part of the URL. This is just to give a way for the
// test runner to make the selection.
if (location.hash === '#server') {
location.hash = '';
document.cookie = '__blazor_execution_mode=server';
location.reload();
}
var src = location.hash === '#server'
? 'blazor.server.js'
: 'blazor.webassembly.js';

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>

View File

@ -12,8 +12,7 @@
<link href="css/site.css" rel="stylesheet" />
</head>
<body>
<app>@(await Html.RenderComponentAsync<App>(new { Name = "Guest" }))</app>
<app>@(await Html.RenderComponentAsync<App>(RenderMode.Server))</app>
<script src="_framework/blazor.server.js" autostart="false"></script>
<script>
Blazor.start({

View File

@ -38,7 +38,7 @@ namespace ComponentsApp.Server
{
endpoints.MapRazorPages();
endpoints.MapControllers();
endpoints.MapBlazorHub<ComponentsApp.App.App>("app");
endpoints.MapBlazorHub();
endpoints.MapFallbackToPage("/_Host");
});
}

View File

@ -2,6 +2,9 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text.Json;
using System.Text.RegularExpressions;
@ -17,6 +20,8 @@ namespace Ignitor
{
public class BlazorClient
{
private const string MarkerPattern = ".*?<!--Blazor:(.*?)-->.*?";
public BlazorClient()
{
CancellationTokenSource = new CancellationTokenSource();
@ -29,6 +34,7 @@ namespace Ignitor
}
public TimeSpan? DefaultLatencyTimeout { get; set; } = TimeSpan.FromMilliseconds(500);
public TimeSpan? DefaultConnectTimeout { get; set; } = TimeSpan.FromSeconds(10);
public Func<string, Exception> FormatError { get; set; }
@ -265,7 +271,7 @@ namespace Ignitor
await PrepareForNextDisconnect(timeout ?? DefaultLatencyTimeout);
}
public async Task<bool> ConnectAsync(Uri uri, bool prerendered, bool connectAutomatically = true)
public async Task<bool> ConnectAsync(Uri uri, bool connectAutomatically = true)
{
var builder = new HubConnectionBuilder();
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IHubProtocol, IgnitorMessagePackHubProtocol>());
@ -293,21 +299,11 @@ namespace Ignitor
return true;
}
// Now everything is registered so we can start the circuit.
if (prerendered)
{
CircuitId = await GetPrerenderedCircuitIdAsync(uri);
var result = false;
await ExpectRenderBatch(async () => result = await HubConnection.InvokeAsync<bool>("ConnectCircuit", CircuitId));
return result;
}
else
{
await ExpectRenderBatch(
async () => CircuitId = await HubConnection.InvokeAsync<string>("StartCircuit", uri, uri),
TimeSpan.FromSeconds(10));
return CircuitId != null;
}
var descriptors = await GetPrerenderDescriptors(uri);
await ExpectRenderBatch(
async () => CircuitId = await HubConnection.InvokeAsync<string>("StartCircuit", uri, uri, descriptors),
DefaultConnectTimeout);
return CircuitId != null;
}
private void OnEndInvokeDotNet(string completion)
@ -391,17 +387,16 @@ namespace Ignitor
await ExpectDotNetInterop(() => HubConnection.InvokeAsync("BeginInvokeDotNetFromJS", callId?.ToString(), assemblyName, methodIdentifier, dotNetObjectId ?? 0, argsJson));
}
private static async Task<string> GetPrerenderedCircuitIdAsync(Uri uri)
public async Task<string> GetPrerenderDescriptors(Uri uri)
{
var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Cookie", "__blazor_execution_mode=server");
var response = await httpClient.GetAsync(uri);
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
// <!-- M.A.C.Component:{"circuitId":"CfDJ8KZCIaqnXmdF...PVd6VVzfnmc1","rendererId":"0","componentId":"0"} -->
var match = Regex.Match(content, $"{Regex.Escape("<!-- M.A.C.Component:")}(.+?){Regex.Escape(" -->")}");
var json = JsonDocument.Parse(match.Groups[1].Value);
var circuitId = json.RootElement.GetProperty("circuitId").GetString();
return circuitId;
var match = ReadMarkers(content);
return $"[{string.Join(", ", match)}]";
}
public void Cancel()
@ -469,5 +464,22 @@ namespace Ignitor
public CancellationTokenRegistration CancellationRegistration { get; set; }
}
private string[] ReadMarkers(string content)
{
content = content.Replace("\r\n", "").Replace("\n", "");
var matches = Regex.Matches(content, MarkerPattern);
var markers = matches.Select(s => (value: s.Groups[1].Value, parsed: JsonDocument.Parse(s.Groups[1].Value)))
.Where(s =>
{
var markerType = s.parsed.RootElement.GetProperty("type");
return markerType.ValueKind != JsonValueKind.Undefined && markerType.GetString() == "server";
})
.OrderBy(p => p.parsed.RootElement.GetProperty("sequence").GetInt32())
.Select(p => p.value)
.ToArray();
return markers;
}
}
}

View File

@ -50,7 +50,7 @@ namespace Ignitor
}
};
await client.ConnectAsync(uri, prerendered: true);
await client.ConnectAsync(uri);
await done.Task;
return 0;

View File

@ -0,0 +1,55 @@
@page "/multiple-components"
@using BasicTestApp.MultipleComponents;
<!DOCTYPE html>
<html>
<head>
<title>Multiple component entry points</title>
<base href="~/" />
@* This page is used to validate the ability to render multiple root components in a blazor server-side application.
*@
</head>
<body>
<div id="test-container">
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.ServerPrerendered))
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.Server))
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.Static, new { Name = "John" }))
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.Server))
<div id="container">
<p>Some content before</p>
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.Server))
<p>Some content between</p>
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.ServerPrerendered))
<p>Some content after</p>
<div id="nested-an-extra-level">
<p>Some content before</p>
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.Server))
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.ServerPrerendered))
<p>Some content after</p>
</div>
</div>
</div>
@*
So that E2E tests can make assertions about both the prerendered and
interactive states, we only load the .js file when told to.
*@
<hr />
<button id="load-boot-script" onclick="start()">Load boot script</button>
<script src="_framework/blazor.server.js" autostart="false"></script>
<script>
// Used by InteropOnInitializationComponent
function setElementValue(element, newValue) {
element.value = newValue;
return element.value;
}
function start() {
Blazor.start({
logLevel: 1 // LogLevel.Debug
});
}
</script>
</body>
</html>

View File

@ -7,7 +7,7 @@
<base href="~/" />
</head>
<body>
<app>@(await Html.RenderComponentAsync<TestRouter>())</app>
<app>@(await Html.RenderComponentAsync<TestRouter>(RenderMode.ServerPrerendered))</app>
@*
So that E2E tests can make assertions about both the prerendered and

View File

@ -0,0 +1,39 @@
@page ""
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Basic test app</title>
<base href="~/" />
<link href="style.css" rel="stylesheet" />
<!-- Used by ExternalContentPackage -->
<link href="_content/TestContentPackage/styles.css" rel="stylesheet" />
</head>
<body>
<root>@(await Html.RenderComponentAsync<BasicTestApp.Index>(RenderMode.Server))</root>
<!-- Used for testing interop scenarios between JS and .NET -->
<script src="js/jsinteroptests.js"></script>
<script>
// Used by ElementRefComponent
function setElementValue(element, newValue) {
element.value = newValue;
return element.value;
}
function navigationManagerNavigate() {
Blazor.navigateTo('/subdir/some-path');
}
</script>
<script src="_framework/blazor.server.js"></script>
<!-- Used by ExternalContentPackage -->
<script src="_content/TestContentPackage/prompt.js"></script>
<script>
console.log('Blazor server-side');
</script>
</body>
</html>

View File

@ -1,10 +1,7 @@
using System.Threading.Tasks;
using BasicTestApp;
using BasicTestApp.RouterTest;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Localization;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
@ -82,16 +79,30 @@ namespace TestServer
options.RequestCultureProviders.Add(new CookieRequestCultureProvider());
// We want the default to be en-US so that the tests for bind can work consistently.
options.SetDefaultCulture("en-US");
options.SetDefaultCulture("en-US");
});
app.UseRouting();
app.MapWhen(ctx => ctx.Request.Cookies.TryGetValue("__blazor_execution_mode", out var value) && value == "server",
child =>
{
child.UseRouting();
child.UseEndpoints(childEndpoints =>
{
childEndpoints.MapBlazorHub();
childEndpoints.MapFallbackToPage("/_ServerHost");
});
});
app.UseEndpoints(endpoints =>
{
endpoints.MapBlazorHub(typeof(Index), selector: "root");
endpoints.MapFallbackToClientSideBlazor<BasicTestApp.Startup>("index.html");
});
app.MapWhen(ctx => !ctx.Request.Query.ContainsKey("__blazor_execution_mode"),
child =>
{
child.UseRouting();
child.UseEndpoints(childEndpoints =>
{
childEndpoints.MapBlazorHub();
childEndpoints.MapFallbackToClientSideBlazor<BasicTestApp.Startup>("index.html");
});
});
});
// Separately, mount a prerendered server-side Blazor app on /prerendered
@ -103,7 +114,7 @@ namespace TestServer
app.UseEndpoints(endpoints =>
{
endpoints.MapFallbackToPage("/PrerenderedHost");
endpoints.MapBlazorHub<TestRouter>(selector: "app");
endpoints.MapBlazorHub();
});
});

View File

@ -7,6 +7,7 @@
<Compile Include="Microsoft.AspNetCore.Mvc.ViewFeatures.netcoreapp3.0.cs" />
<Reference Include="Microsoft.AspNetCore.Mvc.Core" />
<Reference Include="Microsoft.AspNetCore.Mvc.DataAnnotations" />
<Reference Include="Microsoft.AspNetCore.DataProtection.Extensions" />
<Reference Include="Microsoft.AspNetCore.Antiforgery" />
<Reference Include="Microsoft.AspNetCore.Diagnostics.Abstractions" />
<Reference Include="Microsoft.AspNetCore.Html.Abstractions" />

View File

@ -324,9 +324,9 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
}
public static partial class HtmlHelperComponentExtensions
{
public static System.Threading.Tasks.Task<Microsoft.AspNetCore.Html.IHtmlContent> RenderComponentAsync<TComponent>(this Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper htmlHelper) where TComponent : Microsoft.AspNetCore.Components.IComponent { throw null; }
public static System.Threading.Tasks.Task<Microsoft.AspNetCore.Html.IHtmlContent> RenderComponentAsync<TComponent>(this Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper htmlHelper, Microsoft.AspNetCore.Mvc.Rendering.RenderMode renderMode) where TComponent : Microsoft.AspNetCore.Components.IComponent { throw null; }
[System.Diagnostics.DebuggerStepThroughAttribute]
public static System.Threading.Tasks.Task<Microsoft.AspNetCore.Html.IHtmlContent> RenderComponentAsync<TComponent>(this Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper htmlHelper, object parameters) where TComponent : Microsoft.AspNetCore.Components.IComponent { throw null; }
public static System.Threading.Tasks.Task<Microsoft.AspNetCore.Html.IHtmlContent> RenderComponentAsync<TComponent>(this Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper htmlHelper, Microsoft.AspNetCore.Mvc.Rendering.RenderMode renderMode, object parameters) where TComponent : Microsoft.AspNetCore.Components.IComponent { throw null; }
}
public static partial class HtmlHelperDisplayExtensions
{
@ -610,6 +610,12 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
public void EndForm() { }
protected virtual void GenerateEndForm() { }
}
public enum RenderMode
{
Static = 1,
Server = 2,
ServerPrerendered = 3,
}
public partial class SelectList : Microsoft.AspNetCore.Mvc.Rendering.MultiSelectList
{
public SelectList(System.Collections.IEnumerable items) : base (default(System.Collections.IEnumerable)) { }

View File

@ -175,6 +175,9 @@ namespace Microsoft.Extensions.DependencyInjection
services.TryAddSingleton<IJsonHelper, SystemTextJsonHelper>();
// Component services for Blazor server-side interop
services.TryAddSingleton<ServerComponentSerializer>();
//
// View Components
//

View File

@ -5,6 +5,7 @@ using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponents;
using Microsoft.Extensions.DependencyInjection;
@ -16,19 +17,22 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
/// </summary>
public static class HtmlHelperComponentExtensions
{
private static readonly object ComponentSequenceKey = new object();
/// <summary>
/// Renders the <typeparamref name="TComponent"/> <see cref="IComponent"/>.
/// </summary>
/// <param name="htmlHelper">The <see cref="IHtmlHelper"/>.</param>
/// <param name="renderMode">The <see cref="RenderMode"/> for the component.</param>
/// <returns>The HTML produced by the rendered <typeparamref name="TComponent"/>.</returns>
public static Task<IHtmlContent> RenderComponentAsync<TComponent>(this IHtmlHelper htmlHelper) where TComponent : IComponent
public static Task<IHtmlContent> RenderComponentAsync<TComponent>(this IHtmlHelper htmlHelper, RenderMode renderMode) where TComponent : IComponent
{
if (htmlHelper == null)
{
throw new ArgumentNullException(nameof(htmlHelper));
}
return htmlHelper.RenderComponentAsync<TComponent>(null);
return htmlHelper.RenderComponentAsync<TComponent>(renderMode, null);
}
/// <summary>
@ -37,9 +41,11 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
/// <param name="htmlHelper">The <see cref="IHtmlHelper"/>.</param>
/// <param name="parameters">An <see cref="object"/> containing the parameters to pass
/// to the component.</param>
/// <param name="renderMode">The <see cref="RenderMode"/> for the component.</param>
/// <returns>The HTML produced by the rendered <typeparamref name="TComponent"/>.</returns>
public static async Task<IHtmlContent> RenderComponentAsync<TComponent>(
this IHtmlHelper htmlHelper,
RenderMode renderMode,
object parameters) where TComponent : IComponent
{
if (htmlHelper == null)
@ -47,20 +53,84 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
throw new ArgumentNullException(nameof(htmlHelper));
}
var httpContext = htmlHelper.ViewContext.HttpContext;
var serviceProvider = httpContext.RequestServices;
var prerenderer = serviceProvider.GetRequiredService<StaticComponentRenderer>();
var context = htmlHelper.ViewContext.HttpContext;
return renderMode switch
{
RenderMode.Server => NonPrerenderedServerComponent(context, GetOrCreateInvocationId(htmlHelper.ViewContext), typeof(TComponent), GetParametersCollection(parameters)),
RenderMode.ServerPrerendered => await PrerenderedServerComponentAsync(context, GetOrCreateInvocationId(htmlHelper.ViewContext), typeof(TComponent), GetParametersCollection(parameters)),
RenderMode.Static => await StaticComponentAsync(context, typeof(TComponent), GetParametersCollection(parameters)),
_ => throw new ArgumentException("Invalid render mode", nameof(renderMode)),
};
}
var parametersCollection = parameters == null ?
private static ServerComponentInvocationSequence GetOrCreateInvocationId(ViewContext viewContext)
{
if (!viewContext.Items.TryGetValue(ComponentSequenceKey, out var result))
{
result = new ServerComponentInvocationSequence();
viewContext.Items[ComponentSequenceKey] = result;
}
return (ServerComponentInvocationSequence)result;
}
private static ParameterView GetParametersCollection(object parameters) => parameters == null ?
ParameterView.Empty :
ParameterView.FromDictionary(HtmlHelper.ObjectToDictionary(parameters));
private static async Task<IHtmlContent> StaticComponentAsync(HttpContext context, Type type, ParameterView parametersCollection)
{
var serviceProvider = context.RequestServices;
var prerenderer = serviceProvider.GetRequiredService<StaticComponentRenderer>();
var result = await prerenderer.PrerenderComponentAsync(
parametersCollection,
httpContext,
typeof(TComponent));
context,
type);
return new ComponentHtmlContent(result);
}
private static async Task<IHtmlContent> PrerenderedServerComponentAsync(HttpContext context, ServerComponentInvocationSequence invocationId, Type type, ParameterView parametersCollection)
{
if (parametersCollection.GetEnumerator().MoveNext())
{
throw new InvalidOperationException("Prerendering server components with parameters is not supported.");
}
var serviceProvider = context.RequestServices;
var prerenderer = serviceProvider.GetRequiredService<StaticComponentRenderer>();
var invocationSerializer = serviceProvider.GetRequiredService<ServerComponentSerializer>();
var currentInvocation = invocationSerializer.SerializeInvocation(
invocationId,
type,
prerendered: true);
var result = await prerenderer.PrerenderComponentAsync(
parametersCollection,
context,
type);
return new ComponentHtmlContent(
invocationSerializer.GetPreamble(currentInvocation),
result,
invocationSerializer.GetEpilogue(currentInvocation));
}
private static IHtmlContent NonPrerenderedServerComponent(HttpContext context, ServerComponentInvocationSequence invocationId, Type type, ParameterView parametersCollection)
{
if (parametersCollection.GetEnumerator().MoveNext())
{
throw new InvalidOperationException("Server components with parameters are not supported.");
}
var serviceProvider = context.RequestServices;
var invocationSerializer = serviceProvider.GetRequiredService<ServerComponentSerializer>();
var currentInvocation = invocationSerializer.SerializeInvocation(invocationId, type, prerendered: false);
return new ComponentHtmlContent(invocationSerializer.GetPreamble(currentInvocation));
}
}
}

View File

@ -1,26 +1,30 @@
// 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 System.IO;
using System.Linq;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures
{
internal class ComponentHtmlContent : IHtmlContent
{
private readonly IEnumerable<string> _preamble;
private readonly IEnumerable<string> _componentResult;
private readonly IEnumerable<string> _epilogue;
public ComponentHtmlContent(IEnumerable<string> componentResult)
{
_componentResult = componentResult;
}
: this(Array.Empty<string>(), componentResult, Array.Empty<string>()) { }
public ComponentHtmlContent(IEnumerable<string> preamble, IEnumerable<string> componentResult, IEnumerable<string> epilogue) =>
(_preamble, _componentResult, _epilogue) = (preamble, componentResult, epilogue);
public void WriteTo(TextWriter writer, HtmlEncoder encoder)
{
foreach (var element in _componentResult)
foreach (var element in _preamble.Concat(_componentResult).Concat(_epilogue))
{
writer.Write(element);
}

View File

@ -1,11 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>ASP.NET Core MVC view rendering features. Contains common types used in most MVC applications as well as view rendering features such as view engines, views, view components, and HTML helpers.
Commonly used types:
Microsoft.AspNetCore.Mvc.Controller
Microsoft.AspNetCore.Mvc.ValidateAntiForgeryTokenAttribute
Microsoft.AspNetCore.Mvc.ViewComponent</Description>
<Description>
ASP.NET Core MVC view rendering features. Contains common types used in most MVC applications as well as view rendering features such as view engines, views, view components, and HTML helpers.
Commonly used types:
Microsoft.AspNetCore.Mvc.Controller
Microsoft.AspNetCore.Mvc.ValidateAntiForgeryTokenAttribute
Microsoft.AspNetCore.Mvc.ViewComponent
</Description>
<TargetFramework>netcoreapp3.0</TargetFramework>
<NoWarn>$(NoWarn);CS1591</NoWarn>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
@ -17,6 +19,7 @@ Microsoft.AspNetCore.Mvc.ViewComponent</Description>
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Mvc.Core" />
<Reference Include="Microsoft.AspNetCore.Mvc.DataAnnotations" />
<Reference Include="Microsoft.AspNetCore.DataProtection.Extensions" />
<Reference Include="Microsoft.AspNetCore.Antiforgery" />
<Reference Include="Microsoft.AspNetCore.Diagnostics.Abstractions" />
@ -26,4 +29,10 @@ Microsoft.AspNetCore.Mvc.ViewComponent</Description>
<Reference Include="Microsoft.AspNetCore.Components.Web" />
</ItemGroup>
<ItemGroup>
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponentSerializationSettings.cs" />
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponentMarker.cs" />
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponent.cs" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,34 @@
// 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.Mvc.Rendering
{
/// <summary>
/// Describes the render mode of the component.
/// </summary>
/// <remarks>
/// The rendering mode determines how the component gets rendered on the page. It configures whether the component
/// is prerendered into the page or not and whether it simply renders static HTML on the page or if it includes the necessary
/// information to bootstrap a Blazor application from the user agent.
/// </remarks>
public enum RenderMode
{
/// <summary>
/// Renders the component into static HTML.
/// </summary>
Static = 1,
/// <summary>
/// Renders a marker for a Blazor server-side application. This doesn't include any output from the component.
/// When the user-agent starts, it uses this marker to bootstrap a blazor application.
/// </summary>
Server = 2,
/// <summary>
/// Renders the component into static HTML and includes a marker for a Blazor server-side application.
/// When the user-agent starts, it uses this marker to bootstrap a blazor application.
/// </summary>
ServerPrerendered = 3,
}
}

View File

@ -2,6 +2,7 @@
// 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 System.IO;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ViewEngines;
@ -16,6 +17,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
{
private FormContext _formContext;
private DynamicViewData _viewBag;
private Dictionary<object, object> _items;
/// <summary>
/// Creates an empty <see cref="ViewContext"/>.
@ -129,12 +131,14 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
Html5DateRenderingMode = viewContext.Html5DateRenderingMode;
ValidationSummaryMessageElement = viewContext.ValidationSummaryMessageElement;
ValidationMessageElement = viewContext.ValidationMessageElement;
ExecutingFilePath = viewContext.ExecutingFilePath;
View = view;
ViewData = viewData;
TempData = viewContext.TempData;
Writer = writer;
// The dictionary needs to be initialized at this point so that child viewcontexts share the same underlying storage;
_items = viewContext.Items;
}
/// <summary>
@ -225,6 +229,11 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
/// </remarks>
public string ExecutingFilePath { get; set; }
/// <summary>
/// Gets a key/value collection that can be used to share data within the scope of this view execution.
/// </summary>
internal Dictionary<object, object> Items => _items ??= new Dictionary<object, object>();
public FormContext GetFormContextForClientValidation()
{
return ClientValidationEnabled ? FormContext : null;

View File

@ -0,0 +1,26 @@
// 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.Security.Cryptography;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures
{
// Used to produce a monotonically increasing sequence starting at 0 that is unique for the scope of the top-level page/view/component being rendered.
internal class ServerComponentInvocationSequence
{
private int _sequence;
public ServerComponentInvocationSequence()
{
Span<byte> bytes = stackalloc byte[16];
RandomNumberGenerator.Fill(bytes);
Value = new Guid(bytes);
_sequence = -1;
}
public Guid Value { get; }
public int Next() => ++_sequence;
}
}

View File

@ -0,0 +1,90 @@
// 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 System.Text.Json;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.DataProtection;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures
{
// See the details of the component serialization protocol in ServerComponentDeserializer.cs on the Components solution.
internal class ServerComponentSerializer
{
private readonly ITimeLimitedDataProtector _dataProtector;
public ServerComponentSerializer(IDataProtectionProvider dataProtectionProvider) =>
_dataProtector = dataProtectionProvider
.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
.ToTimeLimitedDataProtector();
public ServerComponentMarker SerializeInvocation(ServerComponentInvocationSequence invocationId, Type type, bool prerendered)
{
var (sequence, serverComponent) = CreateSerializedServerComponent(invocationId, type);
return prerendered ? ServerComponentMarker.Prerendered(sequence, serverComponent) : ServerComponentMarker.NonPrerendered(sequence, serverComponent);
}
private (int sequence, string payload) CreateSerializedServerComponent(
ServerComponentInvocationSequence invocationId,
Type rootComponent)
{
var sequence = invocationId.Next();
var serverComponent = new ServerComponent(
sequence,
rootComponent.Assembly.GetName().Name,
rootComponent.FullName,
invocationId.Value);
var serializedServerComponent = JsonSerializer.Serialize(serverComponent, ServerComponentSerializationSettings.JsonSerializationOptions);
return (serverComponent.Sequence, _dataProtector.Protect(serializedServerComponent, ServerComponentSerializationSettings.DataExpiration));
}
internal IEnumerable<string> GetPreamble(ServerComponentMarker record)
{
var serializedStartRecord = JsonSerializer.Serialize(
record,
ServerComponentSerializationSettings.JsonSerializationOptions);
if (record.PrerenderId != null)
{
return PrerenderedStart(serializedStartRecord);
}
else
{
return NonPrerenderedSequence(serializedStartRecord);
}
static IEnumerable<string> PrerenderedStart(string startRecord)
{
yield return "<!--Blazor:";
yield return startRecord;
yield return "-->";
}
static IEnumerable<string> NonPrerenderedSequence(string record)
{
yield return "<!--Blazor:";
yield return record;
yield return "-->";
}
}
internal IEnumerable<string> GetEpilogue(ServerComponentMarker record)
{
var serializedStartRecord = JsonSerializer.Serialize(
record.GetEndRecord(),
ServerComponentSerializationSettings.JsonSerializationOptions);
return PrerenderEnd(serializedStartRecord);
static IEnumerable<string> PrerenderEnd(string endRecord)
{
yield return "<!--Blazor:";
yield return endRecord;
yield return "-->";
}
}
}
}

View File

@ -4,9 +4,12 @@
using System;
using System.IO;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc.Rendering;
@ -23,6 +26,11 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
{
public class HtmlHelperComponentExtensionsTests
{
private const string PrerenderedServerComponentPattern = "^<!--Blazor:(?<preamble>.*?)-->(?<content>.+?)<!--Blazor:(?<epilogue>.*?)-->$";
private const string ServerComponentPattern = "^<!--Blazor:(.*?)-->$";
private static readonly IDataProtectionProvider _dataprotectorProvider = new EphemeralDataProtectionProvider();
[Fact]
public async Task CanRender_ParameterlessComponent()
{
@ -31,7 +39,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
var writer = new StringWriter();
// Act
var result = await helper.RenderComponentAsync<TestComponent>();
var result = await helper.RenderComponentAsync<TestComponent>(RenderMode.Static);
result.WriteTo(writer, HtmlEncoder.Default);
var content = writer.ToString();
@ -39,6 +47,126 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
Assert.Equal("<h1>Hello world!</h1>", content);
}
[Fact]
public async Task CanRender_ParameterlessComponent_ServerMode()
{
// Arrange
var helper = CreateHelper();
var writer = new StringWriter();
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
.ToTimeLimitedDataProtector();
// Act
var result = await helper.RenderComponentAsync<TestComponent>(RenderMode.Server);
result.WriteTo(writer, HtmlEncoder.Default);
var content = writer.ToString();
var match = Regex.Match(content, ServerComponentPattern);
// Assert
Assert.True(match.Success);
var marker = JsonSerializer.Deserialize<ServerComponentMarker>(match.Groups[1].Value, ServerComponentSerializationSettings.JsonSerializationOptions);
Assert.Equal(0, marker.Sequence);
Assert.Null(marker.PrerenderId);
Assert.NotNull(marker.Descriptor);
Assert.Equal("server", marker.Type);
var unprotectedServerComponent = protector.Unprotect(marker.Descriptor);
var serverComponent = JsonSerializer.Deserialize<ServerComponent>(unprotectedServerComponent, ServerComponentSerializationSettings.JsonSerializationOptions);
Assert.Equal(0, serverComponent.Sequence);
Assert.Equal(typeof(TestComponent).Assembly.GetName().Name, serverComponent.AssemblyName);
Assert.Equal(typeof(TestComponent).FullName, serverComponent.TypeName);
Assert.NotEqual(Guid.Empty, serverComponent.InvocationId);
}
[Fact]
public async Task CanPrerender_ParameterlessComponent_ServerMode()
{
// Arrange
var helper = CreateHelper();
var writer = new StringWriter();
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
.ToTimeLimitedDataProtector();
// Act
var result = await helper.RenderComponentAsync<TestComponent>(RenderMode.ServerPrerendered);
result.WriteTo(writer, HtmlEncoder.Default);
var content = writer.ToString();
var match = Regex.Match(content, PrerenderedServerComponentPattern, RegexOptions.Multiline);
// Assert
Assert.True(match.Success);
var preamble = match.Groups["preamble"].Value;
var preambleMarker = JsonSerializer.Deserialize<ServerComponentMarker>(preamble, ServerComponentSerializationSettings.JsonSerializationOptions);
Assert.Equal(0, preambleMarker.Sequence);
Assert.NotNull(preambleMarker.PrerenderId);
Assert.NotNull(preambleMarker.Descriptor);
Assert.Equal("server", preambleMarker.Type);
var unprotectedServerComponent = protector.Unprotect(preambleMarker.Descriptor);
var serverComponent = JsonSerializer.Deserialize<ServerComponent>(unprotectedServerComponent, ServerComponentSerializationSettings.JsonSerializationOptions);
Assert.NotEqual(default, serverComponent);
Assert.Equal(0, serverComponent.Sequence);
Assert.Equal(typeof(TestComponent).Assembly.GetName().Name, serverComponent.AssemblyName);
Assert.Equal(typeof(TestComponent).FullName, serverComponent.TypeName);
Assert.NotEqual(Guid.Empty, serverComponent.InvocationId);
var prerenderedContent = match.Groups["content"].Value;
Assert.Equal("<h1>Hello world!</h1>", prerenderedContent);
var epilogue = match.Groups["epilogue"].Value;
var epilogueMarker = JsonSerializer.Deserialize<ServerComponentMarker>(epilogue, ServerComponentSerializationSettings.JsonSerializationOptions);
Assert.Equal(preambleMarker.PrerenderId, epilogueMarker.PrerenderId);
Assert.Null(epilogueMarker.Sequence);
Assert.Null(epilogueMarker.Descriptor);
Assert.Null(epilogueMarker.Type);
}
[Fact]
public async Task CanRenderMultipleServerComponents()
{
// Arrange
var helper = CreateHelper();
var firstWriter = new StringWriter();
var secondWriter = new StringWriter();
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
.ToTimeLimitedDataProtector();
// Act
var firstResult = await helper.RenderComponentAsync<TestComponent>(RenderMode.ServerPrerendered);
firstResult.WriteTo(firstWriter, HtmlEncoder.Default);
var firstComponent = firstWriter.ToString();
var firstMatch = Regex.Match(firstComponent, PrerenderedServerComponentPattern, RegexOptions.Multiline);
var secondResult = await helper.RenderComponentAsync<TestComponent>(RenderMode.Server);
secondResult.WriteTo(secondWriter, HtmlEncoder.Default);
var secondComponent = secondWriter.ToString();
var secondMatch = Regex.Match(secondComponent, ServerComponentPattern);
// Assert
Assert.True(firstMatch.Success);
var preamble = firstMatch.Groups["preamble"].Value;
var preambleMarker = JsonSerializer.Deserialize<ServerComponentMarker>(preamble, ServerComponentSerializationSettings.JsonSerializationOptions);
Assert.Equal(0, preambleMarker.Sequence);
Assert.NotNull(preambleMarker.Descriptor);
var unprotectedFirstServerComponent = protector.Unprotect(preambleMarker.Descriptor);
var firstServerComponent = JsonSerializer.Deserialize<ServerComponent>(unprotectedFirstServerComponent, ServerComponentSerializationSettings.JsonSerializationOptions);
Assert.Equal(0, firstServerComponent.Sequence);
Assert.NotEqual(Guid.Empty, firstServerComponent.InvocationId);
Assert.True(secondMatch.Success);
var marker = secondMatch.Groups[1].Value;
var markerMarker = JsonSerializer.Deserialize<ServerComponentMarker>(marker, ServerComponentSerializationSettings.JsonSerializationOptions);
Assert.Equal(1, markerMarker.Sequence);
Assert.NotNull(markerMarker.Descriptor);
var unprotectedSecondServerComponent = protector.Unprotect(markerMarker.Descriptor);
var secondServerComponent = JsonSerializer.Deserialize<ServerComponent>(unprotectedSecondServerComponent, ServerComponentSerializationSettings.JsonSerializationOptions);
Assert.Equal(1, secondServerComponent.Sequence);
Assert.Equal(firstServerComponent.InvocationId, secondServerComponent.InvocationId);
}
[Fact]
public async Task CanRender_ComponentWithParametersObject()
{
@ -47,10 +175,12 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
var writer = new StringWriter();
// Act
var result = await helper.RenderComponentAsync<GreetingComponent>(new
{
Name = "Steve"
});
var result = await helper.RenderComponentAsync<GreetingComponent>(
RenderMode.Static,
new
{
Name = "Steve"
});
result.WriteTo(writer, HtmlEncoder.Default);
var content = writer.ToString();
@ -58,6 +188,44 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
Assert.Equal("<p>Hello Steve!</p>", content);
}
[Theory]
[InlineData(RenderMode.Server, "Server components with parameters are not supported.")]
[InlineData(RenderMode.ServerPrerendered, "Prerendering server components with parameters is not supported.")]
public async Task ComponentWithParametersObject_ThrowsInvalidOperationExceptionForServerRenderModes(
RenderMode renderMode,
string expectedMessage)
{
// Arrange
var helper = CreateHelper();
var writer = new StringWriter();
// Act & Assert
var result = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderComponentAsync<GreetingComponent>(
renderMode,
new
{
Name = "Steve"
}));
Assert.Equal(expectedMessage, result.Message);
}
[Fact]
public async Task ComponentWithInvalidRenderMode_Throws()
{
// Arrange
var helper = CreateHelper();
var writer = new StringWriter();
// Act & Assert
var result = await Assert.ThrowsAsync<ArgumentException>(() => helper.RenderComponentAsync<GreetingComponent>(
default,
new
{
Name = "Steve"
}));
Assert.Equal("renderMode", result.ParamName);
}
[Fact]
public async Task RenderComponent_DoesNotInvokeOnAfterRenderInComponent()
{
@ -67,10 +235,13 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
// Act
var state = new OnAfterRenderState();
var result = await helper.RenderComponentAsync<OnAfterRenderComponent>(new
{
State = state
});
var result = await helper.RenderComponentAsync<OnAfterRenderComponent>(
RenderMode.Static,
new
{
State = state
});
result.WriteTo(writer, HtmlEncoder.Default);
// Assert
@ -85,10 +256,12 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
var helper = CreateHelper();
// Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderComponentAsync<ExceptionComponent>(new
{
IsAsync = false
}));
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderComponentAsync<ExceptionComponent>(
RenderMode.Static,
new
{
IsAsync = false
}));
// Assert
Assert.Equal("Threw an exception synchronously", exception.Message);
@ -101,10 +274,12 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
var helper = CreateHelper();
// Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderComponentAsync<ExceptionComponent>(new
{
IsAsync = true
}));
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderComponentAsync<ExceptionComponent>(
RenderMode.Static,
new
{
IsAsync = true
}));
// Assert
Assert.Equal("Threw an exception asynchronously", exception.Message);
@ -117,10 +292,13 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
var helper = CreateHelper();
// Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderComponentAsync<ExceptionComponent>(new
{
JsInterop = true
}));
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderComponentAsync<ExceptionComponent>(
RenderMode.Static,
new
{
JsInterop = true
}
));
// Assert
Assert.Equal("JavaScript interop calls cannot be issued during server-side prerendering, " +
@ -146,10 +324,12 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
var writer = new StringWriter();
// Act
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderComponentAsync<RedirectComponent>(new
{
RedirectUri = "http://localhost/redirect"
}));
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderComponentAsync<RedirectComponent>(
RenderMode.Static,
new
{
RedirectUri = "http://localhost/redirect"
}));
Assert.Equal("A navigation command was attempted during prerendering after the server already started sending the response. " +
"Navigation commands can not be issued during server-side prerendering after the response from the server has started. Applications must buffer the" +
@ -170,10 +350,12 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
var helper = CreateHelper(ctx);
// Act
await helper.RenderComponentAsync<RedirectComponent>(new
{
RedirectUri = "http://localhost/redirect"
});
await helper.RenderComponentAsync<RedirectComponent>(
RenderMode.Static,
new
{
RedirectUri = "http://localhost/redirect"
});
// Assert
Assert.Equal(302, ctx.Response.StatusCode);
@ -230,7 +412,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
</table>";
// Act
var result = await helper.RenderComponentAsync<AsyncComponent>();
var result = await helper.RenderComponentAsync<AsyncComponent>(RenderMode.Static);
result.WriteTo(writer, HtmlEncoder.Default);
var content = writer.ToString();
@ -242,6 +424,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
{
var services = new ServiceCollection();
services.AddSingleton(HtmlEncoder.Default);
services.AddSingleton<ServerComponentSerializer>();
services.AddSingleton(_dataprotectorProvider);
services.AddSingleton<IJSRuntime, UnsupportedJavaScriptRuntime>();
services.AddSingleton<NavigationManager, HttpNavigationManager>();
services.AddSingleton<StaticComponentRenderer>();

View File

@ -15,7 +15,7 @@
<link href="css/site.css" rel="stylesheet" />
</head>
<body>
<app>@(await Html.RenderComponentAsync<MvcSandbox.Components.App>())</app>
<app>@(await Html.RenderComponentAsync<MvcSandbox.Components.App>(RenderMode.Static))</app>
<script src="_framework/components.server.js"></script>
</body>

View File

@ -69,7 +69,7 @@ namespace MvcSandbox
builder.MapControllers();
builder.MapRazorPages();
builder.MapBlazorHub<MvcSandbox.Components.App>("app");
builder.MapBlazorHub();
builder.MapFallbackToPage("/Components");
});
}

View File

@ -1,8 +1,8 @@
@using BasicWebSite.RazorComponents;
<h1>Razor components</h1>
<div id="Greetings">@(await Html.RenderComponentAsync<Greetings>())</div>
<div id="Greetings">@(await Html.RenderComponentAsync<Greetings>(RenderMode.Static))</div>
<div id="FetchData">@(await Html.RenderComponentAsync<FetchData>(new { StartDate = new DateTime(2019, 01, 15) }))</div>
<div id="FetchData">@(await Html.RenderComponentAsync<FetchData>(RenderMode.Static, new { StartDate = new DateTime(2019, 01, 15) }))</div>
<div id="Routing">@(await Html.RenderComponentAsync<RouterContainer>())</div>
<div id="Routing">@(await Html.RenderComponentAsync<RouterContainer>(RenderMode.Static))</div>

View File

@ -1,3 +1,3 @@
@using BasicWebSite.RazorComponents;
<h1>Navigation components</h1>
<div id="Navigation">@(await Html.RenderComponentAsync<NavigationComponent>())</div>
<div id="Navigation">@(await Html.RenderComponentAsync<NavigationComponent>(RenderMode.Static))</div>

View File

@ -15,7 +15,7 @@
<body>
<app>
@* Remove the following line of code to disable prerendering *@
@(await Html.RenderComponentAsync<App>())
@(await Html.RenderComponentAsync<App>(RenderMode.ServerPrerendered))
</app>
<script src="_framework/blazor.server.js"></script>

View File

@ -170,7 +170,7 @@ namespace BlazorServerWeb_CSharp
#if (OrganizationalAuth || IndividualAuth)
endpoints.MapControllers();
#endif
endpoints.MapBlazorHub<App>(selector: "app");
endpoints.MapBlazorHub();
endpoints.MapFallbackToPage("/_Host");
});
}

View File

@ -0,0 +1,32 @@
// 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;
namespace Microsoft.AspNetCore.Components
{
// The DTO that we data-protect and include into any
// generated component marker and that allows the client
// to bootstrap a blazor server-side application.
internal struct ServerComponent
{
public ServerComponent(
int sequence,
string assemblyName,
string typeName,
Guid invocationId) =>
(Sequence, AssemblyName, TypeName, InvocationId) = (sequence, assemblyName, typeName, invocationId);
// The order in which this component was rendered
public int Sequence { get; set; }
// The assembly name for the rendered component.
public string AssemblyName { get; set; }
// The type name of the component.
public string TypeName { get; set; }
// An id that uniquely identifies all components generated as part of a single HTTP response.
public Guid InvocationId { get; set; }
}
}

View File

@ -0,0 +1,60 @@
// 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;
namespace Microsoft.AspNetCore.Components
{
// Represents the serialized invocation to a component.
// We serialize this marker into a comment in the generated
// HTML.
internal struct ServerComponentMarker
{
public const string ServerMarkerType = "server";
private ServerComponentMarker(string type, string descriptor, int? sequence, string prerenderId) : this()
{
Type = type;
PrerenderId = prerenderId;
Descriptor = descriptor;
Sequence = sequence;
}
// The order in which this component was rendered/produced
// on the server. It matches the number on the descriptor
// and is used to prevent an infinite amount of components
// from being rendered from the client-side.
public int? Sequence { get; set; }
// The marker type. Right now "server" is the only valid value.
public string Type { get; set; }
// A string to allow the clients to differentiate between prerendered
// and non prerendered components and to uniquely identify start and end
// markers in prererendered components.
public string PrerenderId { get; set; }
// A data-protected payload that allows the server to validate the legitimacy
// of the invocation.
public string Descriptor { get; set; }
// Creates a marker for a prerendered component.
public static ServerComponentMarker Prerendered(int sequence, string descriptor) =>
new ServerComponentMarker(ServerMarkerType, descriptor, sequence, Guid.NewGuid().ToString("N"));
// Creates a marker for a non prerendered component
public static ServerComponentMarker NonPrerendered(int sequence, string descriptor) =>
new ServerComponentMarker(ServerMarkerType, descriptor, sequence, null);
// Creates the end marker for a prerendered component.
public ServerComponentMarker GetEndRecord()
{
if (PrerenderId == null)
{
throw new InvalidOperationException("Can't get an end record for non-prerendered components.");
}
return new ServerComponentMarker(null, null, null, PrerenderId);
}
}
}

View File

@ -0,0 +1,26 @@
// 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.Text.Json;
namespace Microsoft.AspNetCore.Components
{
internal static class ServerComponentSerializationSettings
{
public const string DataProtectionProviderPurpose = "Microsoft.AspNetCore.Components.ComponentDescriptorSerializer,V1";
public static readonly JsonSerializerOptions JsonSerializationOptions =
new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
PropertyNameCaseInsensitive = true,
IgnoreNullValues = true
};
// This setting is not configurable, but realistically we don't expect an app to take more than 30 seconds from when
// it got rendrered to when the circuit got started, and having an expiration on the serialized server-components helps
// prevent old payloads from being replayed.
public static readonly TimeSpan DataExpiration = TimeSpan.FromMinutes(5);
}
}

View File

@ -58,6 +58,13 @@ namespace Microsoft.AspNetCore.E2ETesting
}
public virtual async Task InitializeAsync(string isolationContext)
{
await InitializeBrowser(isolationContext);
InitializeAsyncCore();
}
protected async Task InitializeBrowser(string isolationContext)
{
await _semaphore.WaitAsync(TimeSpan.FromMinutes(30));
_semaphoreHeld = true;
@ -67,8 +74,6 @@ namespace Microsoft.AspNetCore.E2ETesting
_logs.Value = logs;
Browser = browser;
InitializeAsyncCore();
}
protected virtual void InitializeAsyncCore()