[Components] Prerrendering startup experience (#7770)

[Components] Prerrendering startup experience
* Introduces an IComponentPrerrenderer to handle Prerrendering
  * MVC registers a basic static prerrrenderer.
  * Components registers a more feature complete prerrender that will
    handle reconnection to the original circuit after prerrendering in
    the future to allow for prerrendered interactive components.
* Removes UseRazorComponents
  * Removes the SPA fallback in favor of a catch all route in
    Index.cshtml
  * Moves the framework files to be served by the default StaticFiles
    middleware in the pipeline by way of plugging specific providers
    through options.
  * Lifts UseSignalR(r => r.MapHub<ComponentHub>()) into startup and
    replaces it with a shorthand for MapHub using endpoint routing.
  * Adds extension methods to map components to selectors for a given
    hub.
* Updates the razor component templates to include prerendering and use a razor page as the entry 
   point.
This commit is contained in:
Javier Calvarro Nelson 2019-02-21 16:26:44 -08:00 committed by Artak
parent ea97934127
commit 2d9cba86fd
50 changed files with 1073 additions and 445 deletions

View File

@ -0,0 +1,75 @@
// 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="IEndpointConventionBuilder"/>.
/// </summary>
public static class ComponentEndpointConventionBuilderExtensions
{
/// <summary>
/// Adds <typeparamref name="TComponent"/> to the list of components registered with this <see cref="ComponentHub"/> instance.
/// </summary>
/// <typeparam name="TComponent">The component type.</typeparam>
/// <param name="builder">The <see cref="IEndpointConventionBuilder"/>.</param>
/// <param name="selector">A CSS selector that identifies the DOM element into which the <typeparamref name="TComponent"/> will be placed.</param>
/// <returns>The <paramref name="builder"/>.</returns>
public static IEndpointConventionBuilder AddComponent<TComponent>(this IEndpointConventionBuilder builder, string selector)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
if (selector == null)
{
throw new ArgumentNullException(nameof(selector));
}
return AddComponent(builder, typeof(TComponent), selector);
}
/// <summary>
/// Adds <paramref name="componentType"/> to the list of components registered with this <see cref="ComponentHub"/> instance.
/// The selector will default to the component name in lowercase.
/// </summary>
/// <param name="builder">The <see cref="IEndpointConventionBuilder"/>.</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 IEndpointConventionBuilder AddComponent(this IEndpointConventionBuilder builder, Type componentType, string selector)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
if (componentType == null)
{
throw new ArgumentNullException(nameof(componentType));
}
if (selector == null)
{
throw new ArgumentNullException(nameof(selector));
}
builder.Add(endpointBuilder => AddComponent(endpointBuilder.Metadata, componentType, selector));
return builder;
}
private static void AddComponent(IList<object> metadata, Type type, string selector)
{
metadata.Add(new ComponentDescriptor
{
ComponentType = type,
Selector = selector
});
}
}
}

View File

@ -0,0 +1,110 @@
// 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 Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Routing;
namespace Microsoft.AspNetCore.Builder
{
/// <summary>
/// Extensions for <see cref="IEndpointRouteBuilder"/>.
/// </summary>
public static class ComponentEndpointRouteBuilderExtensions
{
/// <summary>
/// Maps the SignalR <see cref="ComponentHub"/> 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 <see cref="ComponentHub"/>.</typeparam>
/// <param name="routes">The <see cref="RouteBuilder"/>.</param>
/// <param name="selector">The selector for the <typeparamref name="TComponent"/>.</param>
/// <returns>The <see cref="IEndpointConventionBuilder"/>.</returns>
public static IEndpointConventionBuilder MapComponentHub<TComponent>(
this IEndpointRouteBuilder routes,
string selector)
{
if (routes == null)
{
throw new ArgumentNullException(nameof(routes));
}
if (selector == null)
{
throw new ArgumentNullException(nameof(selector));
}
return routes.MapComponentHub(typeof(TComponent), selector, ComponentHub.DefaultPath);
}
/// <summary>
/// Maps the SignalR <see cref="ComponentHub"/> 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 <see cref="ComponentHub"/>.</typeparam>
/// <param name="routes">The <see cref="RouteBuilder"/>.</param>
/// <param name="selector">The selector for the <typeparamref name="TComponent"/>.</param>
/// <param name="path">The path to map to which the <see cref="ComponentHub"/> will be mapped.</param>
/// <returns>The <see cref="IEndpointConventionBuilder"/>.</returns>
public static IEndpointConventionBuilder MapComponentHub<TComponent>(
this IEndpointRouteBuilder routes,
string selector,
string path)
{
if (routes == null)
{
throw new ArgumentNullException(nameof(routes));
}
if (path == null)
{
throw new ArgumentNullException(nameof(path));
}
if (selector == null)
{
throw new ArgumentNullException(nameof(selector));
}
return routes.MapComponentHub(typeof(TComponent), selector, path);
}
/// <summary>
/// Maps the SignalR <see cref="ComponentHub"/> 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="routes">The <see cref="RouteBuilder"/>.</param>
/// <param name="componentType">The first <see cref="IComponent"/> associated with this <see cref="ComponentHub"/>.</param>
/// <param name="selector">The selector for the <paramref name="componentType"/>.</param>
/// <param name="path">The path to map to which the <see cref="ComponentHub"/> will be mapped.</param>
/// <returns>The <see cref="IEndpointConventionBuilder"/>.</returns>
public static IEndpointConventionBuilder MapComponentHub(
this IEndpointRouteBuilder routes,
Type componentType,
string selector,
string path)
{
if (routes == null)
{
throw new ArgumentNullException(nameof(routes));
}
if (path == null)
{
throw new ArgumentNullException(nameof(path));
}
if (componentType == null)
{
throw new ArgumentNullException(nameof(componentType));
}
if (selector == null)
{
throw new ArgumentNullException(nameof(selector));
}
return routes.MapHub<ComponentHub>(path).AddComponent(componentType, selector);
}
}
}

View File

@ -1,91 +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 Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Components.Server.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.FileProviders;
namespace Microsoft.AspNetCore.Builder
{
/// <summary>
/// Extension methods to configure an <see cref="IApplicationBuilder"/> for serving interactive components.
/// </summary>
public static class RazorComponentsApplicationBuilderExtensions
{
/// <summary>
/// Adds middleware for serving interactive Razor Components.
/// </summary>
/// <param name="builder">The <see cref="IApplicationBuilder"/>.</param>
/// <typeparam name="TStartup">A components app startup type.</typeparam>
/// <returns>The <see cref="IApplicationBuilder"/>.</returns>
public static IApplicationBuilder UseRazorComponents<TStartup>(
this IApplicationBuilder builder)
{
return UseRazorComponents<TStartup>(builder, null);
}
/// <summary>
/// Adds middleware for serving interactive Razor Components.
/// </summary>
/// <param name="builder">The <see cref="IApplicationBuilder"/>.</param>
/// <param name="configure">A callback that can be used to configure the middleware.</param>
/// <typeparam name="TStartup">A components app startup type.</typeparam>
/// <returns>The <see cref="IApplicationBuilder"/>.</returns>
public static IApplicationBuilder UseRazorComponents<TStartup>(
this IApplicationBuilder builder,
Action<RazorComponentsOptions> configure)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
var options = new RazorComponentsOptions();
configure?.Invoke(options);
// The use case for this flag is when developers want to add their own
// SignalR middleware, e.g., when using Azure SignalR. By default we
// add SignalR and BlazorHub automatically.
if (options.UseSignalRWithBlazorHub)
{
builder.UseSignalR(route => route.MapHub<ComponentsHub>(ComponentsHub.DefaultPath));
}
// Use embedded static content for /_framework
builder.Map("/_framework", frameworkBuilder =>
{
UseFrameworkFiles(frameworkBuilder);
});
// Use SPA fallback routing for anything else
builder.UseSpa(spa => { });
return builder;
}
private static void UseFrameworkFiles(IApplicationBuilder builder)
{
builder.UseStaticFiles(new StaticFileOptions
{
FileProvider = new ManifestEmbeddedFileProvider(
typeof(RazorComponentsApplicationBuilderExtensions).Assembly,
"frameworkFiles"),
OnPrepareResponse = BlazorApplicationBuilderExtensions.SetCacheHeaders
});
// TODO: Remove this
// This is needed temporarily only until we implement a proper version
// of library-embedded static resources for Razor Components apps.
builder.Map("/blazor.boot.json", bootJsonBuilder =>
{
bootJsonBuilder.Use(async (ctx, next) =>
{
ctx.Response.ContentType = "application/json";
await ctx.Response.WriteAsync(@"{ ""cssReferences"": [], ""jsReferences"": [] }");
});
});
}
}
}

