Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,16 @@ internal void AddEndpoints(
// The display name is for debug purposes by endpoint routing.
builder.DisplayName = $"{builder.RoutePattern.RawText} ({pageDefinition.DisplayName})";

builder.RequestDelegate = RazorComponentEndpoint.CreateRouteDelegate(pageDefinition.Type);
builder.RequestDelegate = CreateRouteDelegate(pageDefinition.Type);

endpoints.Add(builder.Build());
}

private static RequestDelegate CreateRouteDelegate(Type componentType)
{
return httpContext =>
{
return new RazorComponentEndpointInvoker(httpContext, componentType).RenderComponent();
};
}
}
79 changes: 0 additions & 79 deletions src/Components/Endpoints/src/RazorComponentEndpoint.cs

This file was deleted.

78 changes: 78 additions & 0 deletions src/Components/Endpoints/src/RazorComponentEndpointInvoker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Buffers;
using System.Text;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.DependencyInjection;

namespace Microsoft.AspNetCore.Components.Endpoints;

internal class RazorComponentEndpointInvoker
{
private readonly HttpContext _context;
private readonly EndpointHtmlRenderer _renderer;
private readonly Type _componentType;

public RazorComponentEndpointInvoker(HttpContext context, Type componentType)
{
_context = context;
_renderer = _context.RequestServices.GetRequiredService<EndpointHtmlRenderer>();
_componentType = componentType;
}

public Task RenderComponent()
{
return _renderer.Dispatcher.InvokeAsync(RenderComponentCore);
}

private async Task RenderComponentCore()
{
_context.Response.ContentType = RazorComponentResultExecutor.DefaultContentType;

// We could pool these dictionary instances if we wanted, and possibly even the ParameterView
// backing buffers could come from a pool like they do during rendering.
var hostParameters = ParameterView.FromDictionary(new Dictionary<string, object?>
{
{ nameof(RazorComponentEndpointHost.RenderMode), RenderMode.Static },
{ nameof(RazorComponentEndpointHost.ComponentType), _componentType },
{ nameof(RazorComponentEndpointHost.ComponentParameters), null },
});

await using var writer = CreateResponseWriter(_context.Response.Body);

// Note that we always use Static rendering mode for the top-level output from a RazorComponentResult,
// because you never want to serialize the invocation of RazorComponentResultHost. Instead, that host
// component takes care of switching into your desired render mode when it produces its own output.
var htmlContent = await _renderer.RenderEndpointComponent(
_context,
typeof(RazorComponentEndpointHost),
hostParameters,
waitForQuiescence: false);

// Importantly, we must not yield this thread (which holds exclusive access to the renderer sync context)
// in between the first call to htmlContent.WriteTo and the point where we start listening for subsequent
// streaming SSR batches (inside SendStreamingUpdatesAsync). Otherwise some other code might dispatch to the
// renderer sync context and cause a batch that would get missed.
htmlContent.WriteTo(writer, HtmlEncoder.Default); // Don't use WriteToAsync, as per the comment above

if (!htmlContent.QuiescenceTask.IsCompleted)
{
await _renderer.SendStreamingUpdatesAsync(_context, htmlContent.QuiescenceTask, writer);
}

// Invoke FlushAsync to ensure any buffered content is asynchronously written to the underlying
// response asynchronously. In the absence of this line, the buffer gets synchronously written to the
// response as part of the Dispose which has a perf impact.
await writer.FlushAsync();
}

private static TextWriter CreateResponseWriter(Stream bodyStream)
{
// Matches MVC's MemoryPoolHttpResponseStreamWriterFactory.DefaultBufferSize
const int DefaultBufferSize = 16 * 1024;
return new HttpResponseStreamWriter(bodyStream, Encoding.UTF8, DefaultBufferSize, ArrayPool<byte>.Shared, ArrayPool<char>.Shared);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,38 +54,71 @@ public async ValueTask<IHtmlAsyncContent> PrerenderComponentAsync(
_ => throw new ArgumentException(Resources.FormatUnsupportedRenderMode(prerenderMode), nameof(prerenderMode)),
};

if (waitForQuiescence)
{
// Full quiescence, i.e., all tasks completed regardless of streaming SSR
await result.QuiescenceTask;
}
else if (_nonStreamingPendingTasks.Count > 0)
{
// Just wait for quiescence of the non-streaming subtrees
await Task.WhenAll(_nonStreamingPendingTasks);
}
await WaitForResultReady(waitForQuiescence, result);

return result;
}
catch (NavigationException navigationException)
{
if (httpContext.Response.HasStarted)
{
// If we're not doing streaming SSR, this has no choice but to be a fatal error because there's no way to
// communicate the redirection to the browser.
// If we are doing streaming SSR, this should not generally happen because if you navigate during the initial
// synchronous render, the response would not yet have started, and if you do so during some later async
// phase, we would already have exited this method since streaming SSR means not awaiting quiescence.
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" +
"response and avoid using features like FlushAsync() before all components on the page have been rendered to prevent failed navigation commands.");
}
else
{
httpContext.Response.Redirect(navigationException.Location);
return PrerenderedComponentHtmlContent.Empty;
}
return await HandleNavigationException(httpContext, navigationException);
}
}

