Skip to content

Commit b40cc0b

Browse files
Refactor HtmlRenderer layering (#47404)
1 parent 453ceec commit b40cc0b

14 files changed

+263
-228
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,18 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Diagnostics.CodeAnalysis;
45
using System.Text.Encodings.Web;
5-
using Microsoft.AspNetCore.Components.Authorization;
6-
using Microsoft.AspNetCore.Components.Infrastructure;
7-
using Microsoft.AspNetCore.Components.Routing;
86
using Microsoft.AspNetCore.Components.Web;
9-
using Microsoft.AspNetCore.DataProtection;
107
using Microsoft.AspNetCore.Html;
118
using Microsoft.AspNetCore.Http;
12-
using Microsoft.AspNetCore.Http.Extensions;
139
using Microsoft.Extensions.DependencyInjection;
1410

1511
namespace Microsoft.AspNetCore.Components.Endpoints;
1612

17-
// Wraps the public HtmlRenderer APIs so that the output also gets annotated with prerendering markers.
18-
// This allows the prerendered content to switch later into interactive mode.
19-
// This class also deals with initializing the standard component DI services once per request.
20-
internal sealed class ComponentPrerenderer : IComponentPrerenderer
13+
internal sealed partial class EndpointHtmlRenderer
2114
{
2215
private static readonly object ComponentSequenceKey = new object();
23-
private static readonly object InvokedRenderModesKey = new object();
24-
25-
private readonly HtmlRenderer _htmlRenderer;
26-
private readonly IServiceProvider _services;
27-
private readonly object _servicesInitializedLock = new();
28-
private Task? _servicesInitializedTask;
29-
30-
public ComponentPrerenderer(
31-
HtmlRenderer htmlRenderer,
32-
IServiceProvider services)
33-
{
34-
_htmlRenderer = htmlRenderer;
35-
_services = services;
36-
}
37-
38-
public Dispatcher Dispatcher => _htmlRenderer.Dispatcher;
3916

4017
public async ValueTask<IHtmlAsyncContent> PrerenderComponentAsync(
4118
HttpContext httpContext,
@@ -52,10 +29,8 @@ public async ValueTask<IHtmlAsyncContent> PrerenderComponentAsync(
5229
}
5330

5431
// Make sure we only initialize the services once, but on every call we wait for that process to complete
55-
lock (_servicesInitializedLock)
56-
{
57-
_servicesInitializedTask ??= InitializeStandardComponentServicesAsync(httpContext);
58-
}
32+
// This does not have to be threadsafe since it's not valid to call this simultaneously from multiple threads.
33+
_servicesInitializedTask ??= InitializeStandardComponentServicesAsync(httpContext);
5934
await _servicesInitializedTask;
6035

6136
UpdateSaveStateRenderMode(httpContext, prerenderMode);
@@ -71,55 +46,19 @@ public async ValueTask<IHtmlAsyncContent> PrerenderComponentAsync(
7146
};
7247
}
7348

74-
public async ValueTask<IHtmlContent> PrerenderPersistedStateAsync(HttpContext httpContext, PersistedStateSerializationMode serializationMode)
75-
{
76-
// First we resolve "infer" mode to a specific mode
77-
if (serializationMode == PersistedStateSerializationMode.Infer)
78-
{
79-
switch (GetPersistStateRenderMode(httpContext))
80-
{
81-
case InvokedRenderModes.Mode.None:
82-
return ComponentStateHtmlContent.Empty;
83-
case InvokedRenderModes.Mode.ServerAndWebAssembly:
84-
throw new InvalidOperationException(
85-
Resources.FailedToInferComponentPersistenceMode);
86-
case InvokedRenderModes.Mode.Server:
87-
serializationMode = PersistedStateSerializationMode.Server;
88-
break;
89-
case InvokedRenderModes.Mode.WebAssembly:
90-
serializationMode = PersistedStateSerializationMode.WebAssembly;
91-
break;
92-
default:
93-
throw new InvalidOperationException("Invalid persistence mode");
94-
}
95-
}
96-
97-
// Now given the mode, we obtain a particular store for that mode
98-
var store = serializationMode switch
99-
{
100-
PersistedStateSerializationMode.Server =>
101-
new ProtectedPrerenderComponentApplicationStore(httpContext.RequestServices.GetRequiredService<IDataProtectionProvider>()),
102-
PersistedStateSerializationMode.WebAssembly =>
103-
new PrerenderComponentApplicationStore(),
104-
_ =>
105-
throw new InvalidOperationException("Invalid persistence mode.")
106-
};
107-
108-
// Finally, persist the state and return the HTML content
109-
var manager = httpContext.RequestServices.GetRequiredService<ComponentStatePersistenceManager>();
110-
await manager.PersistStateAsync(store, _htmlRenderer.Dispatcher);
111-
return new ComponentStateHtmlContent(store);
112-
}
113-
11449
private async ValueTask<HtmlComponent> PrerenderComponentCoreAsync(
11550
ParameterView parameters,
11651
HttpContext httpContext,
117-
Type componentType)
52+
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type componentType)
11853
{
11954
try
12055
{
121-
return await _htmlRenderer.Dispatcher.InvokeAsync(() =>
122-
_htmlRenderer.RenderComponentAsync(componentType, parameters));
56+
return await Dispatcher.InvokeAsync(async () =>
57+
{
58+
var content = BeginRenderingComponent(componentType, parameters);
59+
await content.WaitForQuiescenceAsync();
60+
return content;
61+
});
12362
}
12463
catch (NavigationException navigationException)
12564
{
@@ -150,49 +89,13 @@ private static ServerComponentInvocationSequence GetOrCreateInvocationId(HttpCon
15089
return (ServerComponentInvocationSequence)result!;
15190
}
15291

153-
// Internal for test only
154-
internal static void UpdateSaveStateRenderMode(HttpContext httpContext, RenderMode mode)
155-
{
156-
// TODO: This will all have to change when we support multiple render modes in the same response
157-
if (mode == RenderMode.ServerPrerendered || mode == RenderMode.WebAssemblyPrerendered)
158-
{
159-
if (!httpContext.Items.TryGetValue(InvokedRenderModesKey, out var result))
160-
{
161-
result = new InvokedRenderModes(mode is RenderMode.ServerPrerendered ?
162-
InvokedRenderModes.Mode.Server :
163-
InvokedRenderModes.Mode.WebAssembly);
164-
165-
httpContext.Items[InvokedRenderModesKey] = result;
166-
}
167-
else
168-
{
169-
var currentInvocation = mode is RenderMode.ServerPrerendered ?
170-
InvokedRenderModes.Mode.Server :
171-
InvokedRenderModes.Mode.WebAssembly;
172-
173-
var invokedMode = (InvokedRenderModes)result!;
174-
if (invokedMode.Value != currentInvocation)
175-
{
176-
invokedMode.Value = InvokedRenderModes.Mode.ServerAndWebAssembly;
177-
}
178-
}
179-
}
180-
}
181-
182-
internal static InvokedRenderModes.Mode GetPersistStateRenderMode(HttpContext httpContext)
183-
{
184-
return httpContext.Items.TryGetValue(InvokedRenderModesKey, out var result)
185-
? ((InvokedRenderModes)result!).Value
186-
: InvokedRenderModes.Mode.None;
187-
}
188-
18992
private async ValueTask<IHtmlAsyncContent> StaticComponentAsync(HttpContext context, Type type, ParameterView parametersCollection)
19093
{
19194
var htmlComponent = await PrerenderComponentCoreAsync(
19295
parametersCollection,
19396
context,
19497
type);
195-
return new PrerenderedComponentHtmlContent(_htmlRenderer.Dispatcher, htmlComponent, null, null);
98+
return new PrerenderedComponentHtmlContent(Dispatcher, htmlComponent, null, null);
19699
}
197100

198101
private async Task<IHtmlAsyncContent> PrerenderedServerComponentAsync(HttpContext context, ServerComponentInvocationSequence invocationId, Type type, ParameterView parametersCollection)
@@ -217,7 +120,7 @@ private async Task<IHtmlAsyncContent> PrerenderedServerComponentAsync(HttpContex
217120
context,
218121
type);
219122

220-
return new PrerenderedComponentHtmlContent(_htmlRenderer.Dispatcher, htmlComponent, marker, null);
123+
return new PrerenderedComponentHtmlContent(Dispatcher, htmlComponent, marker, null);
221124
}
222125

223126
private async ValueTask<IHtmlAsyncContent> PrerenderedWebAssemblyComponentAsync(HttpContext context, Type type, ParameterView parametersCollection)
@@ -232,7 +135,7 @@ private async ValueTask<IHtmlAsyncContent> PrerenderedWebAssemblyComponentAsync(
232135
context,
233136
type);
234137

235-
return new PrerenderedComponentHtmlContent(_htmlRenderer.Dispatcher, htmlComponent, null, marker);
138+
return new PrerenderedComponentHtmlContent(Dispatcher, htmlComponent, null, marker);
236139
}
237140

238141
private IHtmlAsyncContent NonPrerenderedServerComponent(HttpContext context, ServerComponentInvocationSequence invocationId, Type type, ParameterView parametersCollection)
@@ -315,65 +218,4 @@ public void WriteTo(TextWriter writer, HtmlEncoder encoder)
315218
}
316219
}
317220
}
318-
319-
private static async Task InitializeStandardComponentServicesAsync(HttpContext httpContext)
320-
{
321-
var navigationManager = (IHostEnvironmentNavigationManager)httpContext.RequestServices.GetRequiredService<NavigationManager>();
322-
navigationManager?.Initialize(GetContextBaseUri(httpContext.Request), GetFullUri(httpContext.Request));
323-
324-
var authenticationStateProvider = httpContext.RequestServices.GetService<AuthenticationStateProvider>() as IHostEnvironmentAuthenticationStateProvider;
325-
if (authenticationStateProvider != null)
326-
{
327-
var authenticationState = new AuthenticationState(httpContext.User);
328-
authenticationStateProvider.SetAuthenticationState(Task.FromResult(authenticationState));
329-
}
330-
331-
// It's important that this is initialized since a component might try to restore state during prerendering
332-
// (which will obviously not work, but should not fail)
333-
var componentApplicationLifetime = httpContext.RequestServices.GetRequiredService<ComponentStatePersistenceManager>();
334-
await componentApplicationLifetime.RestoreStateAsync(new PrerenderComponentApplicationStore());
335-
}
336-
337-
private static string GetFullUri(HttpRequest request)
338-
{
339-
return UriHelper.BuildAbsolute(
340-
request.Scheme,
341-
request.Host,
342-
request.PathBase,
343-
request.Path,
344-
request.QueryString);
345-
}
346-
347-
private static string GetContextBaseUri(HttpRequest request)
348-
{
349-
var result = UriHelper.BuildAbsolute(request.Scheme, request.Host, request.PathBase);
350-
351-
// PathBase may be "/" or "/some/thing", but to be a well-formed base URI
352-
// it has to end with a trailing slash
353-
return result.EndsWith('/') ? result : result += "/";
354-
}
355-
356-
private sealed class ComponentStateHtmlContent : IHtmlContent
357-
{
358-
private PrerenderComponentApplicationStore? _store;
359-
360-
public static ComponentStateHtmlContent Empty { get; }
361-
= new ComponentStateHtmlContent(null);
362-
363-
public ComponentStateHtmlContent(PrerenderComponentApplicationStore? store)
364-
{
365-
_store = store;
366-
}
367-
368-
public void WriteTo(TextWriter writer, HtmlEncoder encoder)
369-
{
370-
if (_store != null)
371-
{
372-
writer.Write("<!--Blazor-Component-State:");
373-
writer.Write(_store.PersistedState);
374-
writer.Write("-->");
375-
_store = null;
376-
}
377-
}
378-
}
379221
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Text.Encodings.Web;
5+
using Microsoft.AspNetCore.Components.Infrastructure;
6+
using Microsoft.AspNetCore.DataProtection;
7+
using Microsoft.AspNetCore.Html;
8+
using Microsoft.AspNetCore.Http;
9+
using Microsoft.Extensions.DependencyInjection;
10+
11+
namespace Microsoft.AspNetCore.Components.Endpoints;
12+
13+
internal sealed partial class EndpointHtmlRenderer
14+
{
15+
private static readonly object InvokedRenderModesKey = new object();
16+
17+
public async ValueTask<IHtmlContent> PrerenderPersistedStateAsync(HttpContext httpContext, PersistedStateSerializationMode serializationMode)
18+
{
19+
// First we resolve "infer" mode to a specific mode
20+
if (serializationMode == PersistedStateSerializationMode.Infer)
21+
{
22+
switch (GetPersistStateRenderMode(httpContext))
23+
{
24+
case InvokedRenderModes.Mode.None:
25+
return ComponentStateHtmlContent.Empty;
26+
case InvokedRenderModes.Mode.ServerAndWebAssembly:
27+
throw new InvalidOperationException(
28+
Resources.FailedToInferComponentPersistenceMode);
29+
case InvokedRenderModes.Mode.Server:
30+
serializationMode = PersistedStateSerializationMode.Server;
31+
break;
32+
case InvokedRenderModes.Mode.WebAssembly:
33+
serializationMode = PersistedStateSerializationMode.WebAssembly;
34+
break;
35+
default:
36+
throw new InvalidOperationException("Invalid persistence mode");
37+
}
38+
}
39+
40+
// Now given the mode, we obtain a particular store for that mode
41+
var store = serializationMode switch
42+
{
43+
PersistedStateSerializationMode.Server =>
44+
new ProtectedPrerenderComponentApplicationStore(httpContext.RequestServices.GetRequiredService<IDataProtectionProvider>()),
45+
PersistedStateSerializationMode.WebAssembly =>
46+
new PrerenderComponentApplicationStore(),
47+
_ =>
48+
throw new InvalidOperationException("Invalid persistence mode.")
49+
};
50+
51+
// Finally, persist the state and return the HTML content
52+
var manager = httpContext.RequestServices.GetRequiredService<ComponentStatePersistenceManager>();
53+
await manager.PersistStateAsync(store, Dispatcher);
54+
return new ComponentStateHtmlContent(store);
55+
}
56+
57+
// Internal for test only
58+
internal static void UpdateSaveStateRenderMode(HttpContext httpContext, RenderMode mode)
59+
{
60+
// TODO: This will all have to change when we support multiple render modes in the same response
61+
if (mode == RenderMode.ServerPrerendered || mode == RenderMode.WebAssemblyPrerendered)
62+
{
63+
if (!httpContext.Items.TryGetValue(InvokedRenderModesKey, out var result))
64+
{
65+
result = new InvokedRenderModes(mode is RenderMode.ServerPrerendered ?
66+
InvokedRenderModes.Mode.Server :
67+
InvokedRenderModes.Mode.WebAssembly);
68+
69+
httpContext.Items[InvokedRenderModesKey] = result;
70+
}
71+
else
72+
{
73+
var currentInvocation = mode is RenderMode.ServerPrerendered ?
74+
InvokedRenderModes.Mode.Server :
75+
InvokedRenderModes.Mode.WebAssembly;
76+
77+
var invokedMode = (InvokedRenderModes)result!;
78+
if (invokedMode.Value != currentInvocation)
79+
{
80+
invokedMode.Value = InvokedRenderModes.Mode.ServerAndWebAssembly;
81+
}
82+
}
83+
}
84+
}
85+
86+
internal static InvokedRenderModes.Mode GetPersistStateRenderMode(HttpContext httpContext)
87+
{
88+
return httpContext.Items.TryGetValue(InvokedRenderModesKey, out var result)
89+
? ((InvokedRenderModes)result!).Value
90+
: InvokedRenderModes.Mode.None;
91+
}
92+
93+
private sealed class ComponentStateHtmlContent : IHtmlContent
94+
{
95+
private PrerenderComponentApplicationStore? _store;
96+
97+
public static ComponentStateHtmlContent Empty { get; }
98+
= new ComponentStateHtmlContent(null);
99+
100+
public ComponentStateHtmlContent(PrerenderComponentApplicationStore? store)
101+
{
102+
_store = store;
103+
}
104+
105+
public void WriteTo(TextWriter writer, HtmlEncoder encoder)
106+
{
107+
if (_store != null)
108+
{
109+
writer.Write("<!--Blazor-Component-State:");
110+
writer.Write(_store.PersistedState);
111+
writer.Write("-->");
112+
_store = null;
113+
}
114+
}
115+
}
116+
}

0 commit comments

Comments
 (0)