Remove stateful prerendering

Fixes: #12245
Fixes: #12630

This change removes stateful pre-rendering from Server-Side Blazor. This
means that when you render a component during the initial HTTP request,
we we will no longer preserve the component instances and their
parameters. While this feature was useful, it cause serious scalability
concerns.

This means that it will now be required to register "entry-point"
components in startup similar to client-side Blazor.
This commit is contained in:
Ryan Nowak 2019-07-29 16:28:38 -07:00 committed by Ryan Nowak
parent 97489dc50d
commit 45f50905d5
54 changed files with 100 additions and 1871 deletions

View File

@ -101,7 +101,6 @@
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Razor" ProjectPath="$(RepoRoot)src\Razor\Razor\src\Microsoft.AspNetCore.Razor.csproj" RefProjectPath="$(RepoRoot)src\Razor\Razor\ref\Microsoft.AspNetCore.Razor.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Mvc.Abstractions" ProjectPath="$(RepoRoot)src\Mvc\Mvc.Abstractions\src\Microsoft.AspNetCore.Mvc.Abstractions.csproj" RefProjectPath="$(RepoRoot)src\Mvc\Mvc.Abstractions\ref\Microsoft.AspNetCore.Mvc.Abstractions.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Mvc.ApiExplorer" ProjectPath="$(RepoRoot)src\Mvc\Mvc.ApiExplorer\src\Microsoft.AspNetCore.Mvc.ApiExplorer.csproj" RefProjectPath="$(RepoRoot)src\Mvc\Mvc.ApiExplorer\ref\Microsoft.AspNetCore.Mvc.ApiExplorer.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Mvc.Components.Prerendering" ProjectPath="$(RepoRoot)src\Mvc\Mvc.Components.Prerendering\src\Microsoft.AspNetCore.Mvc.Components.Prerendering.csproj" RefProjectPath="$(RepoRoot)src\Mvc\Mvc.Components.Prerendering\ref\Microsoft.AspNetCore.Mvc.Components.Prerendering.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Mvc.Core" ProjectPath="$(RepoRoot)src\Mvc\Mvc.Core\src\Microsoft.AspNetCore.Mvc.Core.csproj" RefProjectPath="$(RepoRoot)src\Mvc\Mvc.Core\ref\Microsoft.AspNetCore.Mvc.Core.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Mvc.Cors" ProjectPath="$(RepoRoot)src\Mvc\Mvc.Cors\src\Microsoft.AspNetCore.Mvc.Cors.csproj" RefProjectPath="$(RepoRoot)src\Mvc\Mvc.Cors\ref\Microsoft.AspNetCore.Mvc.Cors.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Mvc.DataAnnotations" ProjectPath="$(RepoRoot)src\Mvc\Mvc.DataAnnotations\src\Microsoft.AspNetCore.Mvc.DataAnnotations.csproj" RefProjectPath="$(RepoRoot)src\Mvc\Mvc.DataAnnotations\ref\Microsoft.AspNetCore.Mvc.DataAnnotations.csproj" />

View File

@ -73,7 +73,6 @@
<AspNetCoreAppReference Include="Microsoft.AspNetCore.Razor" />
<AspNetCoreAppReference Include="Microsoft.AspNetCore.Mvc.Abstractions" />
<AspNetCoreAppReference Include="Microsoft.AspNetCore.Mvc.ApiExplorer" />
<AspNetCoreAppReference Include="Microsoft.AspNetCore.Mvc.Components.Prerendering" />
<AspNetCoreAppReference Include="Microsoft.AspNetCore.Mvc.Core" />
<AspNetCoreAppReference Include="Microsoft.AspNetCore.Mvc.Cors" />
<AspNetCoreAppReference Include="Microsoft.AspNetCore.Mvc.DataAnnotations" />

View File

@ -13,8 +13,12 @@ namespace Microsoft.AspNetCore.Analyzers.TestFiles.CompilationFeatureDetectorTes
app.UseEndpoints(endpoints =>
{
endpoints.MapBlazorHub();
endpoints.MapBlazorHub<App>("app");
});
}
public class App : Microsoft.AspNetCore.Components.ComponentBase
{
}
}
}

View File

