-
Notifications
You must be signed in to change notification settings - Fork 10.3k
Give a useful error if rendermode endpoints aren't mapped #50311
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
namespace Microsoft.AspNetCore.Components.Endpoints; | ||
|
||
internal class ConfiguredRenderModesMetadata(IComponentRenderMode[] configuredRenderModes) | ||
{ | ||
public IComponentRenderMode[] ConfiguredRenderModes => configuredRenderModes; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,7 +24,8 @@ internal void AddEndpoints( | |
[DynamicallyAccessedMembers(Component)] Type rootComponent, | ||
PageComponentInfo pageDefinition, | ||
IReadOnlyList<Action<EndpointBuilder>> conventions, | ||
IReadOnlyList<Action<EndpointBuilder>> finallyConventions) | ||
IReadOnlyList<Action<EndpointBuilder>> finallyConventions, | ||
ConfiguredRenderModesMetadata configuredRenderModesMetadata) | ||
{ | ||
// We do not provide a way to establish the order or the name for the page routes. | ||
// Order is not supported in our client router. | ||
|
@@ -48,6 +49,7 @@ internal void AddEndpoints( | |
builder.Metadata.Add(HttpMethodsMetadata); | ||
builder.Metadata.Add(new ComponentTypeMetadata(pageDefinition.Type)); | ||
builder.Metadata.Add(new RootComponentMetadata(rootComponent)); | ||
builder.Metadata.Add(configuredRenderModesMetadata); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @SteveSandersonMS might be "easier" to add this to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It looks like that would be less easy since we'd have to flow the list of configured render modes deeper through It's internal and doesn't impact any public APIs in any case. |
||
|
||
foreach (var convention in conventions) | ||
{ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,7 @@ | |
using System.Globalization; | ||
using System.Security.Cryptography; | ||
using System.Text; | ||
using Microsoft.AspNetCore.Builder; | ||
using Microsoft.AspNetCore.Components.Rendering; | ||
using Microsoft.AspNetCore.Components.Web; | ||
using Microsoft.AspNetCore.Http; | ||
|
@@ -31,8 +32,13 @@ internal class SSRRenderModeBoundary : IComponent | |
private IReadOnlyDictionary<string, object?>? _latestParameters; | ||
private string? _markerKey; | ||
|
||
public SSRRenderModeBoundary([DynamicallyAccessedMembers(Component)] Type componentType, IComponentRenderMode renderMode) | ||
public SSRRenderModeBoundary( | ||
HttpContext httpContext, | ||
[DynamicallyAccessedMembers(Component)] Type componentType, | ||
IComponentRenderMode renderMode) | ||
{ | ||
AssertRenderModeIsConfigured(httpContext, componentType, renderMode); | ||
|
||
_componentType = componentType; | ||
_renderMode = renderMode; | ||
_prerender = renderMode switch | ||
|
@@ -44,6 +50,50 @@ public SSRRenderModeBoundary([DynamicallyAccessedMembers(Component)] Type compon | |
}; | ||
} | ||
|
||
private static void AssertRenderModeIsConfigured(HttpContext httpContext, Type componentType, IComponentRenderMode renderMode) | ||
{ | ||
var configuredRenderModesMetadata = httpContext.GetEndpoint()?.Metadata.GetMetadata<ConfiguredRenderModesMetadata>(); | ||
if (configuredRenderModesMetadata is null) | ||
{ | ||
// This is not a Razor Components endpoint. It might be that the app is using RazorComponentResult, | ||
// or perhaps something else has changed the endpoint dynamically. In this case we don't know how | ||
// the app is configured so we just proceed and allow any errors to happen if the client-side code | ||
// later tries to reach endpoints that aren't mapped. | ||
return; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Semi-related: a possible way forwards for one of the That doesn't solve the "need a new DI scope" issue but it solves 50% of the problem at least. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @SteveSandersonMS what if the page was able to say "Disallow interactivity" through an attribute or similar (only at the page level). That would mean the Error page can apply that attribute and there's no dance needed between the exception handler and Blazor. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes that's probably reasonable. Let's do any follow-up about that in #50287 |
||
} | ||
|
||
var configuredModes = configuredRenderModesMetadata.ConfiguredRenderModes; | ||
|
||
// We have to allow for specified rendermodes being subclases of the known types | ||
if (renderMode is ServerRenderMode || renderMode is AutoRenderMode) | ||
{ | ||
AssertRenderModeIsConfigured<ServerRenderMode>(componentType, renderMode, configuredModes, "AddServerRenderMode"); | ||
} | ||
|
||
if (renderMode is WebAssemblyRenderMode || renderMode is AutoRenderMode) | ||
{ | ||
AssertRenderModeIsConfigured<WebAssemblyRenderMode>(componentType, renderMode, configuredModes, "AddWebAssemblyRenderMode"); | ||
} | ||
} | ||
|
||
private static void AssertRenderModeIsConfigured<TRequiredMode>(Type componentType, IComponentRenderMode specifiedMode, IComponentRenderMode[] configuredModes, string expectedCall) where TRequiredMode: IComponentRenderMode | ||
{ | ||
foreach (var configuredMode in configuredModes) | ||
{ | ||
// We have to allow for configured rendermodes being subclases of the known types | ||
if (configuredMode is TRequiredMode) | ||
{ | ||
return; | ||
} | ||
} | ||
|
||
throw new InvalidOperationException($"A component of type '{componentType}' has render mode '{specifiedMode.GetType().Name}', " + | ||
$"but the required endpoints are not mapped on the server. When calling " + | ||
$"'{nameof(RazorComponentsEndpointRouteBuilderExtensions.MapRazorComponents)}', add a call to " + | ||
$"'{expectedCall}'. For example, " + | ||
$"'builder.{nameof(RazorComponentsEndpointRouteBuilderExtensions.MapRazorComponents)}<...>.{expectedCall}()'"); | ||
} | ||
|
||
public void Attach(RenderHandle renderHandle) | ||
{ | ||
_renderHandle = renderHandle; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using Microsoft.AspNetCore.Components.Web; | ||
using Microsoft.AspNetCore.Http; | ||
|
||
namespace Microsoft.AspNetCore.Components.Endpoints; | ||
|
||
public class SSRRenderModeBoundaryTest | ||
{ | ||
// While most aspects of SSRRenderModeBoundary are only interesting to test E2E, | ||
// the configuration validation aspect is better covered as unit tests because | ||
// otherwise we would need many different E2E test app configurations. | ||
|
||
[Fact] | ||
public void DoesNotAssertAboutConfiguredRenderModesOnUnknownEndpoints() | ||
{ | ||
// Arrange: an endpoint with no ConfiguredRenderModesMetadata | ||
var httpContext = new DefaultHttpContext(); | ||
httpContext.SetEndpoint(new Endpoint(null, new EndpointMetadataCollection(), null)); | ||
|
||
// Act/Assert: no exception means we're OK | ||
new SSRRenderModeBoundary(httpContext, typeof(TestComponent), new ServerRenderMode()); | ||
new SSRRenderModeBoundary(httpContext, typeof(TestComponent), new WebAssemblyRenderMode()); | ||
new SSRRenderModeBoundary(httpContext, typeof(TestComponent), new AutoRenderMode()); | ||
} | ||
|
||
[Fact] | ||
public void ThrowsIfServerRenderModeUsedAndNotConfigured() | ||
{ | ||
// Arrange | ||
var httpContext = new DefaultHttpContext(); | ||
PrepareEndpoint(httpContext, new WebAssemblyRenderModeSubclass()); | ||
|
||
// Act/Assert | ||
var ex = Assert.Throws<InvalidOperationException>(() => new SSRRenderModeBoundary( | ||
httpContext, typeof(TestComponent), new ServerRenderModeSubclass())); | ||
Assert.Contains($"A component of type '{typeof(TestComponent)}' has render mode '{nameof(ServerRenderModeSubclass)}'", ex.Message); | ||
Assert.Contains($"add a call to 'AddServerRenderMode'", ex.Message); | ||
} | ||
|
||
[Fact] | ||
public void ThrowsIfWebAssemblyRenderModeUsedAndNotConfigured() | ||
{ | ||
// Arrange | ||
var httpContext = new DefaultHttpContext(); | ||
PrepareEndpoint(httpContext, new ServerRenderModeSubclass()); | ||
|
||
// Act/Assert | ||
var ex = Assert.Throws<InvalidOperationException>(() => new SSRRenderModeBoundary( | ||
httpContext, typeof(TestComponent), new WebAssemblyRenderModeSubclass())); | ||
Assert.Contains($"A component of type '{typeof(TestComponent)}' has render mode '{nameof(WebAssemblyRenderModeSubclass)}'", ex.Message); | ||
Assert.Contains($"add a call to 'AddWebAssemblyRenderMode'", ex.Message); | ||
} | ||
|
||
[Fact] | ||
public void ThrowsIfAutoRenderModeUsedAndServerNotConfigured() | ||
{ | ||
// Arrange | ||
var httpContext = new DefaultHttpContext(); | ||
PrepareEndpoint(httpContext, new WebAssemblyRenderModeSubclass()); | ||
|
||
// Act/Assert | ||
var ex = Assert.Throws<InvalidOperationException>(() => new SSRRenderModeBoundary( | ||
httpContext, typeof(TestComponent), new AutoRenderModeSubclass())); | ||
Assert.Contains($"A component of type '{typeof(TestComponent)}' has render mode '{nameof(AutoRenderModeSubclass)}'", ex.Message); | ||
Assert.Contains($"add a call to 'AddServerRenderMode'", ex.Message); | ||
} | ||
|
||
[Fact] | ||
public void ThrowsIfAutoRenderModeUsedAndWebAssemblyNotConfigured() | ||
{ | ||
// Arrange | ||
var httpContext = new DefaultHttpContext(); | ||
PrepareEndpoint(httpContext, new ServerRenderModeSubclass()); | ||
|
||
// Act/Assert | ||
var ex = Assert.Throws<InvalidOperationException>(() => new SSRRenderModeBoundary( | ||
httpContext, typeof(TestComponent), new AutoRenderModeSubclass())); | ||
Assert.Contains($"A component of type '{typeof(TestComponent)}' has render mode '{nameof(AutoRenderModeSubclass)}'", ex.Message); | ||
Assert.Contains($"add a call to 'AddWebAssemblyRenderMode'", ex.Message); | ||
} | ||
|
||
private static void PrepareEndpoint(HttpContext httpContext, params IComponentRenderMode[] configuredModes) | ||
{ | ||
httpContext.SetEndpoint(new Endpoint(null, new EndpointMetadataCollection( | ||
new ConfiguredRenderModesMetadata(configuredModes)), null)); | ||
} | ||
|
||
class TestComponent : IComponent | ||
{ | ||
public void Attach(RenderHandle renderHandle) | ||
=> throw new NotImplementedException(); | ||
|
||
public Task SetParametersAsync(ParameterView parameters) | ||
=> throw new NotImplementedException(); | ||
} | ||
|
||
class ServerRenderModeSubclass : ServerRenderMode { } | ||
class WebAssemblyRenderModeSubclass : WebAssemblyRenderMode { } | ||
class AutoRenderModeSubclass : AutoRenderMode { } | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reference to
UseDeclaredRenderModes
is obsolete as that no longer exists.