View File

@ -1,23 +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 Microsoft.AspNetCore.Builder;
namespace Microsoft.AspNetCore.Components.Server.Builder
{
/// <summary>
/// Specifies options to configure <see cref="RazorComponentsApplicationBuilderExtensions.UseRazorComponents{TStartup}(IApplicationBuilder)"/>
/// </summary>
public class RazorComponentsOptions
{
/// <summary>
/// Gets or sets a flag to indicate whether to attach middleware for
/// communicating with interactive components via SignalR. Defaults
/// to true.
///
/// If the value is set to false, the application must manually add
/// SignalR middleware with <see cref="BlazorHub"/>.
/// </summary>
public bool UseSignalRWithBlazorHub { get; set; } = true;
}
}

View File

@ -1,37 +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.Builder;
namespace Microsoft.AspNetCore.Components.Hosting
{
internal class ServerSideComponentsApplicationBuilder : IComponentsApplicationBuilder
{
public ServerSideComponentsApplicationBuilder(IServiceProvider services)
{
Services = services;
Entries = new List<(Type componentType, string domElementSelector)>();
}
public List<(Type componentType, string domElementSelector)> Entries { get; }
public IServiceProvider Services { get; }
public void AddComponent(Type componentType, string domElementSelector)
{
if (componentType == null)
{
throw new ArgumentNullException(nameof(componentType));
}
if (domElementSelector == null)
{
throw new ArgumentNullException(nameof(domElementSelector));
}
Entries.Add((componentType, domElementSelector));
}
}
}

View File

@ -8,6 +8,10 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
{
internal abstract class CircuitFactory
{
public abstract CircuitHost CreateCircuitHost(HttpContext httpContext, IClientProxy client);
public abstract CircuitHost CreateCircuitHost(
HttpContext httpContext,
IClientProxy client,
string uriAbsolute,
string baseUriAbsolute);
}
}

View File

@ -2,12 +2,12 @@
// 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.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Browser;
using Microsoft.AspNetCore.Components.Browser.Rendering;
using Microsoft.AspNetCore.Components.Builder;
using Microsoft.AspNetCore.Components.Hosting;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.JSInterop;
@ -18,11 +18,10 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
{
private static readonly AsyncLocal<CircuitHost> _current = new AsyncLocal<CircuitHost>();
private readonly IServiceScope _scope;
private readonly IDispatcher _dispatcher;
private readonly CircuitHandler[] _circuitHandlers;
private bool _initialized;
private Action<IComponentsApplicationBuilder> _configure;
/// <summary>
/// Gets the current <see cref="Circuit"/>, if any.
/// </summary>
@ -53,17 +52,19 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
IClientProxy client,
RendererRegistry rendererRegistry,
RemoteRenderer renderer,
Action<IComponentsApplicationBuilder> configure,
IList<ComponentDescriptor> descriptors,
IDispatcher dispatcher,
IJSRuntime jsRuntime,
CircuitHandler[] circuitHandlers)
{
_scope = scope ?? throw new ArgumentNullException(nameof(scope));
Client = client ?? throw new ArgumentNullException(nameof(client));
_dispatcher = dispatcher;
Client = client;
RendererRegistry = rendererRegistry ?? throw new ArgumentNullException(nameof(rendererRegistry));
Descriptors = descriptors ?? throw new ArgumentNullException(nameof(descriptors));
Renderer = renderer ?? throw new ArgumentNullException(nameof(renderer));
_configure = configure ?? throw new ArgumentNullException(nameof(configure));
JSRuntime = jsRuntime ?? throw new ArgumentNullException(nameof(jsRuntime));
Services = scope.ServiceProvider;
Circuit = new Circuit(this);
@ -77,7 +78,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
public Circuit Circuit { get; }
public IClientProxy Client { get; }
public IClientProxy Client { get; set; }
public IJSRuntime JSRuntime { get; }
@ -85,21 +86,29 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
public RendererRegistry RendererRegistry { get; }
public IList<ComponentDescriptor> Descriptors { get; }
public IServiceProvider Services { get; }
public Task<IEnumerable<string>> PrerenderComponentAsync(Type componentType, ParameterCollection parameters)
{
return _dispatcher.InvokeAsync(async () =>
{
Renderer.StartPrerender();
var result = await Renderer.RenderComponentAsync(componentType, parameters);
return result;
});
}
public async Task InitializeAsync(CancellationToken cancellationToken)
{
await Renderer.InvokeAsync(async () =>
{
SetCurrentCircuitHost(this);
var builder = new ServerSideComponentsApplicationBuilder(Services);
_configure(builder);
for (var i = 0; i < builder.Entries.Count; i++)
for (var i = 0; i < Descriptors.Count; i++)
{
var (componentType, domElementSelector) = builder.Entries[i];
var (componentType, domElementSelector) = Descriptors[i];
await Renderer.AddComponentAsync(componentType, domElementSelector);
}

View File

@ -0,0 +1,58 @@
// 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.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
namespace Microsoft.AspNetCore.Components.Server.Circuits
{
internal class CircuitPrerenderer : IComponentPrerenderer
{
private readonly CircuitFactory _circuitFactory;
public CircuitPrerenderer(CircuitFactory circuitFactory)
{
_circuitFactory = circuitFactory;
}
public async Task<IEnumerable<string>> PrerenderComponentAsync(ComponentPrerenderingContext prerenderingContext)
{
var context = prerenderingContext.Context;
var circuitHost = _circuitFactory.CreateCircuitHost(
context,
client: null,
GetFullUri(context.Request),
GetFullBaseUri(context.Request));
// For right now we just do prerendering and dispose the circuit. In the future we will keep the circuit around and
// reconnect to it from the ComponentsHub.
try
{
return await circuitHost.PrerenderComponentAsync(
prerenderingContext.ComponentType,
prerenderingContext.Parameters);
}
finally
{
await circuitHost.DisposeAsync();
}
}
private string GetFullUri(HttpRequest request)
{
return UriHelper.BuildAbsolute(
request.Scheme,
request.Host,
request.PathBase,
request.Path,
request.QueryString);
}
private string GetFullBaseUri(HttpRequest request)
{
return UriHelper.BuildAbsolute(request.Scheme, request.Host, request.PathBase);
}
}
}

View File

@ -2,49 +2,61 @@
// 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.Text.Encodings.Web;
using Microsoft.AspNetCore.Components.Browser;
using Microsoft.AspNetCore.Components.Browser.Rendering;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.SignalR;
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 DefaultCircuitFactoryOptions _options;
private readonly ILoggerFactory _loggerFactory;
public DefaultCircuitFactory(
IServiceScopeFactory scopeFactory,
IOptions<DefaultCircuitFactoryOptions> options,
ILoggerFactory loggerFactory)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
_scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory));
_options = options.Value;
_loggerFactory = loggerFactory;
}
public override CircuitHost CreateCircuitHost(HttpContext httpContext, IClientProxy client)
public override CircuitHost CreateCircuitHost(
HttpContext httpContext,
IClientProxy client,
string uriAbsolute,
string baseUriAbsolute)
{
if (!_options.StartupActions.TryGetValue(httpContext.Request.Path, out var config))
{
var message = $"Could not find an ASP.NET Core Components startup action for request path '{httpContext.Request.Path}'.";
throw new InvalidOperationException(message);
}
var components = ResolveComponentMetadata(httpContext, client);
var scope = _scopeFactory.CreateScope();
var jsRuntime = new RemoteJSRuntime(client);
var encoder = scope.ServiceProvider.GetRequiredService<HtmlEncoder>();
var jsRuntime = (RemoteJSRuntime)scope.ServiceProvider.GetRequiredService<IJSRuntime>();
if (client != null)
{
jsRuntime.Initialize(client);
}
var uriHelper = (RemoteUriHelper)scope.ServiceProvider.GetRequiredService<IUriHelper>();
if (client != null)
{
uriHelper.Initialize(uriAbsolute, baseUriAbsolute, jsRuntime);
}
else
{
uriHelper.Initialize(uriAbsolute, baseUriAbsolute);
}
var rendererRegistry = new RendererRegistry();
var dispatcher = Renderer.CreateDefaultDispatcher();
var renderer = new RemoteRenderer(
@ -53,6 +65,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
jsRuntime,
client,
dispatcher,
encoder,
_loggerFactory.CreateLogger<RemoteRenderer>());
var circuitHandlers = scope.ServiceProvider.GetServices<CircuitHandler>()
@ -64,15 +77,43 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
client,
rendererRegistry,
renderer,
config,
components,
dispatcher,
jsRuntime,
circuitHandlers);
// Initialize per-circuit data that services need
(circuitHost.Services.GetRequiredService<IJSRuntimeAccessor>() as DefaultJSRuntimeAccessor).JSRuntime = jsRuntime;
// Initialize per - circuit data that services need
(circuitHost.Services.GetRequiredService<ICircuitAccessor>() as DefaultCircuitAccessor).Circuit = circuitHost.Circuit;
return circuitHost;
}
private static IList<ComponentDescriptor> ResolveComponentMetadata(HttpContext httpContext, IClientProxy client)
{
if (client == null)
{
// This is the prerendering case.
return Array.Empty<ComponentDescriptor>();
}
else
{
var endpointFeature = httpContext.Features.Get<IEndpointFeature>();
var endpoint = endpointFeature?.Endpoint;
if (endpoint == null)
{
throw new InvalidOperationException(
$"{nameof(ComponentHub)} doesn't have an associated endpoint. " +
"Use 'app.UseRouting(routes => routes.MapComponentHub<App>(\"app\"))' to register your hub.");
}
var componentsMetadata = endpoint.Metadata.OfType<ComponentDescriptor>().ToList();
if (componentsMetadata.Count == 0)
{
throw new InvalidOperationException("No component was registered with the component hub.");
}
return componentsMetadata;
}
}
}
}