@ -208,8 +208,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
..\..\.editorconfig = ..\..\.editorconfig
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Mvc.Components.Prerendering", "..\Mvc\Mvc.Components.Prerendering\src\Microsoft.AspNetCore.Mvc.Components.Prerendering.csproj", "{3A4132B6-60DA-43A0-8E7B-4BF346F3247C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.SignalR.Protocols.Json", "..\SignalR\common\Protocols.Json\src\Microsoft.AspNetCore.SignalR.Protocols.Json.csproj", "{ED210157-461B-45BB-9D86-B81A62792C30}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.SignalR.Client", "..\SignalR\clients\csharp\Client\src\Microsoft.AspNetCore.SignalR.Client.csproj", "{DA137BD4-F7F1-4D53-855F-5EC40CEA36B0}"
@ -1330,18 +1328,6 @@ Global
{9088E4E4-B855-457F-AE9E-D86709A5E1F4}.Release|x64.Build.0 = Debug|Any CPU
{9088E4E4-B855-457F-AE9E-D86709A5E1F4}.Release|x86.ActiveCfg = Debug|Any CPU
{9088E4E4-B855-457F-AE9E-D86709A5E1F4}.Release|x86.Build.0 = Debug|Any CPU
{3A4132B6-60DA-43A0-8E7B-4BF346F3247C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3A4132B6-60DA-43A0-8E7B-4BF346F3247C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3A4132B6-60DA-43A0-8E7B-4BF346F3247C}.Debug|x64.ActiveCfg = Debug|Any CPU
{3A4132B6-60DA-43A0-8E7B-4BF346F3247C}.Debug|x64.Build.0 = Debug|Any CPU
{3A4132B6-60DA-43A0-8E7B-4BF346F3247C}.Debug|x86.ActiveCfg = Debug|Any CPU
{3A4132B6-60DA-43A0-8E7B-4BF346F3247C}.Debug|x86.Build.0 = Debug|Any CPU
{3A4132B6-60DA-43A0-8E7B-4BF346F3247C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3A4132B6-60DA-43A0-8E7B-4BF346F3247C}.Release|Any CPU.Build.0 = Release|Any CPU
{3A4132B6-60DA-43A0-8E7B-4BF346F3247C}.Release|x64.ActiveCfg = Release|Any CPU
{3A4132B6-60DA-43A0-8E7B-4BF346F3247C}.Release|x64.Build.0 = Release|Any CPU
{3A4132B6-60DA-43A0-8E7B-4BF346F3247C}.Release|x86.ActiveCfg = Release|Any CPU
{3A4132B6-60DA-43A0-8E7B-4BF346F3247C}.Release|x86.Build.0 = Release|Any CPU
{ED210157-461B-45BB-9D86-B81A62792C30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{ED210157-461B-45BB-9D86-B81A62792C30}.Debug|Any CPU.Build.0 = Debug|Any CPU
{ED210157-461B-45BB-9D86-B81A62792C30}.Debug|x64.ActiveCfg = Debug|Any CPU
@ -1548,7 +1534,6 @@ Global
{04262990-929C-42BF-85A9-21C25FA95617} = {2FC10057-7A0A-4E34-8302-879925BC0102}
{DC47C40A-FC38-44E4-94A4-ADE794E76309} = {2FC10057-7A0A-4E34-8302-879925BC0102}
{9088E4E4-B855-457F-AE9E-D86709A5E1F4} = {7260DED9-22A9-4E9D-92F4-5E8A4404DEAF}
{3A4132B6-60DA-43A0-8E7B-4BF346F3247C} = {2FC10057-7A0A-4E34-8302-879925BC0102}
{ED210157-461B-45BB-9D86-B81A62792C30} = {2FC10057-7A0A-4E34-8302-879925BC0102}
{DA137BD4-F7F1-4D53-855F-5EC40CEA36B0} = {2FC10057-7A0A-4E34-8302-879925BC0102}
{0CDAB70B-71DC-43BE-ACB7-AD2EE3541FFB} = {2FC10057-7A0A-4E34-8302-879925BC0102}

View File

@ -14,8 +14,6 @@ namespace Microsoft.AspNetCore.Builder
}
public static partial class ComponentEndpointRouteBuilderExtensions
{
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, 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; }
@ -36,22 +34,6 @@ namespace Microsoft.AspNetCore.Components.Server
public System.TimeSpan DisconnectedCircuitRetentionPeriod { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public System.TimeSpan JSInteropDefaultCallTimeout { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
}
public partial class ComponentPrerenderingContext
{
public ComponentPrerenderingContext() { }
public System.Type ComponentType { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public Microsoft.AspNetCore.Http.HttpContext Context { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public Microsoft.AspNetCore.Components.ParameterView Parameters { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
}
public sealed partial class ComponentPrerenderResult
{
internal ComponentPrerenderResult() { }
public void WriteTo(System.IO.TextWriter writer) { }
}
public partial interface IComponentPrerenderer
{
System.Threading.Tasks.Task<Microsoft.AspNetCore.Components.Server.ComponentPrerenderResult> PrerenderComponentAsync(Microsoft.AspNetCore.Components.Server.ComponentPrerenderingContext context);
}
}
namespace Microsoft.AspNetCore.Components.Server.Circuits
{

View File

@ -15,42 +15,6 @@ namespace Microsoft.AspNetCore.Builder
/// </summary>
public static class ComponentEndpointRouteBuilderExtensions
{
/// <summary>
/// Maps the Blazor <see cref="Hub" /> to the default path.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/>.</param>
/// <returns>The <see cref="ComponentEndpointConventionBuilder"/>.</returns>
public static ComponentEndpointConventionBuilder MapBlazorHub(this IEndpointRouteBuilder endpoints)
{
if (endpoints == null)
{
throw new ArgumentNullException(nameof(endpoints));
}
return endpoints.MapBlazorHub(configureOptions: _ => { });
}
/// <summary>
/// Maps the Blazor <see cref="Hub" /> to the default path.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/>.</param>
/// <param name="configureOptions">A callback to configure dispatcher options.</param>
/// <returns>The <see cref="ComponentEndpointConventionBuilder"/>.</returns>
public static ComponentEndpointConventionBuilder MapBlazorHub(this IEndpointRouteBuilder endpoints, Action<HttpConnectionDispatcherOptions> configureOptions)
{
if (endpoints == null)
{
throw new ArgumentNullException(nameof(endpoints));
}
if (configureOptions == null)
{
throw new ArgumentNullException(nameof(configureOptions));
}
return new ComponentEndpointConventionBuilder(endpoints.MapHub<ComponentHub>(ComponentHub.DefaultPath, configureOptions));
}
/// <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"/>.

View File

@ -7,8 +7,6 @@ using System.Security.Claims;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.Web.Rendering;
using Microsoft.Extensions.DependencyInjection;
@ -57,7 +55,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
CircuitClientProxy client,
RendererRegistry rendererRegistry,
RemoteRenderer renderer,
IList<ComponentDescriptor> descriptors,
IReadOnlyList<ComponentDescriptor> descriptors,
RemoteJSRuntime jsRuntime,
CircuitHandler[] circuitHandlers,
ILogger logger)
@ -92,24 +90,10 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
public RendererRegistry RendererRegistry { get; }
public IList<ComponentDescriptor> Descriptors { get; }
public IReadOnlyList<ComponentDescriptor> Descriptors { get; }
public IServiceProvider Services { get; }
public Task<ComponentRenderedText> PrerenderComponentAsync(Type componentType, ParameterView parameters)
{
return Renderer.Dispatcher.InvokeAsync(async () =>
{
var result = await Renderer.RenderComponentAsync(componentType, parameters);
// When we prerender we start the circuit in a disconnected state. As such, we only call
// OnCircuitOpenenedAsync here and when the client reconnects we run OnConnectionUpAsync
await OnCircuitOpenedAsync(CancellationToken.None);
return result;
});
}
public void SetCircuitUser(ClaimsPrincipal user)
{
var authenticationStateProvider = Services.GetService<AuthenticationStateProvider>() as IHostEnvironmentAuthenticationStateProvider;
@ -120,26 +104,6 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
}
}
internal void InitializeCircuitAfterPrerender(UnhandledExceptionEventHandler unhandledException)
{
if (!_initialized)
{
_initialized = true;
UnhandledException += unhandledException;
var uriHelper = (RemoteUriHelper)Services.GetRequiredService<IUriHelper>();
if (!uriHelper.HasAttachedJSRuntime)
{
uriHelper.AttachJsRuntime(JSRuntime);
}
var navigationInterception = (RemoteNavigationInterception)Services.GetRequiredService<INavigationInterception>();
if (!navigationInterception.HasAttachedJSRuntime)
{
navigationInterception.AttachJSRuntime(JSRuntime);
}
}
}
internal void SendPendingBatches()
{
// Dispatch any buffered renders we accumulated during a disconnect.
@ -188,7 +152,6 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
return;
}
await Renderer.Dispatcher.InvokeAsync(() =>
{
SetCurrentCircuitHost(this);
@ -233,13 +196,11 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
// That's because AddComponentAsync waits for quiescence, which can take
// arbitrarily long. In the meantime we might need to be receiving and
// processing incoming JSInterop calls or similar.
for (var i = 0; i < Descriptors.Count; i++)
var count = Descriptors.Count;
for (var i = 0; i < count; i++)
{
var (componentType, domElementSelector, prerendered) = Descriptors[i];
if (!prerendered)
{
await Renderer.AddComponentAsync(componentType, domElementSelector);
}
var (componentType, domElementSelector) = Descriptors[i];
await Renderer.AddComponentAsync(componentType, domElementSelector);
}
}
catch (Exception ex)
@ -256,7 +217,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
try
{
AssertInitialized();
if(assemblyName == "Microsoft.AspNetCore.Components.Web" && methodIdentifier == "DispatchEvent")
if (assemblyName == "Microsoft.AspNetCore.Components.Web" && methodIdentifier == "DispatchEvent")
{
Log.DispatchEventTroughJSInterop(_logger);
return;

View File

@ -1,207 +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.Linq;
using System.Runtime.ExceptionServices;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
namespace Microsoft.AspNetCore.Components.Server.Circuits
{
internal class CircuitPrerenderer : IComponentPrerenderer
{
private static object CircuitHostKey = new object();
private static object CancellationStatusKey = new object();
private static readonly JsonSerializerOptions _jsonSerializationOptions =
new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
private readonly CircuitFactory _circuitFactory;
private readonly CircuitRegistry _registry;
public CircuitPrerenderer(
CircuitFactory circuitFactory,
CircuitRegistry registry)
{
_circuitFactory = circuitFactory;
_registry = registry;
}
public async Task<ComponentPrerenderResult> PrerenderComponentAsync(ComponentPrerenderingContext prerenderingContext)
{
var context = prerenderingContext.Context;
var cancellationStatus = GetOrCreateCancellationStatus(context);
if (cancellationStatus.Canceled)
{
// Avoid creating a circuit host if other component earlier in the pipeline already triggered
// cancellation (e.g., by navigating or throwing). Instead render nothing.
return new ComponentPrerenderResult(Array.Empty<string>());
}
var circuitHost = GetOrCreateCircuitHost(context, cancellationStatus);
ComponentRenderedText renderResult;
try
{
renderResult = await circuitHost.PrerenderComponentAsync(
prerenderingContext.ComponentType,
prerenderingContext.Parameters);
}
catch (NavigationException navigationException)
{
// Cleanup the state as we won't need it any longer.
// Signal callbacks that we don't have to register the circuit.
await CleanupCircuitState(context, cancellationStatus, circuitHost);
// Navigation was attempted during prerendering.
if (prerenderingContext.Context.Response.HasStarted)
{
// We can't perform a redirect as the server already started sending the response.
// This is considered an application error as the developer should buffer the response until
// all components have rendered.
throw new InvalidOperationException("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" +
"reponse and avoid using features like FlushAsync() before all components on the page have been rendered to prevent failed navigation commands.", navigationException);
}
context.Response.Redirect(navigationException.Location);
return new ComponentPrerenderResult(Array.Empty<string>());
}
catch
{
// If prerendering any component fails, cancel prerendering entirely and dispose the DI scope
await CleanupCircuitState(context, cancellationStatus, circuitHost);
throw;
}
circuitHost.Descriptors.Add(new ComponentDescriptor
{
ComponentType = prerenderingContext.ComponentType,
Prerendered = true
});
var record = JsonSerializer.Serialize(new PrerenderedComponentRecord(
// We need to do this due to the fact that -- is not allowed within HTML comments and HTML doesn't encode '-'.
// We will never have '..' sequences because we Base64UrlEncode the circuit id
circuitHost.CircuitId.Replace("--", ".."),
circuitHost.Renderer.Id,
renderResult.ComponentId),
_jsonSerializationOptions);
var result = (new[] {
$"<!-- M.A.C.Component: {record} -->",
}).Concat(renderResult.Tokens).Concat(
new[] {
$"<!-- M.A.C.Component: {renderResult.ComponentId} -->"
});
return new ComponentPrerenderResult(result);
}
private PrerenderingCancellationStatus GetOrCreateCancellationStatus(HttpContext context)
{
if (context.Items.TryGetValue(CancellationStatusKey, out var existingValue))
{
return (PrerenderingCancellationStatus)existingValue;
}
else
{
var cancellationStatus = new PrerenderingCancellationStatus();
context.Items[CancellationStatusKey] = cancellationStatus;
return cancellationStatus;
}
}
private static async Task CleanupCircuitState(HttpContext context, PrerenderingCancellationStatus cancellationStatus, CircuitHost circuitHost)
{
cancellationStatus.Canceled = true;
context.Items.Remove(CircuitHostKey);
await circuitHost.DisposeAsync();
}
private CircuitHost GetOrCreateCircuitHost(HttpContext context, PrerenderingCancellationStatus cancellationStatus)
{
if (context.Items.TryGetValue(CircuitHostKey, out var existingHost))
{
return (CircuitHost)existingHost;
}
else
{
var result = _circuitFactory.CreateCircuitHost(
context,
client: new CircuitClientProxy(), // This creates an "offline" client.
GetFullUri(context.Request),
GetFullBaseUri(context.Request),
context.User);
result.UnhandledException += CircuitHost_UnhandledException;
context.Response.OnCompleted(() =>
{
result.UnhandledException -= CircuitHost_UnhandledException;
if (!cancellationStatus.Canceled)
{
_registry.RegisterDisconnectedCircuit(result);
}
return Task.CompletedTask;
});
context.Items.Add(CircuitHostKey, result);
return result;
}
}
private void CircuitHost_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
// Throw all exceptions encountered during pre-rendering so the default developer
// error page can respond.
ExceptionDispatchInfo.Capture((Exception)e.ExceptionObject).Throw();
}
private string GetFullUri(HttpRequest request)
{
return UriHelper.BuildAbsolute(
request.Scheme,
request.Host,
request.PathBase,
request.Path,
request.QueryString);
}
private string GetFullBaseUri(HttpRequest request)
{
var result = UriHelper.BuildAbsolute(request.Scheme, request.Host, request.PathBase);
// PathBase may be "/" or "/some/thing", but to be a well-formed base URI
// it has to end with a trailing slash
if (!result.EndsWith('/'))
{
result += '/';
}
return result;
}
private readonly struct PrerenderedComponentRecord
{
public PrerenderedComponentRecord(string circuitId, int rendererId, int componentId)
{
CircuitId = circuitId;
RendererId = rendererId;
ComponentId = componentId;
}
public string CircuitId { get; }
public int RendererId { get; }
public int ComponentId { get; }
}
private class PrerenderingCancellationStatus
{
public bool Canceled { get; set; }
}
}
}

View File

@ -100,7 +100,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
return circuitHost;
}
internal static IList<ComponentDescriptor> ResolveComponentMetadata(HttpContext httpContext, CircuitClientProxy client)
internal static List<ComponentDescriptor> ResolveComponentMetadata(HttpContext httpContext, CircuitClientProxy client)
{
if (!client.Connected)
{

View File

@ -11,13 +11,10 @@ namespace Microsoft.AspNetCore.Components.Server
public string Selector { get; set; }
public bool Prerendered { get; set; }
public void Deconstruct(out Type componentType, out string selector, out bool prerendered)
public void Deconstruct(out Type componentType, out string selector)
{
componentType = ComponentType;
selector = Selector;
prerendered = Prerendered;
}
}
}

View File

@ -48,6 +48,9 @@ namespace Microsoft.AspNetCore.Components.Server
/// <summary>
/// For unit testing only.
/// </summary>
// We store the circuit host in Context.Items which is tied to the lifetime of the underlying
// SignalR connection. There's no need to clean this up, it's a non-owning reference and it
// will go away when the connection does.
internal CircuitHost CircuitHost
{
get => (CircuitHost)Context.Items[CircuitKey];
@ -65,7 +68,6 @@ namespace Microsoft.AspNetCore.Components.Server
return Task.CompletedTask;
}
CircuitHost = null;
if (exception != null)
{
return _circuitRegistry.DisconnectAsync(circuitHost, Context.ConnectionId);
@ -92,6 +94,8 @@ namespace Microsoft.AspNetCore.Components.Server
{
Log.UnhandledExceptionInCircuit(_logger, circuitHost.CircuitId, e);
}
await _circuitRegistry.DisconnectAsync(circuitHost, Context.ConnectionId);
}
/// <summary>
@ -149,8 +153,8 @@ namespace Microsoft.AspNetCore.Components.Server
if (circuitHost != null)
{
CircuitHost = circuitHost;
CircuitHost.UnhandledException += CircuitHost_UnhandledException;
circuitHost.InitializeCircuitAfterPrerender(CircuitHost_UnhandledException);
circuitHost.SetCircuitUser(Context.User);
circuitHost.SendPendingBatches();
return true;

View File

@ -63,12 +63,6 @@ namespace Microsoft.Extensions.DependencyInjection
services.TryAddSingleton<CircuitRegistry>();
// 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
//
// These intentionally replace the non-interactive versions included in MVC.

View File

@ -1,33 +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.Collections.Generic;
using System.IO;
namespace Microsoft.AspNetCore.Components.Server
{
/// <summary>
/// Represents the result of a prerendering an <see cref="IComponent"/>.
/// </summary>
public sealed class ComponentPrerenderResult
{
private readonly IEnumerable<string> _result;
internal ComponentPrerenderResult(IEnumerable<string> result)
{
_result = result;
}
/// <summary>
/// Writes the prerendering result to the given <paramref name="writer"/>.
/// </summary>
/// <param name="writer">The <see cref="TextWriter"/> the results will be written to.</param>
public void WriteTo(TextWriter writer)
{
foreach (var element in _result)
{
writer.Write(element);
}
}
}
}

View File

@ -1,29 +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.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 ParameterView 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

@ -1,20 +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.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<ComponentPrerenderResult> PrerenderComponentAsync(ComponentPrerenderingContext context);
}
}

View File

@ -1,253 +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.IO;
using System.Security.Claims;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Server.Circuits;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Components.Server.Tests.Circuits
{
public class CircuitPrerendererTest
{
private static readonly Regex ContentWrapperRegex = new Regex(
"<!-- M.A.C.Component: {\"circuitId\":\"[^\"]+\",\"rendererId\":0,\"componentId\":0} -->(?<content>.*)<!-- M.A.C.Component: 0 -->",
RegexOptions.Compiled | RegexOptions.Singleline, TimeSpan.FromSeconds(1)); // Treat the entire input string as a single line
private static readonly Regex CircuitInfoRegex = new Regex(
"<!-- M.A.C.Component: (?<info>.*?) -->.*",
RegexOptions.Compiled | RegexOptions.Singleline, TimeSpan.FromSeconds(1)); // Treat the entire input string as a single line
// Because CircuitPrerenderer is a point of integration with HttpContext,
// it's not a good candidate for unit testing. The majority of prerendering
// unit tests should be elsewhere in HtmlRendererTests inside the
// Microsoft.AspNetCore.Components.Tests projects.
//
// The only unit tests added here should specifically be about how we're
// interacting with the HttpContext for configuring the prerenderer.
[Fact]
public async Task ExtractsUriFromHttpContext_EmptyPathBase()
{
// Arrange
var circuitFactory = new TestCircuitFactory();
var circuitRegistry = new CircuitRegistry(
Options.Create(new CircuitOptions()),
Mock.Of<ILogger<CircuitRegistry>>(),
TestCircuitIdFactory.CreateTestFactory());
var circuitPrerenderer = new CircuitPrerenderer(circuitFactory, circuitRegistry);
var httpContext = new DefaultHttpContext();
var httpRequest = httpContext.Request;
httpRequest.Scheme = "https";
httpRequest.Host = new HostString("example.com", 1234);
httpRequest.Path = "/some/path";
var prerenderingContext = new ComponentPrerenderingContext
{
ComponentType = typeof(UriDisplayComponent),
Parameters = ParameterView.Empty,
Context = httpContext
};
// Act
var result = await circuitPrerenderer.PrerenderComponentAsync(prerenderingContext);
// Assert
Assert.Equal(string.Join("", new[]
{
"The current URI is ",
"https://example.com:1234/some/path",
" within base URI ",
"https://example.com:1234/"
}), GetUnwrappedContent(result));
}
private string GetUnwrappedContent(ComponentPrerenderResult rawResult)
{
var writer = new StringWriter();
rawResult.WriteTo(writer);
return ContentWrapperRegex.Match(writer.ToString())
.Groups["content"].Value
.Replace("\r\n","\n");
}
private JsonDocument GetUnwrappedCircuitInfo(ComponentPrerenderResult rawResult)
{
var writer = new StringWriter();
rawResult.WriteTo(writer);
var circuitInfo = CircuitInfoRegex.Match(writer.ToString()).Groups["info"].Value;
return JsonDocument.Parse(circuitInfo);
}
[Fact]
public async Task ExtractsUriFromHttpContext_NonemptyPathBase()
{
// Arrange
var circuitFactory = new TestCircuitFactory();
var circuitRegistry = new CircuitRegistry(
Options.Create(new CircuitOptions()),
Mock.Of<ILogger<CircuitRegistry>>(),
TestCircuitIdFactory.CreateTestFactory());
var circuitPrerenderer = new CircuitPrerenderer(circuitFactory, circuitRegistry);
var httpContext = new DefaultHttpContext();
var httpRequest = httpContext.Request;
httpRequest.Scheme = "https";
httpRequest.Host = new HostString("example.com", 1234);
httpRequest.PathBase = "/my/dir";
httpRequest.Path = "/some/path";
var prerenderingContext = new ComponentPrerenderingContext
{
ComponentType = typeof(UriDisplayComponent),
Parameters = ParameterView.Empty,
Context = httpContext
};
// Act
var result = await circuitPrerenderer.PrerenderComponentAsync(prerenderingContext);
// Assert
Assert.Equal(string.Join("", new[]
{
"The current URI is ",
"https://example.com:1234/my/dir/some/path",
" within base URI ",
"https://example.com:1234/my/dir/"
}), GetUnwrappedContent(result));
}
[Fact]
public async Task ReplacesDashesWithDots_WhenTheyAppearInPairs()
{
// Arrange
var circuitFactory = new TestCircuitFactory(() => "--1234--");
var circuitRegistry = new CircuitRegistry(
Options.Create(new CircuitOptions()),
Mock.Of<ILogger<CircuitRegistry>>(),
TestCircuitIdFactory.CreateTestFactory());
var circuitPrerenderer = new CircuitPrerenderer(circuitFactory, circuitRegistry);
var httpContext = new DefaultHttpContext();
var httpRequest = httpContext.Request;
httpRequest.Scheme = "https";
httpRequest.Host = new HostString("example.com", 1234);
httpRequest.Path = "/some/path";
var prerenderingContext = new ComponentPrerenderingContext
{
ComponentType = typeof(UriDisplayComponent),
Parameters = ParameterView.Empty,
Context = httpContext
};
// Act
var result = await circuitPrerenderer.PrerenderComponentAsync(prerenderingContext);
// Assert
Assert.Equal("..1234..", GetUnwrappedCircuitInfo(result).RootElement.GetProperty("circuitId").GetString());
}
[Fact]
public async Task DisposesCircuitScopeEvenIfPrerenderingThrows()
{
// Arrange
var circuitFactory = new MockServiceScopeCircuitFactory();
var circuitRegistry = new CircuitRegistry(
Options.Create(new CircuitOptions()),
Mock.Of<ILogger<CircuitRegistry>>(),
TestCircuitIdFactory.CreateTestFactory());
var httpContext = new DefaultHttpContext();
var prerenderer = new CircuitPrerenderer(circuitFactory, circuitRegistry);
var prerenderingContext = new ComponentPrerenderingContext
{
ComponentType = typeof(ThrowExceptionComponent),
Parameters = ParameterView.Empty,
Context = httpContext
};
// Act
await Assert.ThrowsAsync<InvalidTimeZoneException>(async () =>
await prerenderer.PrerenderComponentAsync(prerenderingContext));
// Assert
circuitFactory.MockServiceScope.Verify(scope => scope.Dispose(), Times.Once());
}
class TestCircuitFactory : CircuitFactory
{
private readonly Func<string> _circuitIdFactory;
public TestCircuitFactory(Func<string> circuitIdFactory = null)
{
_circuitIdFactory = circuitIdFactory ?? (() => Guid.NewGuid().ToString());
}
public override CircuitHost CreateCircuitHost(HttpContext httpContext, CircuitClientProxy client, string uriAbsolute, string baseUriAbsolute, ClaimsPrincipal user)
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddScoped<IUriHelper>(_ =>
{
var uriHelper = new RemoteUriHelper(NullLogger<RemoteUriHelper>.Instance);
uriHelper.InitializeState(uriAbsolute, baseUriAbsolute);
return uriHelper;
});
var serviceScope = serviceCollection.BuildServiceProvider().CreateScope();
return TestCircuitHost.Create(_circuitIdFactory(), serviceScope);
}
}
class MockServiceScopeCircuitFactory : CircuitFactory
{
public Mock<IServiceScope> MockServiceScope { get; }
= new Mock<IServiceScope>();
public override CircuitHost CreateCircuitHost(HttpContext httpContext, CircuitClientProxy client, string uriAbsolute, string baseUriAbsolute, ClaimsPrincipal user)
{
return TestCircuitHost.Create(Guid.NewGuid().ToString(), MockServiceScope.Object);
}
}
class UriDisplayComponent : IComponent
{
private RenderHandle _renderHandle;
[Inject] IUriHelper UriHelper { get; set; }
public void Attach(RenderHandle renderHandle)
{
_renderHandle = renderHandle;
}
public Task SetParametersAsync(ParameterView parameters)
{
_renderHandle.Render(builder =>
{
builder.AddContent(0, "The current URI is ");
builder.AddContent(1, UriHelper.GetAbsoluteUri());
builder.AddContent(2, " within base URI ");
builder.AddContent(3, UriHelper.GetBaseUri());
});
return Task.CompletedTask;
}
}
class ThrowExceptionComponent : IComponent
{
public void Attach(RenderHandle renderHandle)
=> throw new InvalidTimeZoneException();
public Task SetParametersAsync(ParameterView parameters)
=> Task.CompletedTask;
}
}
}

View File

@ -11,7 +11,6 @@ using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Logging.Testing;
using Microsoft.Extensions.Options;
using Moq;
@ -19,7 +18,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
{
internal class TestCircuitHost : CircuitHost
{
private TestCircuitHost(string circuitId, IServiceScope scope, CircuitClientProxy client, RendererRegistry rendererRegistry, RemoteRenderer renderer, IList<ComponentDescriptor> descriptors, RemoteJSRuntime jsRuntime, CircuitHandler[] circuitHandlers, ILogger logger)
private TestCircuitHost(string circuitId, IServiceScope scope, CircuitClientProxy client, RendererRegistry rendererRegistry, RemoteRenderer renderer, IReadOnlyList<ComponentDescriptor> descriptors, RemoteJSRuntime jsRuntime, CircuitHandler[] circuitHandlers, ILogger logger)
: base(circuitId, scope, client, rendererRegistry, renderer, descriptors, jsRuntime, circuitHandlers, logger)
{
}

View File

@ -2,8 +2,8 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Diagnostics;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Moq;
@ -25,7 +25,7 @@ namespace Microsoft.AspNetCore.Components.Server.Tests
.UseRouting()
.UseEndpoints(endpoints =>
{
endpoints.MapBlazorHub(dispatchOptions => called = true);
endpoints.MapBlazorHub<MyComponent>("app", dispatchOptions => called = true);
}).Build();
// Assert
@ -68,5 +68,18 @@ namespace Microsoft.AspNetCore.Components.Server.Tests
return new ApplicationBuilder(serviceProvder);
}
private class MyComponent : IComponent
{
public void Attach(RenderHandle renderHandle)
{
throw new System.NotImplementedException();
}
public Task SetParametersAsync(ParameterView parameters)
{
throw new System.NotImplementedException();
}
}
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -6,7 +6,7 @@ import { shouldAutoStart } from './BootCommon';
import { RenderQueue } from './Platform/Circuits/RenderQueue';
import { ConsoleLogger } from './Platform/Logging/Loggers';
import { LogLevel, Logger } from './Platform/Logging/Logger';
import { discoverPrerenderedCircuits, startCircuit } from './Platform/Circuits/CircuitManager';
import { startCircuit } from './Platform/Circuits/CircuitManager';
import { setEventDispatcher } from './Rendering/RendererEventDispatcher';
import { resolveOptions, BlazorOptions } from './Platform/Circuits/BlazorOptions';
import { DefaultReconnectionHandler } from './Platform/Circuits/DefaultReconnectionHandler';
@ -27,32 +27,18 @@ 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.');
// Initialize statefully prerendered circuits and their components
// Note: This will all be removed soon
const initialConnection = await initializeConnection(options, logger);
const circuits = discoverPrerenderedCircuits(document);
for (let i = 0; i < circuits.length; i++) {
const circuit = circuits[i];
for (let j = 0; j < circuit.components.length; j++) {
const component = circuit.components[j];
component.initialize();
}
}
const circuit = await startCircuit(initialConnection);
if (!circuit) {
logger.log(LogLevel.Information, 'No preregistered components to render.');
}
const reconnect = async (existingConnection?: signalR.HubConnection): Promise<boolean> => {
if (renderingFailed) {
// We can't reconnect after a failure, so exit early.
return false;
}
const reconnection = existingConnection || await initializeConnection(options, logger);
const results = await Promise.all(circuits.map(circuit => circuit.reconnect(reconnection)));
if (reconnectionFailed(results)) {
const reconnection = existingConnection || await initializeConnection(options, logger);
if (!(await circuit.reconnect(reconnection))) {
logger.log(LogLevel.Information, 'Reconnection attempt failed.');
return false;
}
@ -63,19 +49,7 @@ async function boot(userOptions?: Partial<BlazorOptions>): Promise<void> {
window['Blazor'].reconnect = reconnect;
const reconnectTask = reconnect(initialConnection);
if (circuit) {
circuits.push(circuit);
}
await reconnectTask;
logger.log(LogLevel.Information, 'Blazor server-side application started.');
function reconnectionFailed(results: boolean[]): boolean {
return !results.reduce((current, next) => current && next, true);
}
}
async function initializeConnection(options: BlazorOptions, logger: Logger): Promise<signalR.HubConnection> {

View File

@ -1,14 +1,10 @@
import { internalFunctions as uriHelperFunctions } from '../../Services/UriHelper';
import { ComponentDescriptor, MarkupRegistrationTags, StartComponentComment, EndComponentComment } from './ComponentDescriptor';
export class CircuitDescriptor {
public circuitId: string;
public components: ComponentDescriptor[];
public constructor(circuitId: string, components: ComponentDescriptor[]) {
public constructor(circuitId: string) {
this.circuitId = circuitId;
this.components = components;
}
public reconnect(reconnection: signalR.HubConnection): Promise<boolean> {
@ -16,115 +12,11 @@ export class CircuitDescriptor {
}
}
export function discoverPrerenderedCircuits(document: Document): CircuitDescriptor[] {
const commentPairs = resolveCommentPairs(document);
const discoveredCircuits = new Map<string, ComponentDescriptor[]>();
for (let i = 0; i < commentPairs.length; i++) {
const pair = commentPairs[i];
// We replace '--' on the server with '..' when we prerender due to the fact that this
// is not allowed in HTML comments and doesn't get encoded by default.
const circuitId = pair.start.circuitId.replace('..', '--');
let circuit = discoveredCircuits.get(circuitId);
if (!circuit) {
circuit = [];
discoveredCircuits.set(circuitId, circuit);
}
const entry = new ComponentDescriptor(pair.start.componentId, circuitId, pair.start.rendererId, pair);
circuit.push(entry);
}
const circuits: CircuitDescriptor[] = [];
for (const [key, values] of discoveredCircuits) {
circuits.push(new CircuitDescriptor(key, values));
}
return circuits;
}
export async function startCircuit(connection: signalR.HubConnection): Promise<CircuitDescriptor | undefined> {
export async function startCircuit(connection: signalR.HubConnection): Promise<CircuitDescriptor> {
const result = await connection.invoke<string>('StartCircuit', uriHelperFunctions.getLocationHref(), uriHelperFunctions.getBaseURI());
if (result) {
return new CircuitDescriptor(result, []);
return new CircuitDescriptor(result);
} else {
return undefined;
throw new Error('Circuit failed to start');
}
}
function resolveCommentPairs(node: Node): MarkupRegistrationTags[] {
if (!node.hasChildNodes()) {
return [];
}
const result: MarkupRegistrationTags[] = [];
const children = node.childNodes;
let i = 0;
const childrenLength = children.length;
while (i < childrenLength) {
const currentChildNode = children[i];
const startComponent = getComponentStartComment(currentChildNode);
if (!startComponent) {
i++;
const childResults = resolveCommentPairs(currentChildNode);
for (let j = 0; j < childResults.length; j++) {
const childResult = childResults[j];
result.push(childResult);
}
continue;
}
const endComponent = getComponentEndComment(startComponent, children, i + 1, childrenLength);
result.push({ start: startComponent, end: endComponent });
i = endComponent.index + 1;
}
return result;
}
function getComponentStartComment(node: Node): StartComponentComment | undefined {
if (node.nodeType !== Node.COMMENT_NODE) {
return;
}
if (node.textContent) {
const componentStartComment = /\W+M.A.C.Component:[^{]*(.*)$/;
const definition = componentStartComment.exec(node.textContent);
const json = definition && definition[1];
if (json) {
try {
const { componentId, rendererId, circuitId } = JSON.parse(json);
const allComponents = componentId !== undefined && rendererId !== undefined && !!circuitId;
if (allComponents) {
return {
node: node as Comment,
circuitId,
rendererId: rendererId,
componentId: componentId,
};
}
} catch (error) {
}
throw new Error(`Found malformed start component comment at ${node.textContent}`);
}
}
}
function getComponentEndComment(component: StartComponentComment, children: NodeList, index: number, end: number): EndComponentComment {
for (let i = index; i < end; i++) {
const node = children[i];
if (node.nodeType !== Node.COMMENT_NODE) {
continue;
}
if (!node.textContent) {
continue;
}
const componentEndComment = /\W+M.A.C.Component:\W+(\d+)\W+$/;
const definition = componentEndComment.exec(node.textContent);
const json = definition && definition[1];
if (!json) {
continue;
}
try {
// The value is expected to be a JSON encoded number
const componentId = JSON.parse(json);
if (componentId === component.componentId) {
return { componentId, node: node as Comment, index: i };
}
} catch (error) {
}
throw new Error(`Found malformed end component comment at ${node.textContent}`);
}
throw new Error(`End component comment not found for ${component.node}`);
}

View File

@ -1,46 +0,0 @@
import { attachRootComponentToLogicalElement } from '../../Rendering/Renderer';
import { toLogicalRootCommentElement } from '../../Rendering/LogicalElements';
export interface EndComponentComment {
componentId: number;
node: Comment;
index: number;
}
export interface StartComponentComment {
node: Comment;
rendererId: number;
componentId: number;
circuitId: string;
}
// Represent pairs of start end comments indicating a component that was registered
// in markup (such as a prerendered component)
export interface MarkupRegistrationTags {
start: StartComponentComment;
end: EndComponentComment;
}
export class ComponentDescriptor {
public registrationTags: MarkupRegistrationTags;
public componentId: number;
public circuitId: string;
public rendererId: number;
public constructor(componentId: number, circuitId: string, rendererId: number, descriptor: MarkupRegistrationTags) {
this.componentId = componentId;
this.circuitId = circuitId;
this.rendererId = rendererId;
this.registrationTags = descriptor;
}
public initialize(): void {
const startEndPair = { start: this.registrationTags.start.node, end: this.registrationTags.end.node };
const logicalElement = toLogicalRootCommentElement(startEndPair.start, startEndPair.end);
attachRootComponentToLogicalElement(this.rendererId, logicalElement, this.componentId);
}
}

View File

@ -1,146 +0,0 @@
(global as any).DotNet = { attachReviver: jest.fn() };
import { discoverPrerenderedCircuits } from '../src/Platform/Circuits/CircuitManager';
import { JSDOM } from 'jsdom';
describe('CircuitManager', () => {
it('discoverPrerenderedCircuits returns discovered prerendered circuits', () => {
const dom = new JSDOM(`<!doctype HTML>
<html>
<head>
<title>Page</title>
</head>
<body>
<header>Preamble</header>
<!-- M.A.C.Component: {"circuitId":"1234","rendererId":2,"componentId":1} -->
<p>Prerendered content</p>
<!-- M.A.C.Component: 1 -->
<footer></footer>
</body>
</html>`);
const results = discoverPrerenderedCircuits(dom.window.document);
expect(results.length).toEqual(1);
expect(results[0].components.length).toEqual(1);
const result = results[0].components[0];
expect(result.circuitId).toEqual("1234");
expect(result.rendererId).toEqual(2);
expect(result.componentId).toEqual(1);
});
it('discoverPrerenderedCircuits returns discovers multiple prerendered circuits', () => {
const dom = new JSDOM(`<!doctype HTML>
<html>
<head>
<title>Page</title>
</head>
<body>
<header>Preamble</header>
<!-- M.A.C.Component: {"circuitId":"1234","rendererId":2,"componentId":1} -->
<p>Prerendered content</p>
<!-- M.A.C.Component: 1 -->
<footer>
<!-- M.A.C.Component: {"circuitId":"1234","rendererId":2,"componentId":2} -->
<p>Prerendered content</p>
<!-- M.A.C.Component: 2 -->
</footer>
</body>
</html>`);
const results = discoverPrerenderedCircuits(dom.window.document);
expect(results.length).toEqual(1);
expect(results[0].components.length).toEqual(2);
const first = results[0].components[0];
expect(first.circuitId).toEqual("1234");
expect(first.rendererId).toEqual(2);
expect(first.componentId).toEqual(1);
const second = results[0].components[1];
expect(second.circuitId).toEqual("1234");
expect(second.rendererId).toEqual(2);
expect(second.componentId).toEqual(2);
});
it('discoverPrerenderedCircuits throws for malformed circuits - improper nesting', () => {
const dom = new JSDOM(`<!doctype HTML>
<html>
<head>
<title>Page</title>
</head>
<body>
<header>Preamble</header>
<!-- M.A.C.Component: {"circuitId":"1234","rendererId":2,"componentId":1} -->
<p>Prerendered content</p>
<!-- M.A.C.Component: 2 -->
<footer>
<!-- M.A.C.Component: {"circuitId":"1234","rendererId":2,"componentId":2} -->
<p>Prerendered content</p>
<!-- M.A.C.Component: 1 -->
</footer>
</body>
</html>`);
expect(() => discoverPrerenderedCircuits(dom.window.document))
.toThrow();
});
it('discoverPrerenderedCircuits throws for malformed circuits - mixed string and int', () => {
const dom = new JSDOM(`<!doctype HTML>
<html>
<head>
<title>Page</title>
</head>
<body>
<header>Preamble</header>
<!-- M.A.C.Component: {"circuitId":"1234","rendererId":"2","componentId":"1"} -->
<p>Prerendered content</p>
<!-- M.A.C.Component: 1 -->
<footer>
<!-- M.A.C.Component: {"circuitId":"1234","rendererId":2,"componentId":2} -->
<p>Prerendered content</p>
<!-- M.A.C.Component: 2 -->
</footer>
</body>
</html>`);
expect(() => discoverPrerenderedCircuits(dom.window.document))
.toThrow();
});
it('discoverPrerenderedCircuits initializes circuits', () => {
const dom = new JSDOM(`<!doctype HTML>
<html>
<head>
<title>Page</title>
</head>
<body>
<header>Preamble</header>
<!-- M.A.C.Component: {"circuitId":"1234","rendererId":2,"componentId":1} -->
<p>Prerendered content</p>
<!-- M.A.C.Component: 1 -->
<footer>
<!-- M.A.C.Component: {"circuitId":"1234","rendererId":2,"componentId":2} -->
<p>Prerendered content</p>
<!-- M.A.C.Component: 2 -->
</footer>
</body>
</html>`);
const results = discoverPrerenderedCircuits(dom.window.document);
for (let i = 0; i < results.length; i++) {
const result = results[i];
for (let j = 0; j < result.components.length; j++) {
const component = result.components[j];
component.initialize();
}
}
});
});

View File

@ -184,11 +184,14 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
_ => element.Text != currentValue);
}
// Since we've removed stateful prerendering, the name which is passed in
// during prerendering cannot be retained. The first interactive render
// will remove it.
[Fact]
public void RendersContinueAfterPrerendering()
public void RendersDoNotPreserveState()
{
Browser.FindElement(By.LinkText("Greeter")).Click();
Browser.Equal("Hello Guest", () => Browser.FindElement(By.ClassName("greeting")).Text);
Browser.Equal("Hello", () => Browser.FindElement(By.ClassName("greeting")).Text);
}
[Fact]

View File

@ -6,7 +6,6 @@
<ItemGroup>
<Reference Include="Microsoft.AspNetCore" />
<Reference Include="Microsoft.AspNetCore.Mvc.Components.Prerendering" />
<Reference Include="Microsoft.AspNetCore.Components.Server" />
<Reference Include="Microsoft.AspNetCore.Mvc" />
<Reference Include="Newtonsoft.Json" />

View File

@ -2,7 +2,6 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Components.Server.Circuits;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
@ -40,8 +39,8 @@ namespace ComponentsApp.Server
{
endpoints.MapRazorPages();
endpoints.MapControllers();
endpoints.MapBlazorHub();
endpoints.MapFallbackToPage("/Index");
endpoints.MapBlazorHub<ComponentsApp.App.App>("app");
endpoints.MapFallbackToPage("/_Host");
});
}
}

View File

@ -3,6 +3,7 @@
using System;
using System.Net.Http;
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading;
@ -75,8 +76,6 @@ namespace Ignitor
public async Task ExecuteAsync(Uri uri)
{
string circuitId = await GetPrerenderedCircuitId(uri);
var builder = new HubConnectionBuilder();
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IHubProtocol, IgnitorMessagePackHubProtocol>());
builder.WithUrl(new Uri(uri, "_blazor/"));
@ -93,7 +92,7 @@ namespace Ignitor
connection.Closed += OnClosedAsync;
// Now everything is registered so we can start the circuit.
var success = await connection.InvokeAsync<bool>("ConnectCircuit", circuitId);
var success = await connection.InvokeAsync<bool>("StartCircuit", uri.AbsoluteUri, uri.GetLeftPart(UriPartial.Authority));
await TaskCompletionSource.Task;
@ -131,19 +130,6 @@ namespace Ignitor
}
}
private static async Task<string> GetPrerenderedCircuitId(Uri uri)
{
var httpClient = new HttpClient();
var response = await httpClient.GetAsync(uri);
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;
}
private static async Task ClickAsync(string id, ElementHive hive, HubConnection connection)
{
if (!hive.TryFindElementById(id, out var elementNode))

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
@ -8,11 +8,11 @@
<Reference Include="Microsoft.AspNetCore" />
<Reference Include="Microsoft.AspNetCore.Authentication.Cookies" />
<Reference Include="Microsoft.AspNetCore.Blazor.Server" />
<Reference Include="Microsoft.AspNetCore.Components.Server" />
<Reference Include="Microsoft.AspNetCore.Cors" />
<Reference Include="Microsoft.AspNetCore.Mvc" />
<Reference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" />
<Reference Include="Microsoft.AspNetCore.Components.Server" />
<Reference Include="Microsoft.AspNetCore.Mvc.Components.Prerendering" />
<Reference Include="Microsoft.AspNetCore.Mvc.ViewFeatures" />
<Reference Include="Microsoft.Extensions.Logging.Testing" />
</ItemGroup>

View File

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

View File

@ -1,11 +0,0 @@
<!-- This file is automatically generated. -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netcoreapp3.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp3.0'">
<Compile Include="Microsoft.AspNetCore.Mvc.Components.Prerendering.netcoreapp3.0.cs" />
<Reference Include="Microsoft.AspNetCore.Mvc.ViewFeatures" />
<Reference Include="Microsoft.AspNetCore.Components.Server" />
</ItemGroup>
</Project>

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.
namespace Microsoft.AspNetCore.Mvc.Rendering
{
public static partial class HtmlHelperComponentPrerenderingExtensions
{
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; }
[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; }
}
}

View File

@ -1,25 +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.IO;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Html;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures
{
internal class HtmlContentPrerenderComponentResultAdapter : IHtmlContent
{
private ComponentPrerenderResult _result;
public HtmlContentPrerenderComponentResultAdapter(ComponentPrerenderResult result)
{
_result = result;
}
public void WriteTo(TextWriter writer, HtmlEncoder encoder)
{
_result.WriteTo(writer);
}
}
}

View File

@ -1,75 +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.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Mvc.Rendering
{
/// <summary>
/// Extensions for rendering components.
/// </summary>
public static class HtmlHelperComponentPrerenderingExtensions
{
/// <summary>
/// Renders the <typeparamref name="TComponent"/> <see cref="IComponent"/>.
/// </summary>
/// <param name="htmlHelper">The <see cref="IHtmlHelper"/>.</param>
/// <returns>The HTML produced by the rendered <typeparamref name="TComponent"/>.</returns>
public static Task<IHtmlContent> RenderComponentAsync<TComponent>(this IHtmlHelper htmlHelper) where TComponent : IComponent
{
if (htmlHelper == null)
{
throw new ArgumentNullException(nameof(htmlHelper));
}
return htmlHelper.RenderComponentAsync<TComponent>(null);
}
/// <summary>
/// Renders the <typeparamref name="TComponent"/> <see cref="IComponent"/>.
/// </summary>
/// <param name="htmlHelper">The <see cref="IHtmlHelper"/>.</param>
/// <param name="parameters">An <see cref="object"/> containing the parameters to pass
/// to the component.</param>
/// <returns>The HTML produced by the rendered <typeparamref name="TComponent"/>.</returns>
public static async Task<IHtmlContent> RenderComponentAsync<TComponent>(
this IHtmlHelper htmlHelper,
object parameters) where TComponent : IComponent
{
if (htmlHelper == null)
{
throw new ArgumentNullException(nameof(htmlHelper));
}
var httpContext = htmlHelper.ViewContext.HttpContext;
var serviceProvider = httpContext.RequestServices;
var prerenderer = serviceProvider.GetService<IComponentPrerenderer>();
if (prerenderer == null)
{
throw new InvalidOperationException($"No '{typeof(IComponentPrerenderer).Name}' implementation has been registered in the dependency injection container. " +
$"This typically means a call to 'services.AddServerSideBlazor()' is missing in 'Startup.ConfigureServices'.");
}
var parametersCollection = parameters == null ?
ParameterView.Empty :
ParameterView.FromDictionary(HtmlHelper.ObjectToDictionary(parameters));
var result = await prerenderer.PrerenderComponentAsync(
new ComponentPrerenderingContext
{
ComponentType = typeof(TComponent),
Parameters = parametersCollection,
Context = httpContext
});
return new HtmlContentPrerenderComponentResultAdapter(result);
}
}
}

View File

@ -1,18 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>ASP.NET Core MVC component interactive rendering features. Contains types to integrate server-side rendered components into MVC Views and Pages.
</Description>
<TargetFramework>netcoreapp3.0</TargetFramework>
<NoWarn>$(NoWarn);CS1591</NoWarn>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageTags>aspnetcore;aspnetcoremvc</PackageTags>
<IsAspNetCoreApp>true</IsAspNetCoreApp>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Mvc.ViewFeatures" />
<Reference Include="Microsoft.AspNetCore.Components.Server" />
</ItemGroup>
</Project>

View File

@ -1,512 +0,0 @@
using System;
using System.IO;
using System.Text.Encodings.Web;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.JSInterop;
using Microsoft.Net.Http.Headers;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures
{
public class HtmlHelperComponentExtensionsTests
{
private static readonly Regex ContentWrapperRegex = new Regex(
"<!-- M.A.C.Component: {\"circuitId\":\"[^\"]+\",\"rendererId\":0,\"componentId\":0} -->(?<content>.*)<!-- M.A.C.Component: 0 -->",
RegexOptions.Compiled | RegexOptions.Singleline, TimeSpan.FromSeconds(1)); // Treat the entire input string as a single line
[Fact]
public async Task PrerenderComponentAsync_ThrowsInvalidOperationException_IfNoPrerendererHasBeenRegistered()
{
// Arrange
var helper = CreateHelper(null, s => { });
var writer = new StringWriter();
var expectedmessage = $"No 'IComponentPrerenderer' implementation has been registered in the dependency injection container. " +
$"This typically means a call to 'services.AddServerSideBlazor()' is missing in 'Startup.ConfigureServices'.";
// Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderComponentAsync<TestComponent>());
// Assert
Assert.Equal(expectedmessage, exception.Message);
}
[Fact]
public async Task CanRender_ParameterlessComponent()
{
// Arrange
var helper = CreateHelper();
// Act
var result = await helper.RenderComponentAsync<TestComponent>();
var unwrappedContent = GetUnwrappedContent(result);
// Assert
Assert.Equal("<h1>Hello world!</h1>", unwrappedContent);
}
[Fact]
public async Task CanRender_ComponentWithParametersObject()
{
// Arrange
var helper = CreateHelper();
// Act
var result = await helper.RenderComponentAsync<GreetingComponent>(new
{
Name = "Guest"
});
var unwrappedContent = GetUnwrappedContent(result);
// Assert
Assert.Equal("<p>Hello Guest!</p>", unwrappedContent);
}
[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 at this time. This is because the component is being " +
"prerendered and the page has not yet loaded in the browser or because the circuit is currently disconnected. " +
"Components must wrap any JavaScript interop calls in conditional logic to ensure those interop calls are not " +
"attempted during prerendering or while the client is disconnected.",
exception.Message);
}
[Fact]
public async Task UriHelperRedirect_ThrowsInvalidOperationException_WhenResponseHasAlreadyStarted()
{
// 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 responseMock = new Mock<IHttpResponseFeature>();
responseMock.Setup(r => r.HasStarted).Returns(true);
ctx.Features.Set(responseMock.Object);
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("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" +
"reponse and avoid using features like FlushAsync() before all components on the page have been rendered to prevent failed navigation commands.",
exception.Message);
}
[Fact]
public async Task HtmlHelper_Redirects_WhenComponentNavigates()
{
// 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);
// Act
await helper.RenderComponentAsync<RedirectComponent>(new
{
RedirectUri = "http://localhost/redirect"
});
// Assert
Assert.Equal(302, ctx.Response.StatusCode);
Assert.Equal("http://localhost/redirect", ctx.Response.Headers[HeaderNames.Location]);
}
[Fact]
public async Task HtmlHelper_AvoidsRendering_WhenNavigationHasHappened()
{
// 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 stringWriter = new StringWriter();
await helper.RenderComponentAsync<RedirectComponent>(new
{
RedirectUri = "http://localhost/redirect"
});
// Act
var result = await helper.RenderComponentAsync<GreetingComponent>(new { Name = "George" });
// Assert
Assert.NotNull(result);
result.WriteTo(stringWriter, HtmlEncoder.Default);
Assert.Equal("", stringWriter.ToString());
}
[Fact]
public async Task CanRender_AsyncComponent()
{
// Arrange
var helper = CreateHelper();
var expectedContent = @"<table>
<thead>
<tr>
<th>Date</th>
<th>Summary</th>
<th>F</th>
<th>C</th>
</tr>
</thead>
<tbody>
<tr>
<td>06/05/2018</td>
<td>Freezing</td>
<td>33</td>
<td>33</td>
</tr>
<tr>
<td>07/05/2018</td>
<td>Bracing</td>
<td>57</td>
<td>57</td>
</tr>
<tr>
<td>08/05/2018</td>
<td>Freezing</td>
<td>9</td>
<td>9</td>
</tr>
<tr>
<td>09/05/2018</td>
<td>Balmy</td>
<td>4</td>
<td>4</td>
</tr>
<tr>
<td>10/05/2018</td>
<td>Chilly</td>
<td>29</td>
<td>29</td>
</tr>
</tbody>
</table>";
// Act
var result = await helper.RenderComponentAsync<AsyncComponent>();
var unwrappedContent = GetUnwrappedContent(result);
// Assert
Assert.Equal(expectedContent.Replace("\r\n", "\n"), unwrappedContent);
}
private string GetUnwrappedContent(IHtmlContent rawResult)
{
var writer = new StringWriter();
rawResult.WriteTo(writer, HtmlEncoder.Default);
return ContentWrapperRegex.Match(writer.ToString())
.Groups["content"].Value
.Replace("\r\n", "\n");
}
private class TestComponent : IComponent
{
private RenderHandle _renderHandle;
public void Attach(RenderHandle renderHandle)
{
_renderHandle = renderHandle;
}
public Task SetParametersAsync(ParameterView parameters)
{
_renderHandle.Render(builder =>
{
builder.OpenElement(1, "h1");
builder.AddContent(2, "Hello world!");
builder.CloseElement();
});
return Task.CompletedTask;
}
}
private class RedirectComponent : ComponentBase
{
[Inject] IUriHelper UriHelper { get; set; }
[Parameter] public string RedirectUri { get; set; }
[Parameter] public bool Force { get; set; }
protected override void OnInitialized()
{
UriHelper.NavigateTo(RedirectUri, Force);
}
}
private class ExceptionComponent : ComponentBase
{
[Parameter] public bool IsAsync { get; set; }
[Parameter] public 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; }
protected override void OnParametersSet()
{
base.OnParametersSet();
}
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
base.BuildRenderTree(builder);
builder.OpenElement(1, "p");
builder.AddContent(2, $"Hello {Name}!");
builder.CloseElement();
}
}
private class AsyncComponent : ComponentBase
{
private static WeatherRow[] _weatherData = new[]
{
new WeatherRow
{
DateFormatted = "06/05/2018",
TemperatureC = 1,
Summary = "Freezing",
TemperatureF = 33
},
new WeatherRow
{
DateFormatted = "07/05/2018",
TemperatureC = 14,
Summary = "Bracing",
TemperatureF = 57
},
new WeatherRow
{
DateFormatted = "08/05/2018",
TemperatureC = -13,
Summary = "Freezing",
TemperatureF = 9
},
new WeatherRow
{
DateFormatted = "09/05/2018",
TemperatureC = -16,
Summary = "Balmy",
TemperatureF = 4
},
new WeatherRow
{
DateFormatted = "10/05/2018",
TemperatureC = 2,
Summary = "Chilly",
TemperatureF = 29
}
};
public class WeatherRow
{
public string DateFormatted { get; set; }
public int TemperatureC { get; set; }
public string Summary { get; set; }
public int TemperatureF { get; set; }
}
public WeatherRow[] RowsToDisplay { get; set; }
protected override async Task OnParametersSetAsync()
{
// Simulate an async workflow.
await Task.Yield();
RowsToDisplay = _weatherData;
}
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
base.BuildRenderTree(builder);
builder.OpenElement(0, "table");
builder.AddMarkupContent(1, "\n");
builder.OpenElement(2, "thead");
builder.AddMarkupContent(3, "\n");
builder.OpenElement(4, "tr");
builder.AddMarkupContent(5, "\n");
builder.OpenElement(6, "th");
builder.AddContent(7, "Date");
builder.CloseElement();
builder.AddMarkupContent(8, "\n");
builder.OpenElement(9, "th");
builder.AddContent(10, "Summary");
builder.CloseElement();
builder.AddMarkupContent(11, "\n");
builder.OpenElement(12, "th");
builder.AddContent(13, "F");
builder.CloseElement();
builder.AddMarkupContent(14, "\n");
builder.OpenElement(15, "th");
builder.AddContent(16, "C");
builder.CloseElement();
builder.AddMarkupContent(17, "\n");
builder.CloseElement();
builder.AddMarkupContent(18, "\n");
builder.CloseElement();
builder.AddMarkupContent(19, "\n");
builder.OpenElement(20, "tbody");
builder.AddMarkupContent(21, "\n");
if (RowsToDisplay != null)
{
foreach (var element in RowsToDisplay)
{
builder.OpenElement(22, "tr");
builder.AddMarkupContent(23, "\n");
builder.OpenElement(24, "td");
builder.AddContent(25, element.DateFormatted);
builder.CloseElement();
builder.AddMarkupContent(26, "\n");
builder.OpenElement(27, "td");
builder.AddContent(28, element.Summary);
builder.CloseElement();
builder.AddMarkupContent(29, "\n");
builder.OpenElement(30, "td");
builder.AddContent(31, element.TemperatureF);
builder.CloseElement();
builder.AddMarkupContent(32, "\n");
builder.OpenElement(33, "td");
builder.AddContent(34, element.TemperatureF);
builder.CloseElement();
builder.AddMarkupContent(35, "\n");
builder.CloseElement();
builder.AddMarkupContent(36, "\n");
}
}
builder.CloseElement();
builder.AddMarkupContent(37, "\n");
builder.CloseElement();
}
}
private static IHtmlHelper CreateHelper(HttpContext ctx = null, Action<IServiceCollection> configureServices = null)
{
var services = new ServiceCollection();
services.AddSingleton<IConfiguration>(new ConfigurationBuilder().Build());
services.AddLogging();
services.AddDataProtection();
services.AddSingleton(HtmlEncoder.Default);
configureServices = configureServices ?? (s => s.AddServerSideBlazor());
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 = context
});
return helper.Object;
}
}
}

