Skip to content

Commit 5f081b8

Browse files
Provide a better error (#50311)
1 parent 08cde4d commit 5f081b8

7 files changed

+188
-12
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
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+
namespace Microsoft.AspNetCore.Components.Endpoints;
5+
6+
internal class ConfiguredRenderModesMetadata(IComponentRenderMode[] configuredRenderModes)
7+
{
8+
public IComponentRenderMode[] ConfiguredRenderModes => configuredRenderModes;
9+
}

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,10 +98,12 @@ private void UpdateEndpoints()
9898
{
9999
var endpoints = new List<Endpoint>();
100100
var context = _builder.Build();
101+
var configuredRenderModesMetadata = new ConfiguredRenderModesMetadata(
102+
Options.ConfiguredRenderModes.ToArray());
101103

102104
foreach (var definition in context.Pages)
103105
{
104-
_factory.AddEndpoints(endpoints, typeof(TRootComponent), definition, _conventions, _finallyConventions);
106+
_factory.AddEndpoints(endpoints, typeof(TRootComponent), definition, _conventions, _finallyConventions, configuredRenderModesMetadata);
105107
}
106108

107109
ICollection<IComponentRenderMode> renderModes = Options.ConfiguredRenderModes;
@@ -127,8 +129,8 @@ private void UpdateEndpoints()
127129
if (!found)
128130
{
129131
throw new InvalidOperationException($"Unable to find a provider for the render mode: {renderMode.GetType().FullName}. This generally " +
130-
$"means that a call to 'AddWebAssemblyComponents' or 'AddServerComponents' is missing. " +
131-
$"Alternatively call 'AddWebAssemblyRenderMode', 'AddServerRenderMode' might be missing if you have set UseDeclaredRenderModes = false.");
132+
"means that a call to 'AddWebAssemblyComponents' or 'AddServerComponents' is missing. " +
133+
"For example, change builder.Services.AddRazorComponents() to builder.Services.AddRazorComponents().AddServerComponents().");
132134
}
133135
}
134136