View File

@ -1,18 +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.Builder;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Components.Server
{
internal class DefaultCircuitFactoryOptions
{
// During the DI configuration phase, we use Configure<DefaultCircuitFactoryOptions>(...)
// callbacks to build up this dictionary mapping paths to startup actions
internal Dictionary<PathString, Action<IComponentsApplicationBuilder>> StartupActions { get; }
= new Dictionary<PathString, Action<IComponentsApplicationBuilder>>();
}
}

View File

@ -1,12 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.JSInterop;
namespace Microsoft.AspNetCore.Components.Server.Circuits
{
internal class DefaultJSRuntimeAccessor : IJSRuntimeAccessor
{
public IJSRuntime JSRuntime { get; set; }
}
}

View File

@ -1,12 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.JSInterop;
namespace Microsoft.AspNetCore.Components.Server.Circuits
{
internal interface IJSRuntimeAccessor
{
IJSRuntime JSRuntime { get; }
}
}

View File

@ -9,15 +9,23 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
{
internal class RemoteJSRuntime : JSRuntimeBase
{
private readonly IClientProxy _clientProxy;
private IClientProxy _clientProxy;
public RemoteJSRuntime(IClientProxy clientProxy)
public RemoteJSRuntime()
{
}
internal void Initialize(IClientProxy clientProxy)
{
_clientProxy = clientProxy ?? throw new ArgumentNullException(nameof(clientProxy));
}
protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson)
{
if (_clientProxy == null)
{
throw new InvalidOperationException("The JavaScript runtime is not available during prerendering.");
}
_clientProxy.SendAsync("JS.BeginInvokeJS", asyncHandle, identifier, argsJson);
}
}

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Concurrent;
using System.Text.Encodings.Web;
using System.Threading;
using System.Threading.Tasks;
using MessagePack;
@ -15,20 +16,21 @@ using Microsoft.JSInterop;
namespace Microsoft.AspNetCore.Components.Browser.Rendering
{
internal class RemoteRenderer : Renderer
internal class RemoteRenderer : HtmlRenderer
{
// The purpose of the timeout is just to ensure server resources are released at some
// point if the client disconnects without sending back an ACK after a render
private const int TimeoutMilliseconds = 60 * 1000;
private readonly int _id;
private readonly IClientProxy _client;
private IClientProxy _client;
private readonly IJSRuntime _jsRuntime;
private readonly RendererRegistry _rendererRegistry;
private readonly ConcurrentDictionary<long, AutoCancelTaskCompletionSource<object>> _pendingRenders
= new ConcurrentDictionary<long, AutoCancelTaskCompletionSource<object>>();
private readonly ILogger _logger;
private long _nextRenderId = 1;
private bool _prerenderMode;
/// <summary>
/// Notifies when a rendering exception occured.
@ -49,8 +51,9 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
IJSRuntime jsRuntime,
IClientProxy client,
IDispatcher dispatcher,
HtmlEncoder encoder,
ILogger logger)
: base(serviceProvider, dispatcher)
: base(serviceProvider, encoder.Encode, dispatcher)
{
_rendererRegistry = rendererRegistry;
_jsRuntime = jsRuntime;
@ -97,6 +100,11 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
}
}
internal void StartPrerender()
{
_prerenderMode = true;
}
/// <inheritdoc />
protected override void Dispose(bool disposing)
{
@ -107,6 +115,14 @@ namespace Microsoft.AspNetCore.Components.Browser.Rendering
/// <inheritdoc />
protected override Task UpdateDisplayAsync(in RenderBatch batch)
{
if (_prerenderMode)
{
// Nothing to do in prerender mode for right now.
// In the future we will capture all the serialized render batches and
// resend them to the client upon the initial reconnect.
return Task.CompletedTask;
}
// Note that we have to capture the data as a byte[] synchronously here, because
// SignalR's SendAsync can wait an arbitrary duration before serializing the params.
// The RenderBatch buffer will get reused by subsequent renders, so we need to

View File

@ -14,15 +14,10 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
/// </summary>
public class RemoteUriHelper : UriHelperBase
{
private readonly IJSRuntime _jsRuntime;
private IJSRuntime _jsRuntime;
/// <summary>
/// Creates a new <see cref="RemoteUriHelper"/>.
/// </summary>
/// <param name="jsRuntime"></param>
public RemoteUriHelper(IJSRuntime jsRuntime)
public RemoteUriHelper()
{
_jsRuntime = jsRuntime;
}
/// <summary>
@ -30,18 +25,38 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
/// </summary>
/// <param name="uriAbsolute">The absolute URI of the current page.</param>
/// <param name="baseUriAbsolute">The absolute base URI of the current page.</param>
/// <param name="jsRuntime">The <see cref="IJSRuntime"/> to use for interoperability.</param>
public void Initialize(string uriAbsolute, string baseUriAbsolute)
{
SetAbsoluteBaseUri(baseUriAbsolute);
SetAbsoluteUri(uriAbsolute);
TriggerOnLocationChanged();
}
/// <summary>
/// Initializes the <see cref="RemoteUriHelper"/>.
/// </summary>
/// <param name="uriAbsolute">The absolute URI of the current page.</param>
/// <param name="baseUriAbsolute">The absolute base URI of the current page.</param>
/// <param name="jsRuntime">The <see cref="IJSRuntime"/> to use for interoperability.</param>
public void Initialize(string uriAbsolute, string baseUriAbsolute, IJSRuntime jsRuntime)
{
if (_jsRuntime != null)
{
throw new InvalidOperationException("JavaScript runtime already initialized.");
}
_jsRuntime = jsRuntime;
Initialize(uriAbsolute, baseUriAbsolute);
_jsRuntime.InvokeAsync<object>(
Interop.EnableNavigationInterception,
typeof(RemoteUriHelper).Assembly.GetName().Name,
nameof(NotifyLocationChanged));
Interop.EnableNavigationInterception,
typeof(RemoteUriHelper).Assembly.GetName().Name,
nameof(NotifyLocationChanged));
}
/// <summary>
/// For framework use only.
/// </summary>
@ -61,9 +76,12 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
uriHelper.TriggerOnLocationChanged();
}
/// <inheritdoc />
protected override void NavigateToCore(string uri, bool forceLoad)
{
if (_jsRuntime == null)
{
throw new InvalidOperationException("Navigation is not allowed during prerendering.");
}
_jsRuntime.InvokeAsync<object>(Interop.NavigateTo, uri, forceLoad);
}
}