internal async ValueTask<PrerenderedComponentHtmlContent> RenderEndpointComponent(
HttpContext httpContext,
Type componentType,
ParameterView parameters,
bool waitForQuiescence)
{
await InitializeStandardComponentServicesAsync(httpContext);

try
{
var component = BeginRenderingComponent(componentType, parameters);
var result = new PrerenderedComponentHtmlContent(Dispatcher, component, null, null);

await WaitForResultReady(waitForQuiescence, result);

return result;
}
catch (NavigationException navigationException)
{
return await HandleNavigationException(httpContext, navigationException);
}
}

private async Task WaitForResultReady(bool waitForQuiescence, PrerenderedComponentHtmlContent result)
{
if (waitForQuiescence)
{
// Full quiescence, i.e., all tasks completed regardless of streaming SSR
await result.QuiescenceTask;
}
else if (_nonStreamingPendingTasks.Count > 0)
{
// Just wait for quiescence of the non-streaming subtrees
await Task.WhenAll(_nonStreamingPendingTasks);
}
}

private static ValueTask<PrerenderedComponentHtmlContent> HandleNavigationException(HttpContext httpContext, NavigationException navigationException)
{
if (httpContext.Response.HasStarted)
{
// If we're not doing streaming SSR, this has no choice but to be a fatal error because there's no way to
// communicate the redirection to the browser.
// If we are doing streaming SSR, this should not generally happen because if you navigate during the initial
// synchronous render, the response would not yet have started, and if you do so during some later async
// phase, we would already have exited this method since streaming SSR means not awaiting quiescence.
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" +
"response and avoid using features like FlushAsync() before all components on the page have been rendered to prevent failed navigation commands.");
}
else
{
httpContext.Response.Redirect(navigationException.Location);
return new ValueTask<PrerenderedComponentHtmlContent>(PrerenderedComponentHtmlContent.Empty);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Buffers;
using System.Text;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.DependencyInjection;

namespace Microsoft.AspNetCore.Components.Endpoints;

Expand Down Expand Up @@ -30,11 +35,67 @@ public virtual Task ExecuteAsync(HttpContext httpContext, RazorComponentResult r
response.StatusCode = result.StatusCode.Value;
}

return RazorComponentEndpoint.RenderComponentToResponse(
return RenderComponentToResponse(
httpContext,
result.RenderMode,
result.ComponentType,
result.Parameters,
result.PreventStreamingRendering);
}

internal static Task RenderComponentToResponse(
HttpContext httpContext,
RenderMode renderMode,
Type componentType,
IReadOnlyDictionary<string, object?>? componentParameters,
bool preventStreamingRendering)
{
var endpointHtmlRenderer = httpContext.RequestServices.GetRequiredService<EndpointHtmlRenderer>();
return endpointHtmlRenderer.Dispatcher.InvokeAsync(async () =>
{
// We could pool these dictionary instances if we wanted, and possibly even the ParameterView
// backing buffers could come from a pool like they do during rendering.
var hostParameters = ParameterView.FromDictionary(new Dictionary<string, object?>
{
{ nameof(RazorComponentEndpointHost.RenderMode), renderMode },
{ nameof(RazorComponentEndpointHost.ComponentType), componentType },
{ nameof(RazorComponentEndpointHost.ComponentParameters), componentParameters },
});

await using var writer = CreateResponseWriter(httpContext.Response.Body);

// Note that we always use Static rendering mode for the top-level output from a RazorComponentResult,
// because you never want to serialize the invocation of RazorComponentResultHost. Instead, that host
// component takes care of switching into your desired render mode when it produces its own output.
var htmlContent = (EndpointHtmlRenderer.PrerenderedComponentHtmlContent)(await endpointHtmlRenderer.PrerenderComponentAsync(
httpContext,
typeof(RazorComponentEndpointHost),
RenderMode.Static,
hostParameters,
waitForQuiescence: preventStreamingRendering));

// Importantly, we must not yield this thread (which holds exclusive access to the renderer sync context)
// in between the first call to htmlContent.WriteTo and the point where we start listening for subsequent
// streaming SSR batches (inside SendStreamingUpdatesAsync). Otherwise some other code might dispatch to the
// renderer sync context and cause a batch that would get missed.
htmlContent.WriteTo(writer, HtmlEncoder.Default); // Don't use WriteToAsync, as per the comment above

if (!htmlContent.QuiescenceTask.IsCompleted)
{
await endpointHtmlRenderer.SendStreamingUpdatesAsync(httpContext, htmlContent.QuiescenceTask, writer);
}

// Invoke FlushAsync to ensure any buffered content is asynchronously written to the underlying
// response asynchronously. In the absence of this line, the buffer gets synchronously written to the
// response as part of the Dispose which has a perf impact.
await writer.FlushAsync();
});
}

private static TextWriter CreateResponseWriter(Stream bodyStream)
{
// Matches MVC's MemoryPoolHttpResponseStreamWriterFactory.DefaultBufferSize
const int DefaultBufferSize = 16 * 1024;
return new HttpResponseStreamWriter(bodyStream, Encoding.UTF8, DefaultBufferSize, ArrayPool<byte>.Shared, ArrayPool<char>.Shared);
}
}
Loading