[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:
parent
8283e6ac2b
commit
74b801506b
|
|
@ -13,7 +13,7 @@ namespace Microsoft.AspNetCore.Analyzers.TestFiles.CompilationFeatureDetectorTes
|
|||
|
||||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
endpoints.MapBlazorHub<App>("app");
|
||||
endpoints.MapBlazorHub();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -37,7 +37,6 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
|||
serviceScope.ServiceProvider ?? Mock.Of<IServiceProvider>(),
|
||||
NullLoggerFactory.Instance,
|
||||
new CircuitOptions(),
|
||||
jsRuntime,
|
||||
clientProxy,
|
||||
NullLogger.Instance);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ namespace ComponentsApp.Server
|
|||
{
|
||||
endpoints.MapRazorPages();
|
||||
endpoints.MapControllers();
|
||||
endpoints.MapBlazorHub<ComponentsApp.App.App>("app");
|
||||
endpoints.MapBlazorHub();
|
||||
endpoints.MapFallbackToPage("/_Host");
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ namespace Ignitor
|
|||
}
|
||||
};
|
||||
|
||||
await client.ConnectAsync(uri, prerendered: true);
|
||||
await client.ConnectAsync(uri);
|
||||
await done.Task;
|
||||
|
||||
return 0;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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)) { }
|
||||
|
|
|
|||
|
|
@ -175,6 +175,9 @@ namespace Microsoft.Extensions.DependencyInjection
|
|||
|
||||
services.TryAddSingleton<IJsonHelper, SystemTextJsonHelper>();
|
||||
|
||||
// Component services for Blazor server-side interop
|
||||
services.TryAddSingleton<ServerComponentSerializer>();
|
||||
|
||||
//
|
||||
// View Components
|
||||
//
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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 "-->";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>();
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ namespace MvcSandbox
|
|||
|
||||
builder.MapControllers();
|
||||
builder.MapRazorPages();
|
||||
builder.MapBlazorHub<MvcSandbox.Components.App>("app");
|
||||
builder.MapBlazorHub();
|
||||
builder.MapFallbackToPage("/Components");
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -170,7 +170,7 @@ namespace BlazorServerWeb_CSharp
|
|||
#if (OrganizationalAuth || IndividualAuth)
|
||||
endpoints.MapControllers();
|
||||
#endif
|
||||
endpoints.MapBlazorHub<App>(selector: "app");
|
||||
endpoints.MapBlazorHub();
|
||||
endpoints.MapFallbackToPage("/_Host");
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Reference in New Issue