View File

@ -15,7 +15,7 @@ namespace Microsoft.AspNetCore.Components.Server
/// <summary>
/// A SignalR hub that accepts connections to an ASP.NET Core Components application.
/// </summary>
public sealed class ComponentsHub : Hub
public sealed class ComponentHub : Hub
{
private static readonly object CircuitKey = new object();
private readonly CircuitFactory _circuitFactory;
@ -25,7 +25,7 @@ namespace Microsoft.AspNetCore.Components.Server
/// Intended for framework use only. Applications should not instantiate
/// this class directly.
/// </summary>
public ComponentsHub(IServiceProvider services, ILogger<ComponentsHub> logger)
public ComponentHub(IServiceProvider services, ILogger<ComponentHub> logger)
{
_circuitFactory = services.GetRequiredService<CircuitFactory>();
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
@ -58,11 +58,13 @@ namespace Microsoft.AspNetCore.Components.Server
/// </summary>
public async Task StartCircuit(string uriAbsolute, string baseUriAbsolute)
{
var circuitHost = _circuitFactory.CreateCircuitHost(Context.GetHttpContext(), Clients.Caller);
circuitHost.UnhandledException += CircuitHost_UnhandledException;
var circuitHost = _circuitFactory.CreateCircuitHost(
Context.GetHttpContext(),
Clients.Caller,
uriAbsolute,
baseUriAbsolute);
var uriHelper = (RemoteUriHelper)circuitHost.Services.GetRequiredService<IUriHelper>();
uriHelper.Initialize(uriAbsolute, baseUriAbsolute);
circuitHost.UnhandledException += CircuitHost_UnhandledException;
// If initialization fails, this will throw. The caller will fail if they try to call into any interop API.
await circuitHost.InitializeAsync(Context.ConnectionAborted);

View File

@ -0,0 +1,20 @@
// 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.Server
{
internal class ComponentDescriptor
{
public Type ComponentType { get; set; }
public string Selector { get; set; }
public void Deconstruct(out Type componentType, out string selector)
{
componentType = ComponentType;
selector = Selector;
}
}
}

View File

@ -0,0 +1,49 @@
// 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 Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Components.Server.Circuits;
using Microsoft.AspNetCore.Components.Services;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using Microsoft.JSInterop;
namespace Microsoft.Extensions.DependencyInjection
{
/// <summary>
/// Extension methods to configure an <see cref="IServiceCollection"/> for components.
/// </summary>
public static class ComponentServiceCollectionExtensions
{
/// <summary>
/// Adds Razor Component app services to the service collection.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/>.</param>
/// <returns>The <see cref="IServiceCollection"/>.</returns>
public static IServiceCollection AddRazorComponents(this IServiceCollection services)
{
services.AddSignalR().AddMessagePackProtocol();
// Here we add a bunch of services that don't vary in any way based on the
// user's configuration. So even if the user has multiple independent server-side
// 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.TryAddScoped(s => s.GetRequiredService<ICircuitAccessor>().Circuit);
services.TryAddScoped<ICircuitAccessor, DefaultCircuitAccessor>();
// We explicitly take over the prerendering and components services here.
// We can't have two separate component implementations coexisting at the
// same time, so when you register components (Circuits) it takes over
// all the abstractions.
services.AddScoped<IComponentPrerenderer, CircuitPrerenderer>();
// Standard razor component services implementations
services.AddScoped<IUriHelper, RemoteUriHelper>();
services.AddScoped<IJSRuntime, RemoteJSRuntime>();
return services;
}
}
}

View File

@ -0,0 +1,115 @@
// 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.IO;
using System.Text;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.Components.Server
{
internal class ConfigureStaticFilesOptions : IPostConfigureOptions<StaticFileOptions>
{
public ConfigureStaticFilesOptions(IWebHostEnvironment environment)
{
Environment = environment;
}
public IWebHostEnvironment Environment { get; }
public void PostConfigure(string name, StaticFileOptions options)
{
name = name ?? throw new ArgumentNullException(nameof(name));
options = options ?? throw new ArgumentNullException(nameof(options));
if (name != Options.DefaultName)
{
return;
}
// Basic initialization in case the options weren't initialized by any other component
options.ContentTypeProvider = options.ContentTypeProvider ?? new FileExtensionContentTypeProvider();
if (options.FileProvider == null && Environment.WebRootFileProvider == null)
{
throw new InvalidOperationException("Missing FileProvider.");
}
options.FileProvider = options.FileProvider ?? Environment.WebRootFileProvider;
var prepareResponse = options.OnPrepareResponse;
if (prepareResponse == null)
{
options.OnPrepareResponse = BlazorApplicationBuilderExtensions.SetCacheHeaders;
}
else
{
void PrepareResponse(StaticFileResponseContext context)
{
prepareResponse(context);
BlazorApplicationBuilderExtensions.SetCacheHeaders(context);
}
options.OnPrepareResponse = PrepareResponse;
}
// Add our provider
var provider = new ManifestEmbeddedFileProvider(typeof(ConfigureStaticFilesOptions).Assembly);
options.FileProvider = new CompositeFileProvider(provider, new ContentReferencesFileProvider(), options.FileProvider);
}
private class ContentReferencesFileProvider : IFileProvider
{
byte[] _data = Encoding.UTF8.GetBytes(@"{ ""cssReferences"": [], ""jsReferences"": [] }");
public IDirectoryContents GetDirectoryContents(string subpath)
{
return new NotFoundDirectoryContents();
}
public IFileInfo GetFileInfo(string subpath)
{
if (subpath.Equals("/_framework/blazor.boot.json", StringComparison.OrdinalIgnoreCase))
{
return new MemoryFileInfo(_data);
}
return new NotFoundFileInfo(subpath);
}
public IChangeToken Watch(string filter) => NullChangeToken.Singleton;
private class MemoryFileInfo : IFileInfo
{
private readonly byte[] _data;
public MemoryFileInfo(byte[] data)
{
_data = data;
}
public bool Exists => true;
public long Length => _data.Length;
public string PhysicalPath => "/_framework/blazor.boot.json";
public string Name => "blazor.boot.json";
public DateTimeOffset LastModified => DateTimeOffset.FromUnixTimeSeconds(0);
public bool IsDirectory => false;
public Stream CreateReadStream()
{
return new MemoryStream(_data, writable: false);
}
}
}
}
}

View File

@ -1,115 +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 Microsoft.AspNetCore.Components.Hosting;
using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Components.Server.Circuits;
using Microsoft.AspNetCore.Components.Services;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace Microsoft.Extensions.DependencyInjection
{
/// <summary>
/// Extension methods to configure an <see cref="IServiceCollection"/> for interactive components.
/// </summary>
public static class RazorComponentsServiceCollectionExtensions
{
/// <summary>
/// Adds Razor Component services to the service collection.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/>.</param>
/// <param name="startupType">A Razor Components project startup type.</param>
/// <returns>The <see cref="IServiceCollection"/>.</returns>
public static IServiceCollection AddRazorComponents(
this IServiceCollection services,
Type startupType)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
if (startupType == null)
{
throw new ArgumentNullException(nameof(startupType));
}
return AddRazorComponentsCore(services, startupType);
}
/// <summary>
/// Adds Razor Component app services to the service collection.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/>.</param>
/// <typeparam name="TStartup">A Components app startup type.</typeparam>
/// <returns>The <see cref="IServiceCollection"/>.</returns>
public static IServiceCollection AddRazorComponents<TStartup>(
this IServiceCollection services)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
return AddRazorComponentsCore(services, typeof(TStartup));
}
private static IServiceCollection AddRazorComponentsCore(
IServiceCollection services,
Type startupType)
{
AddStandardRazorComponentsServices(services);
if (startupType != null)
{
// Call TStartup's ConfigureServices method immediately
var startup = Activator.CreateInstance(startupType);
var wrapper = new ConventionBasedStartup(startup);
wrapper.ConfigureServices(services);
// Configure the circuit factory to call a startup action when each
// incoming connection is established. The startup action is "call
// TStartup's Configure method".
services.Configure<DefaultCircuitFactoryOptions>(circuitFactoryOptions =>
{
var endpoint = ComponentsHub.DefaultPath; // TODO: allow configuring this
if (circuitFactoryOptions.StartupActions.ContainsKey(endpoint))
{
throw new InvalidOperationException(
"Multiple Components app entries are configured to use " +
$"the same endpoint '{endpoint}'.");
}
circuitFactoryOptions.StartupActions.Add(endpoint, builder =>
{
wrapper.Configure(builder, builder.Services);
});
});
}
return services;
}
private static void AddStandardRazorComponentsServices(IServiceCollection services)
{
// Here we add a bunch of services that don't vary in any way based on the
// user's configuration. So even if the user has multiple independent server-side
// Components entrypoints, this lot is the same and repeated registrations are a no-op.
services.TryAddSingleton<CircuitFactory, DefaultCircuitFactory>();
services.TryAddScoped<ICircuitAccessor, DefaultCircuitAccessor>();
services.TryAddScoped(s => s.GetRequiredService<ICircuitAccessor>().Circuit);
services.TryAddScoped<IJSRuntimeAccessor, DefaultJSRuntimeAccessor>();
services.TryAddScoped(s => s.GetRequiredService<IJSRuntimeAccessor>().JSRuntime);
services.TryAddScoped<IUriHelper, RemoteUriHelper>();
// We've discussed with the SignalR team and believe it's OK to have repeated
// calls to AddSignalR (making the nonfirst ones no-ops). If we want to change
// this in the future, we could change AddComponents to be an extension
// method on ISignalRServerBuilder so the developer always has to chain it onto
// their own AddSignalR call. For now we're keeping it like this because it's
// simpler for developers in common cases.
services.AddSignalR().AddMessagePackProtocol();
}
}
}

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
@ -30,7 +30,7 @@
<ItemGroup Condition="'$(BuildNodeJS)' != 'false'">
<!-- We need .Browser.JS to build first so we can embed its .js output -->
<ProjectReference Include="..\..\Browser.JS\src\Microsoft.AspNetCore.Components.Browser.JS.npmproj" ReferenceOutputAssembly="false" />
<EmbeddedResource Include="..\..\Browser.JS\src\dist\components.server.js" LogicalName="frameworkFiles\%(Filename)%(Extension)" />
<EmbeddedResource Include="..\..\Browser.JS\src\dist\components.server.js" LogicalName="_framework\%(Filename)%(Extension)" />
</ItemGroup>
</Project>

View File

@ -2,10 +2,13 @@
// 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.Encodings.Web;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Browser;
using Microsoft.AspNetCore.Components.Browser.Rendering;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
@ -22,7 +25,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
{
// Arrange
var serviceScope = new Mock<IServiceScope>();
var remoteRenderer = GetRemoteRenderer();
var remoteRenderer = GetRemoteRenderer(Renderer.CreateDefaultDispatcher());
var circuitHost = GetCircuitHost(
serviceScope.Object,
remoteRenderer);
@ -130,8 +133,8 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
var clientProxy = Mock.Of<IClientProxy>();
var renderRegistry = new RendererRegistry();
var jsRuntime = Mock.Of<IJSRuntime>();
remoteRenderer = remoteRenderer ?? GetRemoteRenderer();
var dispatcher = Renderer.CreateDefaultDispatcher();
remoteRenderer = remoteRenderer ?? GetRemoteRenderer(dispatcher);
handlers = handlers ?? Array.Empty<CircuitHandler>();
return new CircuitHost(
@ -139,24 +142,26 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
clientProxy,
renderRegistry,
remoteRenderer,
configure: _ => { },
new List<ComponentDescriptor>(),
dispatcher,
jsRuntime: jsRuntime,
handlers);
}
private static TestRemoteRenderer GetRemoteRenderer()
private static TestRemoteRenderer GetRemoteRenderer(IDispatcher dispatcher)
{
return new TestRemoteRenderer(
Mock.Of<IServiceProvider>(),
new RendererRegistry(),
dispatcher,
Mock.Of<IJSRuntime>(),
Mock.Of<IClientProxy>());
}
private class TestRemoteRenderer : RemoteRenderer
{
public TestRemoteRenderer(IServiceProvider serviceProvider, RendererRegistry rendererRegistry, IJSRuntime jsRuntime, IClientProxy client)
: base(serviceProvider, rendererRegistry, jsRuntime, client, CreateDefaultDispatcher(), NullLogger.Instance)
public TestRemoteRenderer(IServiceProvider serviceProvider, RendererRegistry rendererRegistry, IDispatcher dispatcher, IJSRuntime jsRuntime, IClientProxy client)
: base(serviceProvider, rendererRegistry, jsRuntime, client, dispatcher, HtmlEncoder.Default, NullLogger.Instance)
{
}

View File

@ -126,7 +126,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Infrastructure
output = builder.ToString();
}
throw new InvalidOperationException($"Failed to start selenium sever. {Environment.NewLine}{output}", ex.GetBaseException());
throw new InvalidOperationException($"Failed to start selenium sever. {System.Environment.NewLine}{output}", ex.GetBaseException());
}
}