src/Components/Endpoints/src/Builder/RazorComponentEndpointFactory.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ internal void AddEndpoints(
2424
[DynamicallyAccessedMembers(Component)] Type rootComponent,
2525
PageComponentInfo pageDefinition,
2626
IReadOnlyList<Action<EndpointBuilder>> conventions,
27-
IReadOnlyList<Action<EndpointBuilder>> finallyConventions)
27+
IReadOnlyList<Action<EndpointBuilder>> finallyConventions,
28+
ConfiguredRenderModesMetadata configuredRenderModesMetadata)
2829
{
2930
// We do not provide a way to establish the order or the name for the page routes.
3031
// Order is not supported in our client router.
@@ -48,6 +49,7 @@ internal void AddEndpoints(
4849
builder.Metadata.Add(HttpMethodsMetadata);
4950
builder.Metadata.Add(new ComponentTypeMetadata(pageDefinition.Type));
5051
builder.Metadata.Add(new RootComponentMetadata(rootComponent));
52+
builder.Metadata.Add(configuredRenderModesMetadata);
5153

5254
foreach (var convention in conventions)
5355
{

src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ protected override IComponent ResolveComponentForRenderMode([DynamicallyAccessed
2929
else
3030
{
3131
// This component is the start of a subtree with a rendermode, so introduce a new rendermode boundary here
32-
return new SSRRenderModeBoundary(componentType, renderMode);
32+
return new SSRRenderModeBoundary(_httpContext, componentType, renderMode);
3333
}
3434
}
3535

@@ -84,7 +84,7 @@ public async ValueTask<IHtmlAsyncContent> PrerenderComponentAsync(
8484
{
8585
var rootComponent = prerenderMode is null
8686
? InstantiateComponent(componentType)
87-
: new SSRRenderModeBoundary(componentType, prerenderMode);
87+
: new SSRRenderModeBoundary(_httpContext, componentType, prerenderMode);
8888
var htmlRootComponent = await Dispatcher.InvokeAsync(() => BeginRenderingComponent(rootComponent, parameters));
8989
var result = new PrerenderedComponentHtmlContent(Dispatcher, htmlRootComponent);
9090

src/Components/Endpoints/src/Rendering/SSRRenderModeBoundary.cs

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Globalization;
88
using System.Security.Cryptography;
99
using System.Text;
10+
using Microsoft.AspNetCore.Builder;
1011
using Microsoft.AspNetCore.Components.Rendering;
1112
using Microsoft.AspNetCore.Components.Web;
1213
using Microsoft.AspNetCore.Http;
@@ -31,8 +32,13 @@ internal class SSRRenderModeBoundary : IComponent
3132
private IReadOnlyDictionary<string, object?>? _latestParameters;
3233
private string? _markerKey;
3334

34-
public SSRRenderModeBoundary([DynamicallyAccessedMembers(Component)] Type componentType, IComponentRenderMode renderMode)
35+
public SSRRenderModeBoundary(
36+
HttpContext httpContext,
37+
[DynamicallyAccessedMembers(Component)] Type componentType,
38+
IComponentRenderMode renderMode)
3539
{
40+
AssertRenderModeIsConfigured(httpContext, componentType, renderMode);
41+
3642
_componentType = componentType;
3743
_renderMode = renderMode;
3844
_prerender = renderMode switch
@@ -44,6 +50,50 @@ public SSRRenderModeBoundary([DynamicallyAccessedMembers(Component)] Type compon
4450
};
4551
}
4652

53+
private static void AssertRenderModeIsConfigured(HttpContext httpContext, Type componentType, IComponentRenderMode renderMode)
54+
{
55+
var configuredRenderModesMetadata = httpContext.GetEndpoint()?.Metadata.GetMetadata<ConfiguredRenderModesMetadata>();
56+
if (configuredRenderModesMetadata is null)
57+
{
58+
// This is not a Razor Components endpoint. It might be that the app is using RazorComponentResult,
59+
// or perhaps something else has changed the endpoint dynamically. In this case we don't know how
60+
// the app is configured so we just proceed and allow any errors to happen if the client-side code
61+
// later tries to reach endpoints that aren't mapped.
62+
return;
63+
}
64+
65+
var configuredModes = configuredRenderModesMetadata.ConfiguredRenderModes;
66+
67+
// We have to allow for specified rendermodes being subclases of the known types
68+
if (renderMode is ServerRenderMode || renderMode is AutoRenderMode)
69+
{
70+
AssertRenderModeIsConfigured<ServerRenderMode>(componentType, renderMode, configuredModes, "AddServerRenderMode");
71+
}
72+
73+
if (renderMode is WebAssemblyRenderMode || renderMode is AutoRenderMode)
74+
{
75+
AssertRenderModeIsConfigured<WebAssemblyRenderMode>(componentType, renderMode, configuredModes, "AddWebAssemblyRenderMode");
76+
}
77+
}
78+
79+
private static void AssertRenderModeIsConfigured<TRequiredMode>(Type componentType, IComponentRenderMode specifiedMode, IComponentRenderMode[] configuredModes, string expectedCall) where TRequiredMode: IComponentRenderMode
80+
{
81+
foreach (var configuredMode in configuredModes)
82+
{
83+
// We have to allow for configured rendermodes being subclases of the known types
84+
if (configuredMode is TRequiredMode)
85+
{
86+
return;
87+
}
88+
}
89+
90+
throw new InvalidOperationException($"A component of type '{componentType}' has render mode '{specifiedMode.GetType().Name}', " +
91+
$"but the required endpoints are not mapped on the server. When calling " +
92+
$"'{nameof(RazorComponentsEndpointRouteBuilderExtensions.MapRazorComponents)}', add a call to " +
93+
$"'{expectedCall}'. For example, " +
94+
$"'builder.{nameof(RazorComponentsEndpointRouteBuilderExtensions.MapRazorComponents)}<...>.{expectedCall}()'");
95+
}
96+
4797
public void Attach(RenderHandle renderHandle)
4898
{
4999
_renderHandle = renderHandle;

src/Components/Endpoints/test/RazorComponentEndpointFactoryTest.cs

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,16 @@ public void AddEndpoints_CreatesEndpointWithExpectedMetadata()
1818
var factory = new RazorComponentEndpointFactory();
1919
var conventions = new List<Action<EndpointBuilder>>();
2020
var finallyConventions = new List<Action<EndpointBuilder>>();
21+
var testRenderMode = new TestRenderMode();
22+
var configuredRenderModes = new ConfiguredRenderModesMetadata(new[] { testRenderMode });
2123
factory.AddEndpoints(endpoints, typeof(App), new PageComponentInfo(
2224
"App",
2325
typeof(App),
2426
"/",
2527
new object[] { new AuthorizeAttribute() }),
2628
conventions,
27-
finallyConventions);
29+
finallyConventions,
30+
configuredRenderModes);
2831

2932
var endpoint = Assert.Single(endpoints);
3033
Assert.Equal("/ (App)", endpoint.DisplayName);
@@ -35,6 +38,8 @@ public void AddEndpoints_CreatesEndpointWithExpectedMetadata()
3538
Assert.Contains(endpoint.Metadata, m => m is ComponentTypeMetadata);
3639
Assert.Contains(endpoint.Metadata, m => m is SuppressLinkGenerationMetadata);
3740
Assert.Contains(endpoint.Metadata, m => m is AuthorizeAttribute);
41+
Assert.Contains(endpoint.Metadata, m => m is ConfiguredRenderModesMetadata c
42+
&& c.ConfiguredRenderModes.Single() == testRenderMode);
3843
Assert.NotNull(endpoint.RequestDelegate);
3944

4045
var methods = Assert.Single(endpoint.Metadata.GetOrderedMetadata<HttpMethodMetadata>());
@@ -63,7 +68,8 @@ public void AddEndpoints_RunsConventions()
6368
"/",
6469
Array.Empty<object>()),
6570
conventions,
66-
finallyConventions);
71+
finallyConventions,
72+
new ConfiguredRenderModesMetadata(Array.Empty<IComponentRenderMode>()));
6773

6874
var endpoint = Assert.Single(endpoints);
6975
Assert.Contains(endpoint.Metadata, m => m is AuthorizeAttribute);
@@ -90,7 +96,8 @@ public void AddEndpoints_RunsFinallyConventions()
9096
"/",
9197
Array.Empty<object>()),
9298
conventions,
93-
finallyConventions);
99+
finallyConventions,
100+
new ConfiguredRenderModesMetadata(Array.Empty<IComponentRenderMode>()));
94101

