Skip to content

Commit 6f0e422

Browse files
Razor Component endpoint implementation (#47117)
1 parent 4c17c74 commit 6f0e422

File tree

58 files changed

+686
-567
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+686
-567
lines changed

src/Components/Analyzers/test/Microsoft.AspNetCore.Components.Analyzers.Tests.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,6 @@
1717
</ItemGroup>
1818

1919
<ItemGroup>
20-
<Content Include="TestFiles\**\*.*" CopyToPublishDirectory="PreserveNewest" />
20+
<Content Include="TestFiles\**\*.*" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
2121
</ItemGroup>
2222
</Project>

src/Components/Components.slnf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"src\\Components\\QuickGrid\\Microsoft.AspNetCore.Components.QuickGrid.EntityFrameworkAdapter\\src\\Microsoft.AspNetCore.Components.QuickGrid.EntityFrameworkAdapter.csproj",
2020
"src\\Components\\QuickGrid\\Microsoft.AspNetCore.Components.QuickGrid\\src\\Microsoft.AspNetCore.Components.QuickGrid.csproj",
2121
"src\\Components\\Samples\\BlazorServerApp\\BlazorServerApp.csproj",
22+
"src\\Components\\Samples\\BlazorUnitedApp\\BlazorUnitedApp.csproj",
2223
"src\\Components\\Server\\src\\Microsoft.AspNetCore.Components.Server.csproj",
2324
"src\\Components\\Server\\test\\Microsoft.AspNetCore.Components.Server.Tests.csproj",
2425
"src\\Components\\WebAssembly\\Authentication.Msal\\src\\Microsoft.Authentication.WebAssembly.Msal.csproj",

src/Components/Endpoints/src/Builder/RazorComponentEndpointDataSource.cs

Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Linq;
66
using System.Reflection;
77
using Microsoft.AspNetCore.Components;
8+
using Microsoft.AspNetCore.Components.Endpoints;
89
using Microsoft.AspNetCore.Http;
910
using Microsoft.AspNetCore.Routing;
1011
using Microsoft.AspNetCore.Routing.Patterns;
@@ -80,7 +81,7 @@ private void UpdateEndpoints()
8081
{
8182
// TODO: Proper endpoint definition https://github.com/dotnet/aspnetcore/issues/46985
8283
var endpoint = new RouteEndpoint(
83-
CreateRouteDelegate(type),
84+
RazorComponentEndpoint.CreateRouteDelegate(type),
8485
RoutePatternFactory.Parse(route!.Template),
8586
order: 0,
8687
new EndpointMetadataCollection(type.GetCustomAttributes(inherit: true)),
@@ -91,31 +92,6 @@ private void UpdateEndpoints()
9192
_endpoints = endpoints;
9293
}
9394

94-
private static RequestDelegate CreateRouteDelegate(Type type)
95-
{
96-
// TODO: Proper endpoint implementation https://github.com/dotnet/aspnetcore/issues/46988
97-
return (ctx) =>
98-
{
99-
ctx.Response.StatusCode = 200;
100-
ctx.Response.ContentType = "text/html; charset=utf-8";
101-
return ctx.Response.WriteAsync($"""
102-
<!DOCTYPE html>
103-
<html lang="en">
104-
<head>
105-
<meta charset="UTF-8">
106-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
107-
<meta http-equiv="X-UA-Compatible" content="ie=edge">
108-
<title>{type.FullName}</title>
109-
<link rel="stylesheet" href="style.css">
110-
</head>
111-
<body>
112-
<p>{type.FullName}</p>
113-
</body>
114-
</html>
115-
""");
116-
};
117-
}
118-
11995
public override IChangeToken GetChangeToken()
12096
{
12197
// TODO: Handle updates if necessary (for hot reload).

src/Mvc/Mvc.ViewFeatures/src/RazorComponents/ComponentPrerenderer.cs renamed to src/Components/Endpoints/src/DependencyInjection/ComponentPrerenderer.cs

Lines changed: 103 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,38 +2,37 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Text.Encodings.Web;
5-
using Microsoft.AspNetCore.Components;
65
using Microsoft.AspNetCore.Components.Authorization;
76
using Microsoft.AspNetCore.Components.Infrastructure;
87
using Microsoft.AspNetCore.Components.Routing;
98
using Microsoft.AspNetCore.Components.Web;
9+
using Microsoft.AspNetCore.DataProtection;
1010
using Microsoft.AspNetCore.Html;
1111
using Microsoft.AspNetCore.Http;
1212
using Microsoft.AspNetCore.Http.Extensions;
13-
using Microsoft.AspNetCore.Mvc.Rendering;
1413
using Microsoft.Extensions.DependencyInjection;
1514

16-
namespace Microsoft.AspNetCore.Mvc.ViewFeatures;
15+
namespace Microsoft.AspNetCore.Components.Endpoints;
1716

1817
// Wraps the public HtmlRenderer APIs so that the output also gets annotated with prerendering markers.
1918
// This allows the prerendered content to switch later into interactive mode.
2019
// This class also deals with initializing the standard component DI services once per request.
21-
internal sealed class ComponentPrerenderer
20+
internal sealed class ComponentPrerenderer : IComponentPrerenderer
2221
{
2322
private static readonly object ComponentSequenceKey = new object();
2423
private static readonly object InvokedRenderModesKey = new object();
2524

2625
private readonly HtmlRenderer _htmlRenderer;
27-
private readonly ServerComponentSerializer _serverComponentSerializer;
26+
private readonly IServiceProvider _services;
2827
private readonly object _servicesInitializedLock = new();
29-
private Task _servicesInitializedTask;
28+
private Task? _servicesInitializedTask;
3029

3130
public ComponentPrerenderer(
3231
HtmlRenderer htmlRenderer,
33-
ServerComponentSerializer serverComponentSerializer)
32+
IServiceProvider services)
3433
{
35-
_serverComponentSerializer = serverComponentSerializer;
3634
_htmlRenderer = htmlRenderer;
35+
_services = services;
3736
}
3837

3938
public Dispatcher Dispatcher => _htmlRenderer.Dispatcher;
@@ -72,6 +71,46 @@ public async ValueTask<IHtmlAsyncContent> PrerenderComponentAsync(
7271
};
7372
}
7473

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+
75114
private async ValueTask<HtmlComponent> PrerenderComponentCoreAsync(
76115
ParameterView parameters,
77116
HttpContext httpContext,
@@ -108,7 +147,7 @@ private static ServerComponentInvocationSequence GetOrCreateInvocationId(HttpCon
108147
httpContext.Items[ComponentSequenceKey] = result;
109148
}
110149

111-
return (ServerComponentInvocationSequence)result;
150+
return (ServerComponentInvocationSequence)result!;
112151
}
113152

114153
// Internal for test only
@@ -131,7 +170,7 @@ internal static void UpdateSaveStateRenderMode(HttpContext httpContext, RenderMo
131170
InvokedRenderModes.Mode.Server :
132171
InvokedRenderModes.Mode.WebAssembly;
133172

134-
var invokedMode = (InvokedRenderModes)result;
173+
var invokedMode = (InvokedRenderModes)result!;
135174
if (invokedMode.Value != currentInvocation)
136175
{
137176
invokedMode.Value = InvokedRenderModes.Mode.ServerAndWebAssembly;
@@ -142,14 +181,9 @@ internal static void UpdateSaveStateRenderMode(HttpContext httpContext, RenderMo
142181

143182
internal static InvokedRenderModes.Mode GetPersistStateRenderMode(HttpContext httpContext)
144183
{
145-
if (httpContext.Items.TryGetValue(InvokedRenderModesKey, out var result))
146-
{
147-
return ((InvokedRenderModes)result).Value;
148-
}
149-
else
150-
{
151-
return InvokedRenderModes.Mode.None;
152-
}
184+
return httpContext.Items.TryGetValue(InvokedRenderModesKey, out var result)
185+
? ((InvokedRenderModes)result!).Value
186+
: InvokedRenderModes.Mode.None;
153187
}
154188

155189
private async ValueTask<IHtmlAsyncContent> StaticComponentAsync(HttpContext context, Type type, ParameterView parametersCollection)
@@ -168,7 +202,11 @@ private async Task<IHtmlAsyncContent> PrerenderedServerComponentAsync(HttpContex
168202
context.Response.Headers.CacheControl = "no-cache, no-store, max-age=0";
169203
}
170204

171-
var marker = _serverComponentSerializer.SerializeInvocation(
205+
// Lazy because we don't actually want to require a whole chain of services including Data Protection
206+
// to be required unless you actually use Server render mode.
207+
var serverComponentSerializer = _services.GetRequiredService<ServerComponentSerializer>();
208+
209+
var marker = serverComponentSerializer.SerializeInvocation(
172210
invocationId,
173211
type,
174212
parametersCollection,
@@ -204,7 +242,11 @@ private IHtmlAsyncContent NonPrerenderedServerComponent(HttpContext context, Ser
204242
context.Response.Headers.CacheControl = "no-cache, no-store, max-age=0";
205243
}
206244

207-
var marker = _serverComponentSerializer.SerializeInvocation(invocationId, type, parametersCollection, prerendered: false);
245+
// Lazy because we don't actually want to require a whole chain of services including Data Protection
246+
// to be required unless you actually use Server render mode.
247+
var serverComponentSerializer = _services.GetRequiredService<ServerComponentSerializer>();
248+
249+
var marker = serverComponentSerializer.SerializeInvocation(invocationId, type, parametersCollection, prerendered: false);
208250
return new PrerenderedComponentHtmlContent(null, null, marker, null);
209251
}
210252

@@ -216,16 +258,16 @@ private static IHtmlAsyncContent NonPrerenderedWebAssemblyComponent(Type type, P
216258

217259
// An implementation of IHtmlContent that holds a reference to a component until we're ready to emit it as HTML to the response.
218260
// We don't construct the actual HTML until we receive the call to WriteTo.
219-
private class PrerenderedComponentHtmlContent : IHtmlContent, IHtmlAsyncContent
261+
private class PrerenderedComponentHtmlContent : IHtmlAsyncContent
220262
{
221-
private readonly Dispatcher _dispatcher;
222-
private readonly HtmlComponent _htmlToEmitOrNull;
263+
private readonly Dispatcher? _dispatcher;
264+
private readonly HtmlComponent? _htmlToEmitOrNull;
223265
private readonly ServerComponentMarker? _serverMarker;
224266
private readonly WebAssemblyComponentMarker? _webAssemblyMarker;
225267

226268
public PrerenderedComponentHtmlContent(
227-
Dispatcher dispatcher,
228-
HtmlComponent htmlToEmitOrNull, // If null, we're only emitting the markers
269+
Dispatcher? dispatcher, // If null, we're only emitting the markers
270+
HtmlComponent? htmlToEmitOrNull, // If null, we're only emitting the markers
229271
ServerComponentMarker? serverMarker,
230272
WebAssemblyComponentMarker? webAssemblyMarker)
231273
{
@@ -235,9 +277,18 @@ public PrerenderedComponentHtmlContent(
235277
_webAssemblyMarker = webAssemblyMarker;
236278
}
237279

238-
// For back-compat, we have to supply an implemention of IHtmlContent. However this will only work
239-
// if you call it on the HtmlRenderer's sync context. The framework itself will not call this directly
240-
// and will instead use WriteToAsync which deals with dispatching to the sync context.
280+
public async ValueTask WriteToAsync(TextWriter writer)
281+
{
282+
if (_dispatcher is null)
283+
{
284+
WriteTo(writer, HtmlEncoder.Default);
285+
}
286+
else
287+
{
288+
await _dispatcher.InvokeAsync(() => WriteTo(writer, HtmlEncoder.Default));
289+
}
290+
}
291+
241292
public void WriteTo(TextWriter writer, HtmlEncoder encoder)
242293
{
243294
if (_serverMarker.HasValue)
@@ -263,18 +314,6 @@ public void WriteTo(TextWriter writer, HtmlEncoder encoder)
263314
}
264315
}
265316
}
266-
267-
public async ValueTask WriteToAsync(TextWriter writer)
268-
{
269-
if (_dispatcher is null)
270-
{
271-
WriteTo(writer, HtmlEncoder.Default);
272-
}
273-
else
274-
{
275-
await _dispatcher.InvokeAsync(() => WriteTo(writer, HtmlEncoder.Default));
276-
}
277-
}
278317
}
279318

280319
private static async Task InitializeStandardComponentServicesAsync(HttpContext httpContext)
@@ -313,4 +352,28 @@ private static string GetContextBaseUri(HttpRequest request)
313352
// it has to end with a trailing slash
314353
return result.EndsWith('/') ? result : result += "/";
315354
}
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+
}
316379
}

src/Mvc/Mvc.ViewFeatures/src/Infrastructure/HttpNavigationManager.cs renamed to src/Components/Endpoints/src/DependencyInjection/HttpNavigationManager.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
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 Microsoft.AspNetCore.Components;
54
using Microsoft.AspNetCore.Components.Routing;
65

7-
namespace Microsoft.AspNetCore.Mvc.ViewFeatures;
6+
namespace Microsoft.AspNetCore.Components.Endpoints;
87

98
internal sealed class HttpNavigationManager : NavigationManager, IHostEnvironmentNavigationManager
109
{
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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 Microsoft.AspNetCore.Html;
5+
using Microsoft.AspNetCore.Http;
6+
7+
namespace Microsoft.AspNetCore.Components.Endpoints;
8+
9+
/// <summary>
10+
/// A service that can prerender Razor Components as HTML.
11+
/// </summary>
12+
public interface IComponentPrerenderer
13+
{
14+
/// <summary>
15+
/// Prerenders a Razor Component as HTML.
16+
/// </summary>
17+
/// <param name="httpContext">The <see cref="HttpContext"/>.</param>
18+
/// <param name="componentType">The type of component to prerender. This must implement <see cref="IComponent"/>.</param>
19+
/// <param name="renderMode">The mode in which to prerender the component.</param>
20+
/// <param name="parameters">Parameters for the component.</param>
21+
/// <returns>A task that completes with the prerendered content.</returns>
22+
ValueTask<IHtmlAsyncContent> PrerenderComponentAsync(
23+
HttpContext httpContext,
24+
Type componentType,
25+
RenderMode renderMode,
26+
ParameterView parameters);
27+
28+
/// <summary>
29+
/// Prepares a serialized representation of any component state that is persistible within the current request.
30+
/// </summary>
31+
/// <param name="httpContext">The <see cref="HttpContext"/>.</param>
32+
/// <param name="serializationMode">The <see cref="PersistedStateSerializationMode"/>.</param>
33+
/// <returns>A task that completes with the prerendered state content.</returns>
34+
ValueTask<IHtmlContent> PrerenderPersistedStateAsync(
35+
HttpContext httpContext,
36+
PersistedStateSerializationMode serializationMode);
37+
38+
/// <summary>
39+
/// Gets a <see cref="Dispatcher"/> that should be used for calls to <see cref="PrerenderComponentAsync(HttpContext, Type, RenderMode, ParameterView)"/>.
40+
/// </summary>
41+
Dispatcher Dispatcher { get; }
42+
}

src/Components/Endpoints/src/DependencyInjection/IRazorComponentsBuilder.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
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-
namespace Microsoft.Extensions.DependencyInjection;
4+
using Microsoft.Extensions.DependencyInjection;
5+
6+
namespace Microsoft.AspNetCore.Components.Endpoints;
57

68
/// <summary>
79
///

src/Mvc/Mvc.ViewFeatures/src/RazorComponents/InvokedRenderModes.cs renamed to src/Components/Endpoints/src/DependencyInjection/InvokedRenderModes.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
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 Microsoft.AspNetCore.Mvc.Rendering;
5-
6-
namespace Microsoft.AspNetCore.Mvc.ViewFeatures;
4+
namespace Microsoft.AspNetCore.Components.Endpoints;
75

86
internal sealed class InvokedRenderModes
97
{

0 commit comments

Comments
 (0)