View File

@ -5,9 +5,9 @@
</div>
<div class="main">
<div class="top-row px-4">
@*<div class="top-row px-4">
<a href="http://blazor.net" target="_blank" class="ml-md-auto">About</a>
</div>
</div>*@
<div class="content px-4">
@Body

View File

@ -10,6 +10,7 @@
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Components.Server" />
<Reference Include="Microsoft.AspNetCore.Mvc" />
<ProjectReference Include="..\ComponentsApp.App\ComponentsApp.App.csproj" />
</ItemGroup>

View File

@ -1,3 +1,6 @@
@page "{*clientroutes}"
@using ComponentsApp.App
<!DOCTYPE html>
<html>
<head>
@ -9,7 +12,7 @@
<link href="css/site.css" rel="stylesheet" />
</head>
<body>
<app>Loading...</app>
<app>@(await Html.RenderComponentAsync<App>())</app>
<script src="_framework/components.server.js"></script>
</body>

View File

@ -14,8 +14,10 @@ namespace ComponentsApp.Server
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddSingleton<CircuitHandler, LoggingCircuitHandler>();
services.AddRazorComponents<App.Startup>();
services.AddRazorComponents();
services.AddSingleton<WeatherForecastService, DefaultWeatherForecastService>();
}
@ -27,7 +29,11 @@ namespace ComponentsApp.Server
}
app.UseStaticFiles();
app.UseRazorComponents<App.Startup>();
app.UseRouting(builder =>
{
builder.MapRazorPages();
builder.MapComponentHub<App.App>("app");
});
}
}
}