95102
var endpoint = Assert.Single(endpoints);
96103
Assert.Contains(endpoint.Metadata, m => m is AuthorizeAttribute);
@@ -117,7 +124,8 @@ public void AddEndpoints_RouteOrderCanNotBeChanged()
117124
"/",
118125
Array.Empty<object>()),
119126
conventions,
120-
finallyConventions);
127+
finallyConventions,
128+
new ConfiguredRenderModesMetadata(Array.Empty<IComponentRenderMode>()));
121129

122130
var endpoint = Assert.Single(endpoints);
123131
var routeEndpoint = Assert.IsType<RouteEndpoint>(endpoint);
@@ -148,9 +156,12 @@ public void AddEndpoints_RunsFinallyConventionsAfterRegularConventions()
148156
"/",
149157
Array.Empty<object>()),
150158
conventions,
151-
finallyConventions);
159+
finallyConventions,
160+
new ConfiguredRenderModesMetadata(Array.Empty<IComponentRenderMode>()));
152161

153162
var endpoint = Assert.Single(endpoints);
154163
Assert.DoesNotContain(endpoint.Metadata, m => m is AuthorizeAttribute);
155164
}
165+
166+
class TestRenderMode : IComponentRenderMode { }
156167
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
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.Components.Web;
5+
using Microsoft.AspNetCore.Http;
6+
7+
namespace Microsoft.AspNetCore.Components.Endpoints;
8+
9+
public class SSRRenderModeBoundaryTest
10+
{
11+
// While most aspects of SSRRenderModeBoundary are only interesting to test E2E,
12+
// the configuration validation aspect is better covered as unit tests because
13+
// otherwise we would need many different E2E test app configurations.
14+
15+
[Fact]
16+
public void DoesNotAssertAboutConfiguredRenderModesOnUnknownEndpoints()
17+
{
18+
// Arrange: an endpoint with no ConfiguredRenderModesMetadata
19+
var httpContext = new DefaultHttpContext();
20+
httpContext.SetEndpoint(new Endpoint(null, new EndpointMetadataCollection(), null));
21+
22+
// Act/Assert: no exception means we're OK
23+
new SSRRenderModeBoundary(httpContext, typeof(TestComponent), new ServerRenderMode());
24+
new SSRRenderModeBoundary(httpContext, typeof(TestComponent), new WebAssemblyRenderMode());
25+
new SSRRenderModeBoundary(httpContext, typeof(TestComponent), new AutoRenderMode());
26+
}
27+
28+
[Fact]
29+
public void ThrowsIfServerRenderModeUsedAndNotConfigured()
30+
{
31+
// Arrange
32+
var httpContext = new DefaultHttpContext();
33+
PrepareEndpoint(httpContext, new WebAssemblyRenderModeSubclass());
34+
35+
// Act/Assert
36+
var ex = Assert.Throws<InvalidOperationException>(() => new SSRRenderModeBoundary(
37+
httpContext, typeof(TestComponent), new ServerRenderModeSubclass()));
38+
Assert.Contains($"A component of type '{typeof(TestComponent)}' has render mode '{nameof(ServerRenderModeSubclass)}'", ex.Message);
39+
Assert.Contains($"add a call to 'AddServerRenderMode'", ex.Message);
40+
}
41+
42+
[Fact]
43+
public void ThrowsIfWebAssemblyRenderModeUsedAndNotConfigured()
44+
{
45+
// Arrange
46+
var httpContext = new DefaultHttpContext();
47+
PrepareEndpoint(httpContext, new ServerRenderModeSubclass());
48+
49+
// Act/Assert
50+
var ex = Assert.Throws<InvalidOperationException>(() => new SSRRenderModeBoundary(
51+
httpContext, typeof(TestComponent), new WebAssemblyRenderModeSubclass()));
52+
Assert.Contains($"A component of type '{typeof(TestComponent)}' has render mode '{nameof(WebAssemblyRenderModeSubclass)}'", ex.Message);
53+
Assert.Contains($"add a call to 'AddWebAssemblyRenderMode'", ex.Message);
54+
}
55+
56+
[Fact]
57+
public void ThrowsIfAutoRenderModeUsedAndServerNotConfigured()
58+
{
59+
// Arrange
60+
var httpContext = new DefaultHttpContext();
61+
PrepareEndpoint(httpContext, new WebAssemblyRenderModeSubclass());
62+
63+
// Act/Assert
64+
var ex = Assert.Throws<InvalidOperationException>(() => new SSRRenderModeBoundary(
65+
httpContext, typeof(TestComponent), new AutoRenderModeSubclass()));
66+
Assert.Contains($"A component of type '{typeof(TestComponent)}' has render mode '{nameof(AutoRenderModeSubclass)}'", ex.Message);
67+
Assert.Contains($"add a call to 'AddServerRenderMode'", ex.Message);
68+
}
69+
70+
[Fact]
71+
public void ThrowsIfAutoRenderModeUsedAndWebAssemblyNotConfigured()
72+
{
73+
// Arrange
74+
var httpContext = new DefaultHttpContext();
75+
PrepareEndpoint(httpContext, new ServerRenderModeSubclass());
76+
77+
// Act/Assert
78+
var ex = Assert.Throws<InvalidOperationException>(() => new SSRRenderModeBoundary(
79+
httpContext, typeof(TestComponent), new AutoRenderModeSubclass()));
80+
Assert.Contains($"A component of type '{typeof(TestComponent)}' has render mode '{nameof(AutoRenderModeSubclass)}'", ex.Message);
81+
Assert.Contains($"add a call to 'AddWebAssemblyRenderMode'", ex.Message);
82+
}
83+
84+
private static void PrepareEndpoint(HttpContext httpContext, params IComponentRenderMode[] configuredModes)
85+
{
86+
httpContext.SetEndpoint(new Endpoint(null, new EndpointMetadataCollection(
87+
new ConfiguredRenderModesMetadata(configuredModes)), null));
88+
}
89+
90+
class TestComponent : IComponent
91+
{
92+
public void Attach(RenderHandle renderHandle)
93+
=> throw new NotImplementedException();
94+
95+
public Task SetParametersAsync(ParameterView parameters)
96+
=> throw new NotImplementedException();
97+
}
98+
99+
class ServerRenderModeSubclass : ServerRenderMode { }
100+
class WebAssemblyRenderModeSubclass : WebAssemblyRenderMode { }
101+
class AutoRenderModeSubclass : AutoRenderMode { }
102+
}

0 commit comments

Comments
 (0)