View File

@ -1,11 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\shared\Mvc.Views.TestCommon\Microsoft.AspNetCore.Mvc.Views.TestCommon.csproj" />
<ProjectReference Include="..\..\shared\Mvc.TestDiagnosticListener\Microsoft.AspNetCore.Mvc.TestDiagnosticListener.csproj" />
</ItemGroup>
</Project>

View File

@ -322,6 +322,12 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
Rfc3339 = 0,
CurrentCulture = 1,
}
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; }
[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 partial class HtmlHelperDisplayExtensions
{
public static Microsoft.AspNetCore.Html.IHtmlContent Display(this Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper htmlHelper, string expression) { throw null; }
@ -465,12 +471,6 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
public static System.Threading.Tasks.Task RenderPartialAsync(this Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper htmlHelper, string partialViewName, Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary viewData) { throw null; }
public static System.Threading.Tasks.Task RenderPartialAsync(this Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper htmlHelper, string partialViewName, object model) { throw null; }
}
public static partial class HtmlHelperRazorComponentExtensions
{
public static System.Threading.Tasks.Task<Microsoft.AspNetCore.Html.IHtmlContent> RenderStaticComponentAsync<TComponent>(this Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper htmlHelper) where TComponent : Microsoft.AspNetCore.Components.IComponent { throw null; }
[System.Diagnostics.DebuggerStepThroughAttribute]
public static System.Threading.Tasks.Task<Microsoft.AspNetCore.Html.IHtmlContent> RenderStaticComponentAsync<TComponent>(this Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper htmlHelper, object parameters) where TComponent : Microsoft.AspNetCore.Components.IComponent { throw null; }
}
public static partial class HtmlHelperSelectExtensions
{
public static Microsoft.AspNetCore.Html.IHtmlContent DropDownList(this Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper htmlHelper, string expression) { throw null; }

View File

@ -14,21 +14,21 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
/// <summary>
/// Extensions for rendering components.
/// </summary>
public static class HtmlHelperRazorComponentExtensions
public static class HtmlHelperComponentExtensions
{
/// <summary>
/// Renders the <typeparamref name="TComponent"/> <see cref="IComponent"/>.
/// </summary>
/// <param name="htmlHelper">The <see cref="IHtmlHelper"/>.</param>
/// <returns>The HTML produced by the rendered <typeparamref name="TComponent"/>.</returns>
public static Task<IHtmlContent> RenderStaticComponentAsync<TComponent>(this IHtmlHelper htmlHelper) where TComponent : IComponent
public static Task<IHtmlContent> RenderComponentAsync<TComponent>(this IHtmlHelper htmlHelper) where TComponent : IComponent
{
if (htmlHelper == null)
{
throw new ArgumentNullException(nameof(htmlHelper));
}
return htmlHelper.RenderStaticComponentAsync<TComponent>(null);
return htmlHelper.RenderComponentAsync<TComponent>(null);
}
/// <summary>
@ -38,7 +38,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
/// <param name="parameters">An <see cref="object"/> containing the parameters to pass
/// to the component.</param>
/// <returns>The HTML produced by the rendered <typeparamref name="TComponent"/>.</returns>
public static async Task<IHtmlContent> RenderStaticComponentAsync<TComponent>(
public static async Task<IHtmlContent> RenderComponentAsync<TComponent>(
this IHtmlHelper htmlHelper,
object parameters) where TComponent : IComponent
{

View File

@ -7,7 +7,6 @@ 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.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc.Rendering;
@ -32,7 +31,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
var writer = new StringWriter();
// Act
var result = await helper.RenderStaticComponentAsync<TestComponent>();
var result = await helper.RenderComponentAsync<TestComponent>();
result.WriteTo(writer, HtmlEncoder.Default);
var content = writer.ToString();
@ -48,7 +47,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
var writer = new StringWriter();
// Act
var result = await helper.RenderStaticComponentAsync<GreetingComponent>(new
var result = await helper.RenderComponentAsync<GreetingComponent>(new
{
Name = "Steve"
});
@ -66,7 +65,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
var helper = CreateHelper();
// Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderStaticComponentAsync<ExceptionComponent>(new
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderComponentAsync<ExceptionComponent>(new
{
IsAsync = false
}));
@ -82,7 +81,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
var helper = CreateHelper();
// Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderStaticComponentAsync<ExceptionComponent>(new
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderComponentAsync<ExceptionComponent>(new
{
IsAsync = true
}));
@ -98,7 +97,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
var helper = CreateHelper();
// Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderStaticComponentAsync<ExceptionComponent>(new
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderComponentAsync<ExceptionComponent>(new
{
JsInterop = true
}));
@ -127,7 +126,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
var writer = new StringWriter();
// Act
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderStaticComponentAsync<RedirectComponent>(new
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderComponentAsync<RedirectComponent>(new
{
RedirectUri = "http://localhost/redirect"
}));
@ -151,7 +150,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
var helper = CreateHelper(ctx);
// Act
await helper.RenderStaticComponentAsync<RedirectComponent>(new
await helper.RenderComponentAsync<RedirectComponent>(new
{
RedirectUri = "http://localhost/redirect"
});
@ -211,7 +210,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
</table>";
// Act
var result = await helper.RenderStaticComponentAsync<AsyncComponent>();
var result = await helper.RenderComponentAsync<AsyncComponent>();
result.WriteTo(writer, HtmlEncoder.Default);
var content = writer.ToString();

View File

@ -313,12 +313,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Signal
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RazorBuildWebSite.PrecompiledViews", "test\WebSites\RazorBuildWebSite.PrecompiledViews\RazorBuildWebSite.PrecompiledViews.csproj", "{A8C3066F-E80D-4E03-9962-741B551B8FBC}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Mvc.Components.Prerendering", "Mvc.Components.Prerendering", "{45CE788D-4B69-4F83-981C-F43D8F15B0F1}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Mvc.Components.Prerendering", "Mvc.Components.Prerendering\src\Microsoft.AspNetCore.Mvc.Components.Prerendering.csproj", "{6D6489E5-48BD-4F9B-9EEE-22AEEA1E1890}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Mvc.Components.Prerendering.Test", "Mvc.Components.Prerendering\test\Microsoft.AspNetCore.Mvc.Components.Prerendering.Test.csproj", "{C6F3BCE6-1EFD-4360-932B-B98573E78926}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.SignalR.Protocols.Json", "..\SignalR\common\Protocols.Json\src\Microsoft.AspNetCore.SignalR.Protocols.Json.csproj", "{637119E8-5BBB-4FC7-A372-DAF38FF5EBD9}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extensions.ApiDescription.Server", "Extensions.ApiDescription.Server", "{C15AA245-9E54-4FD6-90FF-B46F47779C46}"

View File

@ -65,9 +65,7 @@
"Mvc.NewtonsoftJson\\test\\Microsoft.AspNetCore.Mvc.NewtonsoftJson.Test.csproj",
"Mvc.Razor.RuntimeCompilation\\src\\Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.csproj",
"Mvc.Razor.RuntimeCompilation\\test\\Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.Test.csproj",
"test\\WebSites\\RazorBuildWebSite.PrecompiledViews\\RazorBuildWebSite.PrecompiledViews.csproj",
"Mvc.Components.Prerendering\\src\\Microsoft.AspNetCore.Mvc.Components.Prerendering.csproj",
"Mvc.Components.Prerendering\\test\\Microsoft.AspNetCore.Mvc.Components.Prerendering.Test.csproj",
"test\\WebSites\\RazorBuildWebSite.PrecompiledViews\\RazorBuildWebSite.PrecompiledViews.csproj"
]
}
}

View File

@ -6,7 +6,6 @@
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Mvc" />
<Reference Include="Microsoft.AspNetCore.Mvc.Components.Prerendering" />
<Reference Include="Microsoft.AspNetCore.Components.Server" />
<Reference Include="Microsoft.AspNetCore.Diagnostics" />
<Reference Include="Microsoft.AspNetCore.Server.IISIntegration" />

View File

@ -8,7 +8,6 @@
<ItemGroup>
<ProjectReference Include="..\Mvc.Core.TestCommon\Microsoft.AspNetCore.Mvc.Core.TestCommon.csproj" />
<Reference Include="Microsoft.AspNetCore.Mvc.ViewFeatures" />
<Reference Include="Microsoft.AspNetCore.Mvc.Components.Prerendering" />
<Reference Include="Microsoft.AspNetCore.Razor.Runtime" />
<Reference Include="Microsoft.Extensions.WebEncoders" />

View File

@ -482,7 +482,6 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
{
"BasicWebSite",
"Microsoft.AspNetCore.Components.Server",
"Microsoft.AspNetCore.Mvc.Components.Prerendering",
"Microsoft.AspNetCore.SpaServices",
"Microsoft.AspNetCore.SpaServices.Extensions",
"Microsoft.AspNetCore.Mvc.TagHelpers",

View File

@ -40,23 +40,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
var content = await response.Content.ReadAsStringAsync();
AssertComponent("\n<p>Hello world!</p>", "Greetings", content);
}
[Fact]
public async Task Renders_BasicComponent_UsingRazorComponents_Prerenderer()
{
// Arrange & Act
var client = CreateClient(Factory
.WithWebHostBuilder(builder => builder.ConfigureServices(services => services.AddServerSideBlazor())));
var response = await client.GetAsync("http://localhost/components");
// Assert
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
var content = await response.Content.ReadAsStringAsync();
AssertComponent("\n<p>Hello world!</p>", "Greetings", content);
AssertComponent("<p>Hello world!</p>", "Greetings", content);
}
[Fact]
@ -71,7 +55,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
var content = await response.Content.ReadAsStringAsync();
AssertComponent("\nRouter component\n<p>Routed successfully</p>", "Routing", content);
AssertComponent("Router component\n<p>Routed successfully</p>", "Routing", content);
}
[Fact]
@ -89,21 +73,6 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
Assert.Equal("http://localhost/navigation-redirect", response.Headers.Location.ToString());
}
[Fact]
public async Task Redirects_Navigation_ComponentInteractive()
{
// Arrange & Act
var fixture = Factory.WithWebHostBuilder(builder => builder.ConfigureServices(services => services.AddServerSideBlazor()));
fixture.ClientOptions.AllowAutoRedirect = false;
var client = CreateClient(fixture);
var response = await client.GetAsync("http://localhost/components/Navigation/false");
// Assert
await response.AssertStatusCodeAsync(HttpStatusCode.Redirect);
Assert.Equal("http://localhost/navigation-redirect", response.Headers.Location.ToString());
}
[Fact]
public async Task Renders_RoutingComponent_UsingRazorComponents_Prerenderer()
{
@ -117,39 +86,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
var content = await response.Content.ReadAsStringAsync();
AssertComponent("\nRouter component\n<p>Routed successfully</p>", "Routing", content);
}
[Fact]
public async Task Renders_BasicComponentInteractive_UsingRazorComponents_Prerenderer()
{
// Arrange & Act
var client = CreateClient(Factory
.WithWebHostBuilder(builder => builder.ConfigureServices(services => services.AddServerSideBlazor())));
var response = await client.GetAsync("http://localhost/components/false");
// Assert
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
var content = await response.Content.ReadAsStringAsync();
AssertComponent("<p>Hello world!</p>", "Greetings", content, unwrap: true);
}
[Fact]
public async Task Renders_RoutingComponentInteractive_UsingRazorComponents_Prerenderer()
{
// Arrange & Act
var client = CreateClient(Factory
.WithWebHostBuilder(builder => builder.ConfigureServices(services => services.AddServerSideBlazor())));
var response = await client.GetAsync("http://localhost/components/routable/false");
// Assert
await response.AssertStatusCodeAsync(HttpStatusCode.OK);
var content = await response.Content.ReadAsStringAsync();
AssertComponent("Router component\n<p>Routed successfully</p>", "Routing", content, unwrap: true);
AssertComponent("Router component\n<p>Routed successfully</p>", "Routing", content);
}
[Fact]
@ -171,8 +108,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
public async Task Renders_AsyncComponent()
{
// Arrange & Act
var expectedHtml = @"
<h1>Weather forecast</h1>
var expectedHtml = @"<h1>Weather forecast</h1>
<p>This component demonstrates fetching data from the server.</p>
@ -230,24 +166,17 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
AssertComponent(expectedHtml, "FetchData", content);
}
private void AssertComponent(string expectedContent, string divId, string responseContent, bool unwrap = false)
private void AssertComponent(string expectedContent, string divId, string responseContent)
{
var parser = new HtmlParser();
var htmlDocument = parser.Parse(responseContent);
var div = htmlDocument.Body.QuerySelector($"#{divId}");
var content = unwrap ? GetUnwrappedContent(div.InnerHtml) : div.InnerHtml;
var content = div.InnerHtml;
Assert.Equal(
expectedContent.Replace("\r\n","\n"),
content.Replace("\r\n","\n"));
}
private string GetUnwrappedContent(string rawResult)
{
return ContentWrapperRegex.Match(rawResult)
.Groups["content"].Value
.Replace("\r\n", "\n");
}
// A simple delegating handler used in setting up test services so that we can configure
// services that talk back to the TestServer using HttpClient.
private class LoopHttpHandler : DelegatingHandler