View File

@ -1,6 +1,8 @@
using Microsoft.AspNetCore.Components.Server;
using BasicTestApp;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
@ -24,7 +26,7 @@ namespace TestServer
{
options.AddPolicy("AllowAll", _ => { /* Controlled below */ });
});
services.AddRazorComponents<BasicTestApp.Startup>();
services.AddRazorComponents();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
@ -47,8 +49,12 @@ namespace TestServer
// we're not relying on any extra magic inside UseServerSideBlazor, since it's
// important that people can set up these bits of middleware manually (e.g., to
// swap in UseAzureSignalR instead of UseSignalR).
subdirApp.UseSignalR(route => route.MapHub<ComponentsHub>(ComponentsHub.DefaultPath));
subdirApp.UseBlazor<BasicTestApp.Startup>();
subdirApp.UseRouting(routes =>
routes.MapHub<ComponentHub>(ComponentHub.DefaultPath).AddComponent<Index>(selector: "root"));
subdirApp.MapWhen(
ctx => ctx.Features.Get<IEndpointFeature>()?.Endpoint == null,
blazorBuilder => blazorBuilder.UseBlazor<BasicTestApp.Startup>());
});
}

View File

@ -4,6 +4,8 @@
using System;
using System.Buffers;
using System.Linq;
using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Components.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
@ -17,8 +19,10 @@ using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Buffers;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Filters;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Infrastructure;
using Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponents;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using Microsoft.JSInterop;
namespace Microsoft.Extensions.DependencyInjection
{
@ -199,6 +203,12 @@ namespace Microsoft.Extensions.DependencyInjection
ServiceDescriptor.Transient<IApplicationModelProvider, ViewDataAttributeApplicationModelProvider>());
services.TryAddSingleton<SaveTempDataFilter>();
//
// Component prerendering
//
services.TryAddSingleton<IComponentPrerenderer, MvcRazorComponentPrerenderer>();
services.TryAddScoped<IUriHelper, HttpUriHelper>();
services.TryAddScoped<IJSRuntime, UnsupportedJavaScriptRuntime>();
services.TryAddTransient<ControllerSaveTempDataPropertyFilter>();

View File

@ -1,14 +1,13 @@
// 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.Collections.Generic;
using System.IO;
using System.Text.Encodings.Web;
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponents;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures
@ -16,7 +15,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
/// <summary>
/// Extensions for rendering components.
/// </summary>
public static class HtmlHelperComponentExtensions
public static class HtmlHelperRazorComponentExtensions
{
/// <summary>
/// Renders the <typeparamref name="TComponent"/> <see cref="IComponent"/>.
@ -27,7 +26,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
{
if (htmlHelper == null)
{
throw new System.ArgumentNullException(nameof(htmlHelper));
throw new ArgumentNullException(nameof(htmlHelper));
}
return htmlHelper.RenderComponentAsync<TComponent>(null);
@ -46,39 +45,21 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
{
if (htmlHelper == null)
{
throw new System.ArgumentNullException(nameof(htmlHelper));
throw new ArgumentNullException(nameof(htmlHelper));
}
var serviceProvider = htmlHelper.ViewContext.HttpContext.RequestServices;
var encoder = serviceProvider.GetRequiredService<HtmlEncoder>();
var dispatcher = Renderer.CreateDefaultDispatcher();
using (var htmlRenderer = new HtmlRenderer(serviceProvider, encoder.Encode, dispatcher))
var httpContext = htmlHelper.ViewContext.HttpContext;
var serviceProvider = httpContext.RequestServices;
var prerenderer = serviceProvider.GetRequiredService<IComponentPrerenderer>();
var result = await prerenderer.PrerenderComponentAsync(new ComponentPrerenderingContext
{
var result = await dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync<TComponent>(
parameters == null ?
ParameterCollection.Empty :
ParameterCollection.FromDictionary(HtmlHelper.ObjectToDictionary(parameters))));
Context = httpContext,
ComponentType = typeof(TComponent),
Parameters = parameters == null ? ParameterCollection.Empty : ParameterCollection.FromDictionary(HtmlHelper.ObjectToDictionary(parameters))
});
return new ComponentHtmlContent(result);
}
}
private class ComponentHtmlContent : IHtmlContent
{
private readonly IEnumerable<string> _componentResult;
public ComponentHtmlContent(IEnumerable<string> componentResult)
{
_componentResult = componentResult;
}
public void WriteTo(TextWriter writer, HtmlEncoder encoder)
{
foreach (var element in _componentResult)
{
writer.Write(element);
}
}
return new ComponentHtmlContent(result);
}
}
}

View File

@ -0,0 +1,29 @@
// 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.Collections.Generic;
using System.IO;
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> _componentResult;
public ComponentHtmlContent(IEnumerable<string> componentResult)
{
_componentResult = componentResult;
}
public void WriteTo(TextWriter writer, HtmlEncoder encoder)
{
foreach (var element in _componentResult)
{
writer.Write(element);
}
}
}
}

View File

@ -0,0 +1,59 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Components.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures
{
internal class HttpUriHelper : UriHelperBase
{
private HttpContext _context;
public HttpUriHelper()
{
}
public void InitializeState(HttpContext context)
{
_context = context;
InitializeState();
}
protected override void InitializeState()
{
if (_context == null)
{
throw new InvalidOperationException($"'{typeof(HttpUriHelper)}' not initialized.");
}
SetAbsoluteBaseUri(GetContextBaseUri());
SetAbsoluteUri(GetFullUri());
}
private string GetFullUri()
{
var request = _context.Request;
return UriHelper.BuildAbsolute(
request.Scheme,
request.Host,
request.PathBase,
request.Path,
request.QueryString);
}
private string GetContextBaseUri()
{
var request = _context.Request;
return UriHelper.BuildAbsolute(request.Scheme, request.Host, request.PathBase);
}
protected override void NavigateToCore(string uri, bool forceLoad)
{
// For now throw as we don't have a good way of aborting the request from here.
throw new InvalidOperationException(
"Redirects are not supported on a prerendering environment.");
}
}
}

View File

@ -0,0 +1,22 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading.Tasks;
using Microsoft.JSInterop;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures
{
internal class UnsupportedJavaScriptRuntime : IJSRuntime
{
public Task<T> InvokeAsync<T>(string identifier, params object[] args)
{
throw new InvalidOperationException("JavaScript interop calls cannot be issued during server-side prerendering, because the page has not yet loaded in the browser. Prerendered components must wrap any JavaScript interop calls in conditional logic to ensure those interop calls are not attempted during prerendering.");
}
public void UntrackObjectRef(DotNetObjectRef dotNetObjectRef)
{
throw new InvalidOperationException("JavaScript interop calls cannot be issued during server-side prerendering, because the page has not yet loaded in the browser. Prerendered components must wrap any JavaScript interop calls in conditional logic to ensure those interop calls are not attempted during prerendering.");
}
}
}

View File

@ -0,0 +1,39 @@
// 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.Collections.Generic;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Components.Services;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponents
{
internal class MvcRazorComponentPrerenderer : IComponentPrerenderer
{
private readonly HtmlEncoder _encoder;
public MvcRazorComponentPrerenderer(HtmlEncoder encoder)
{
_encoder = encoder;
}
public async Task<IEnumerable<string>> PrerenderComponentAsync(ComponentPrerenderingContext context)
{
var dispatcher = Renderer.CreateDefaultDispatcher();
var parameters = context.Parameters;
// This shouldn't be moved to the constructor as we want a request scoped service.
var helper = (HttpUriHelper)context.Context.RequestServices.GetRequiredService<IUriHelper>();
helper.InitializeState(context.Context);
using (var htmlRenderer = new HtmlRenderer(context.Context.RequestServices, _encoder.Encode, dispatcher))
{
return await dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync(
context.ComponentType,
parameters));
}
}
}
}

View File

@ -0,0 +1,29 @@
// 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 Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Components.Server
{
/// <summary>
/// The context for prerendering a component.
/// </summary>
public class ComponentPrerenderingContext
{
/// <summary>
/// Gets or sets the component type.
/// </summary>
public Type ComponentType { get; set; }
/// <summary>
/// Gets or sets the parameters for the component.
/// </summary>
public ParameterCollection Parameters { get; set; }
/// <summary>
/// Gets or sets the <see cref="HttpContext"/> in which the prerendering has been initiated.
/// </summary>
public HttpContext Context { get; set; }
}
}

View File

@ -0,0 +1,21 @@
// 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.Collections.Generic;
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.Components.Server
{
/// <summary>
/// Prerrenders <see cref="IComponent"/> instances.
/// </summary>
public interface IComponentPrerenderer
{
/// <summary>
/// Prerrenders the component <see cref="ComponentPrerenderingContext.ComponentType"/>.
/// </summary>
/// <param name="context">The context in which the prerrendering is happening.</param>
/// <returns><see cref="Task{TResult}"/> that will complete when the prerendering is done.</returns>
Task<IEnumerable<string>> PrerenderComponentAsync(ComponentPrerenderingContext context);
}
}

View File