View File

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

View File

@ -1,10 +1,6 @@
// 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.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@ -51,16 +47,16 @@ namespace BasicWebSite.Controllers
}
};
[HttpGet("/components/{staticPrerender=true}")]
[HttpGet("/components/routable/{staticPrerender=true}")]
public IActionResult Index(bool staticPrerender)
[HttpGet("/components/{**slug}")]
[HttpGet("/components/routable/{**slug}")]
public IActionResult Index()
{
// Override the path so that the router finds the RoutedPage component
// as the client router doesn't support optional parameters.
Request.Path = Request.Path.StartsWithSegments("/components/routable") ?
PathString.FromUriComponent("/components/routable") : Request.Path;
return View(staticPrerender);
return View();
}
[HttpGet("/WeatherData")]
@ -70,10 +66,10 @@ namespace BasicWebSite.Controllers
return Ok(_weatherData);
}
[HttpGet("/components/Navigation/{staticPrerender=true}")]
public IActionResult Navigation(bool staticPrerender)
[HttpGet("/components/Navigation")]
public IActionResult Navigation()
{
return View(staticPrerender);
return View();
}
private class WeatherRow

View File

@ -50,4 +50,4 @@ else
forecasts = await ForecastService.GetForecastAsync(StartDate);
}
}
}