@ -2,15 +2,18 @@
// 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.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Components.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponents;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.JSInterop;
using Moq;
using Xunit;
@ -53,6 +56,82 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
Assert.Equal("<p>Hello Steve!</p>", content);
}
[Fact]
public async Task CanCatch_ComponentWithSynchronousException()
{
// Arrange
var helper = CreateHelper();
// Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderComponentAsync<ExceptionComponent>(new
{
IsAsync = false
}));
// Assert
Assert.Equal("Threw an exception synchronously", exception.Message);
}
[Fact]
public async Task CanCatch_ComponentWithAsynchronousException()
{
// Arrange
var helper = CreateHelper();
// Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderComponentAsync<ExceptionComponent>(new
{
IsAsync = true
}));
// Assert
Assert.Equal("Threw an exception asynchronously", exception.Message);
}
[Fact]
public async Task Rendering_ComponentWithJsInteropThrows()
{
// Arrange
var helper = CreateHelper();
// Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderComponentAsync<ExceptionComponent>(new
{
JsInterop = true
}));
// Assert
Assert.Equal("JavaScript interop calls cannot be issued during server-side prerendering, " +
"because the page has not yet loaded in the browser. Prerendered components must wrap any JavaScript " +
"interop calls in conditional logic to ensure those interop calls are not attempted during prerendering.",
exception.Message);
}
[Fact]
public async Task UriHelperRedirect_ThrowsInvalidOperationException()
{
// Arrange
var ctx = new DefaultHttpContext();
ctx.Request.Scheme = "http";
ctx.Request.Host = new HostString("localhost");
ctx.Request.PathBase = "/base";
ctx.Request.Path = "/path";
ctx.Request.QueryString = new QueryString("?query=value");
var helper = CreateHelper(ctx);
var writer = new StringWriter();
// Act
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderComponentAsync<RedirectComponent>(new
{
RedirectUri = "http://localhost/redirect"
}));
Assert.Equal("Redirects are not supported on a prerendering environment.", exception.Message);
}
[Fact]
public async Task CanRender_AsyncComponent()
{
@ -108,23 +187,32 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
var content = writer.ToString();
// Assert
Assert.Equal(expectedContent.Replace("\r\n","\n"), content);
Assert.Equal(expectedContent.Replace("\r\n", "\n"), content);
}
private static IHtmlHelper CreateHelper(Action<IServiceCollection> configureServices = null)
private static IHtmlHelper CreateHelper(HttpContext ctx = null, Action<IServiceCollection> configureServices = null)
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton<HtmlEncoder>(HtmlEncoder.Default);
configureServices?.Invoke(serviceCollection);
var services = new ServiceCollection();
services.AddSingleton(HtmlEncoder.Default);
services.AddSingleton<IJSRuntime,UnsupportedJavaScriptRuntime>();
services.AddSingleton<IUriHelper,HttpUriHelper>();
services.AddSingleton<IComponentPrerenderer, MvcRazorComponentPrerenderer>();
configureServices?.Invoke(services);
var helper = new Mock<IHtmlHelper>();
var context = ctx ?? new DefaultHttpContext();
context.RequestServices = services.BuildServiceProvider();
context.Request.Scheme = "http";
context.Request.Host = new HostString("localhost");
context.Request.PathBase = "/base";
context.Request.Path = "/path";
context.Request.QueryString = QueryString.FromUriComponent("?query=value");
helper.Setup(h => h.ViewContext)
.Returns(new ViewContext()
{
HttpContext = new DefaultHttpContext()
{
RequestServices = serviceCollection.BuildServiceProvider()
}
HttpContext = context
});
return helper.Object;
}
@ -151,6 +239,47 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
}
}
private class RedirectComponent : ComponentBase
{
[Inject] IUriHelper UriHelper { get; set; }
[Parameter] public string RedirectUri { get; set; }
[Parameter] public bool Force { get; set; }
protected override void OnInit()
{
UriHelper.NavigateTo(RedirectUri, Force);
}
}
private class ExceptionComponent : ComponentBase
{
[Parameter] bool IsAsync { get; set; }
[Parameter] bool JsInterop { get; set; }
[Inject] IJSRuntime JsRuntime { get; set; }
protected override async Task OnParametersSetAsync()
{
if (JsInterop)
{
await JsRuntime.InvokeAsync<int>("window.alert", "Interop!");
}
if (!IsAsync)
{
throw new InvalidOperationException("Threw an exception synchronously");
}
else
{
await Task.Yield();
throw new InvalidOperationException("Threw an exception asynchronously");
}
}
}
private class GreetingComponent : ComponentBase
{
[Parameter] public string Name { get; set; }

View File

@ -481,6 +481,9 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
var expected = new[]
{
"BasicWebSite",
"Microsoft.AspNetCore.Components.Server",
"Microsoft.AspNetCore.SpaServices",
"Microsoft.AspNetCore.SpaServices.Extensions",
"Microsoft.AspNetCore.Mvc.TagHelpers",
"Microsoft.AspNetCore.Mvc.Razor",
};

View File

@ -5,6 +5,7 @@ using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using AngleSharp.Parser.Html;
using BasicWebSite;
using BasicWebSite.Services;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
@ -15,11 +16,14 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
{
public ComponentRenderingFunctionalTests(MvcTestFixture<BasicWebSite.StartupWithoutEndpointRouting> fixture)
{
Factory = fixture;
Client = Client ?? CreateClient(fixture);
}
public HttpClient Client { get; }
public MvcTestFixture<StartupWithoutEndpointRouting> Factory { get; }
[Fact]
public async Task Renders_BasicComponent()
{
@ -33,6 +37,53 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
AssertComponent("\n <p>Hello world!</p>\n", "Greetings", content);
}
[Fact]
public async Task Renders_BasicComponent_UsingRazorComponents_Prerrenderer()
{
// Arrange & Act
var client = Factory
.WithWebHostBuilder(builder => builder.ConfigureServices(services => services.AddRazorComponents()))
.CreateClient();
var response = await client.GetAsync("http://localhost/components");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var content = await response.Content.ReadAsStringAsync();
AssertComponent("\n <p>Hello world!</p>\n", "Greetings", content);
}
[Fact]
public async Task Renders_RoutingComponent()
{
// Arrange & Act
var response = await Client.GetAsync("http://localhost/components/routable");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var content = await response.Content.ReadAsStringAsync();
AssertComponent("\n Router component\n<p>Routed successfully</p>\n", "Routing", content);
}
[Fact]
public async Task Renders_RoutingComponent_UsingRazorComponents_Prerrenderer()
{
// Arrange & Act
var client = Factory
.WithWebHostBuilder(builder => builder.ConfigureServices(services => services.AddRazorComponents()))
.CreateClient();
var response = await client.GetAsync("http://localhost/components/routable");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var content = await response.Content.ReadAsStringAsync();
AssertComponent("\n Router component\n<p>Routed successfully</p>\n", "Routing", content);
}
[Fact]
public async Task Renders_AsyncComponent()
{

View File

@ -15,6 +15,7 @@
<Reference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" />
<Reference Include="Microsoft.AspNetCore.Authentication" />
<Reference Include="Microsoft.AspNetCore.Components.Server" />
<Reference Include="Microsoft.AspNetCore.Localization.Routing" />
<Reference Include="Microsoft.AspNetCore.Server.IISIntegration" />
<Reference Include="Microsoft.AspNetCore.Server.Kestrel" />

View File

@ -9,7 +9,7 @@ using Microsoft.AspNetCore.Mvc;
namespace BasicWebSite.Controllers
{
public class ComponentsController : Controller
public class RazorComponentsController : Controller
{
private static WeatherRow[] _weatherData = new[]
{
@ -51,6 +51,7 @@ namespace BasicWebSite.Controllers
};
[HttpGet("/components")]
[HttpGet("/components/routable")]
public IActionResult Index()
{
return View();

View File

@ -0,0 +1 @@
<p>Route not found</p>

View File

@ -0,0 +1,2 @@
@page "/components/routable"
<p>Routed successfully</p>

View File

@ -0,0 +1,5 @@
Router component
<Router
AppAssembly="System.Reflection.Assembly.GetAssembly(typeof(RouterContainer))"
FallbackComponent="typeof(Fallback)">
</Router>

View File

@ -6,4 +6,8 @@
<div id="FetchData">
@(await Html.RenderComponentAsync<FetchData>(new { StartDate = new DateTime(2019, 01, 15) }))
</div>
<div id="Routing">
@(await Html.RenderComponentAsync<RouterContainer>())
</div>

View File

@ -10,6 +10,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.Server" Version="${MicrosoftAspNetCoreComponentsServerPackageVersion}" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="${MicrosoftAspNetCoreMvcNewtonsoftJsonPackageVersion}" />
</ItemGroup>
</Project>

View File

@ -1,12 +0,0 @@
using Microsoft.AspNetCore.Components.Builder;
namespace RazorComponentsWeb_CSharp.Components
{
public class Startup
{
public void Configure(IComponentsApplicationBuilder app)
{
app.AddComponent<App>("app");
}
}
}

View File

@ -1,3 +1,4 @@
@page "{*clientPath}"
<!DOCTYPE html>
<html>
<head>
@ -9,7 +10,7 @@
<link href="css/site.css" rel="stylesheet" />
</head>
<body>
<app>Loading...</app>
<app>@(await Html.RenderComponentAsync<App>())</app>
<script src="_framework/components.server.js"></script>
</body>

View File

@ -0,0 +1,3 @@
@using RazorComponentsWeb_CSharp.Components
@namespace RazorComponentsWeb_CSharp.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

View File

@ -10,6 +10,7 @@ using Microsoft.AspNetCore.HttpsPolicy;
#endif
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using RazorComponentsWeb_CSharp.Components;
using RazorComponentsWeb_CSharp.Services;
namespace RazorComponentsWeb_CSharp
@ -20,8 +21,12 @@ namespace RazorComponentsWeb_CSharp
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc()
.AddNewtonsoftJson();
services.AddRazorComponents();
services.AddSingleton<WeatherForecastService>();
services.AddRazorComponents<Components.Startup>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
@ -43,7 +48,12 @@ namespace RazorComponentsWeb_CSharp
app.UseHttpsRedirection();
#endif
app.UseStaticFiles();
app.UseRazorComponents<Components.Startup>();
app.UseRouting(routes =>
{
routes.MapRazorPages();
routes.MapComponentHub<App>("app");
});
}
}
}