View File

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

View File

@ -1,13 +1,3 @@
@using BasicWebSite.RazorComponents;
@model bool;
<h1>Navigation components</h1>
<div id="Navigation">
@if (Model)
{
@(await Html.RenderStaticComponentAsync<NavigationComponent>())
}
else
{
@(await Html.RenderComponentAsync<NavigationComponent>())
}
</div>
<div id="Navigation">@(await Html.RenderComponentAsync<NavigationComponent>())</div>

View File

@ -175,8 +175,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_dependencies", "_dependenc
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.ApiAuthorization.IdentityServer", "..\Identity\ApiAuthorization.IdentityServer\src\Microsoft.AspNetCore.ApiAuthorization.IdentityServer.csproj", "{6012D544-32B4-4F5C-B335-A224AA4F661D}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Mvc.Components.Prerendering", "..\Mvc\Mvc.Components.Prerendering\src\Microsoft.AspNetCore.Mvc.Components.Prerendering.csproj", "{706DBAF7-0BB4-4B4F-9B44-E7C3372ECDF2}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.SpaServices.Extensions", "..\Middleware\SpaServices.Extensions\src\Microsoft.AspNetCore.SpaServices.Extensions.csproj", "{06D0D7B2-EDA3-45A2-A060-AB791ED1DB80}"
EndProject
Global

View File

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