Ensure DI scope is disposed if prerendering fails
This commit is contained in:
parent
ee1cbda155
commit
b31cd35f92
|
|
@ -8,14 +8,13 @@ using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Components.Rendering;
|
using Microsoft.AspNetCore.Components.Rendering;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Http.Extensions;
|
using Microsoft.AspNetCore.Http.Extensions;
|
||||||
using Microsoft.AspNetCore.Http.Features;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Components.Server.Circuits
|
namespace Microsoft.AspNetCore.Components.Server.Circuits
|
||||||
{
|
{
|
||||||
internal class CircuitPrerenderer : IComponentPrerenderer
|
internal class CircuitPrerenderer : IComponentPrerenderer
|
||||||
{
|
{
|
||||||
private static object CircuitHostKey = new object();
|
private static object CircuitHostKey = new object();
|
||||||
private static object NavigationStatusKey = new object();
|
private static object CancellationStatusKey = new object();
|
||||||
|
|
||||||
private readonly CircuitFactory _circuitFactory;
|
private readonly CircuitFactory _circuitFactory;
|
||||||
private readonly CircuitRegistry _registry;
|
private readonly CircuitRegistry _registry;
|
||||||
|
|
@ -29,15 +28,15 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
||||||
public async Task<ComponentPrerenderResult> PrerenderComponentAsync(ComponentPrerenderingContext prerenderingContext)
|
public async Task<ComponentPrerenderResult> PrerenderComponentAsync(ComponentPrerenderingContext prerenderingContext)
|
||||||
{
|
{
|
||||||
var context = prerenderingContext.Context;
|
var context = prerenderingContext.Context;
|
||||||
var navigationStatus = GetOrCreateNavigationStatus(context);
|
var cancellationStatus = GetOrCreateCancellationStatus(context);
|
||||||
if (navigationStatus.Navigated)
|
if (cancellationStatus.Canceled)
|
||||||
{
|
{
|
||||||
// Avoid creating a circuit host if other component earlier in the pipeline already triggered
|
// Avoid creating a circuit host if other component earlier in the pipeline already triggered
|
||||||
// a navigation request. Instead rendre nothing
|
// cancelation (e.g., by navigating or throwing). Instead render nothing.
|
||||||
return new ComponentPrerenderResult(Array.Empty<string>());
|
return new ComponentPrerenderResult(Array.Empty<string>());
|
||||||
}
|
}
|
||||||
var circuitHost = GetOrCreateCircuitHost(context, navigationStatus);
|
var circuitHost = GetOrCreateCircuitHost(context, cancellationStatus);
|
||||||
ComponentRenderedText renderResult = default;
|
ComponentRenderedText renderResult;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
renderResult = await circuitHost.PrerenderComponentAsync(
|
renderResult = await circuitHost.PrerenderComponentAsync(
|
||||||
|
|
@ -48,7 +47,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
||||||
{
|
{
|
||||||
// Cleanup the state as we won't need it any longer.
|
// Cleanup the state as we won't need it any longer.
|
||||||
// Signal callbacks that we don't have to register the circuit.
|
// Signal callbacks that we don't have to register the circuit.
|
||||||
await CleanupCircuitState(context, navigationStatus, circuitHost);
|
await CleanupCircuitState(context, cancellationStatus, circuitHost);
|
||||||
|
|
||||||
// Navigation was attempted during prerendering.
|
// Navigation was attempted during prerendering.
|
||||||
if (prerenderingContext.Context.Response.HasStarted)
|
if (prerenderingContext.Context.Response.HasStarted)
|
||||||
|
|
@ -64,6 +63,12 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
||||||
context.Response.Redirect(navigationException.Location);
|
context.Response.Redirect(navigationException.Location);
|
||||||
return new ComponentPrerenderResult(Array.Empty<string>());
|
return new ComponentPrerenderResult(Array.Empty<string>());
|
||||||
}
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// If prerendering any component fails, cancel prerendering entirely and dispose the DI scope
|
||||||
|
await CleanupCircuitState(context, cancellationStatus, circuitHost);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
circuitHost.Descriptors.Add(new ComponentDescriptor
|
circuitHost.Descriptors.Add(new ComponentDescriptor
|
||||||
{
|
{
|
||||||
|
|
@ -81,28 +86,28 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
||||||
return new ComponentPrerenderResult(result);
|
return new ComponentPrerenderResult(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
private CircuitNavigationStatus GetOrCreateNavigationStatus(HttpContext context)
|
private PrerenderingCancellationStatus GetOrCreateCancellationStatus(HttpContext context)
|
||||||
{
|
{
|
||||||
if (context.Items.TryGetValue(NavigationStatusKey, out var existingHost))
|
if (context.Items.TryGetValue(CancellationStatusKey, out var existingValue))
|
||||||
{
|
{
|
||||||
return (CircuitNavigationStatus)existingHost;
|
return (PrerenderingCancellationStatus)existingValue;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var navigationStatus = new CircuitNavigationStatus();
|
var cancellationStatus = new PrerenderingCancellationStatus();
|
||||||
context.Items[NavigationStatusKey] = navigationStatus;
|
context.Items[CancellationStatusKey] = cancellationStatus;
|
||||||
return navigationStatus;
|
return cancellationStatus;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task CleanupCircuitState(HttpContext context, CircuitNavigationStatus navigationStatus, CircuitHost circuitHost)
|
private static async Task CleanupCircuitState(HttpContext context, PrerenderingCancellationStatus cancellationStatus, CircuitHost circuitHost)
|
||||||
{
|
{
|
||||||
navigationStatus.Navigated = true;
|
cancellationStatus.Canceled = true;
|
||||||
context.Items.Remove(CircuitHostKey);
|
context.Items.Remove(CircuitHostKey);
|
||||||
await circuitHost.DisposeAsync();
|
await circuitHost.DisposeAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private CircuitHost GetOrCreateCircuitHost(HttpContext context, CircuitNavigationStatus navigationStatus)
|
private CircuitHost GetOrCreateCircuitHost(HttpContext context, PrerenderingCancellationStatus cancellationStatus)
|
||||||
{
|
{
|
||||||
if (context.Items.TryGetValue(CircuitHostKey, out var existingHost))
|
if (context.Items.TryGetValue(CircuitHostKey, out var existingHost))
|
||||||
{
|
{
|
||||||
|
|
@ -120,7 +125,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
||||||
context.Response.OnCompleted(() =>
|
context.Response.OnCompleted(() =>
|
||||||
{
|
{
|
||||||
result.UnhandledException -= CircuitHost_UnhandledException;
|
result.UnhandledException -= CircuitHost_UnhandledException;
|
||||||
if (!navigationStatus.Navigated)
|
if (!cancellationStatus.Canceled)
|
||||||
{
|
{
|
||||||
_registry.RegisterDisconnectedCircuit(result);
|
_registry.RegisterDisconnectedCircuit(result);
|
||||||
}
|
}
|
||||||
|
|
@ -164,9 +169,9 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private class CircuitNavigationStatus
|
private class PrerenderingCancellationStatus
|
||||||
{
|
{
|
||||||
public bool Navigated { get; set; }
|
public bool Canceled { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,32 @@ namespace Microsoft.AspNetCore.Components.Server.Tests.Circuits
|
||||||
}), GetUnwrappedContent(result));
|
}), GetUnwrappedContent(result));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DisposesCircuitScopeEvenIfPrerenderingThrows()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var circuitFactory = new MockServiceScopeCircuitFactory();
|
||||||
|
var circuitRegistry = new CircuitRegistry(
|
||||||
|
Options.Create(new CircuitOptions()),
|
||||||
|
Mock.Of<ILogger<CircuitRegistry>>(),
|
||||||
|
TestCircuitIdFactory.CreateTestFactory());
|
||||||
|
var httpContext = new DefaultHttpContext();
|
||||||
|
var prerenderer = new CircuitPrerenderer(circuitFactory, circuitRegistry);
|
||||||
|
var prerenderingContext = new ComponentPrerenderingContext
|
||||||
|
{
|
||||||
|
ComponentType = typeof(ThrowExceptionComponent),
|
||||||
|
Parameters = ParameterCollection.Empty,
|
||||||
|
Context = httpContext
|
||||||
|
};
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await Assert.ThrowsAsync<InvalidTimeZoneException>(async () =>
|
||||||
|
await prerenderer.PrerenderComponentAsync(prerenderingContext));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
circuitFactory.MockServiceScope.Verify(scope => scope.Dispose(), Times.Once());
|
||||||
|
}
|
||||||
|
|
||||||
class TestCircuitFactory : CircuitFactory
|
class TestCircuitFactory : CircuitFactory
|
||||||
{
|
{
|
||||||
public override CircuitHost CreateCircuitHost(HttpContext httpContext, CircuitClientProxy client, string uriAbsolute, string baseUriAbsolute)
|
public override CircuitHost CreateCircuitHost(HttpContext httpContext, CircuitClientProxy client, string uriAbsolute, string baseUriAbsolute)
|
||||||
|
|
@ -127,6 +153,17 @@ namespace Microsoft.AspNetCore.Components.Server.Tests.Circuits
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class MockServiceScopeCircuitFactory : CircuitFactory
|
||||||
|
{
|
||||||
|
public Mock<IServiceScope> MockServiceScope { get; }
|
||||||
|
= new Mock<IServiceScope>();
|
||||||
|
|
||||||
|
public override CircuitHost CreateCircuitHost(HttpContext httpContext, CircuitClientProxy client, string uriAbsolute, string baseUriAbsolute)
|
||||||
|
{
|
||||||
|
return TestCircuitHost.Create(Guid.NewGuid().ToString(), MockServiceScope.Object);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class UriDisplayComponent : IComponent
|
class UriDisplayComponent : IComponent
|
||||||
{
|
{
|
||||||
private RenderHandle _renderHandle;
|
private RenderHandle _renderHandle;
|
||||||
|
|
@ -151,5 +188,14 @@ namespace Microsoft.AspNetCore.Components.Server.Tests.Circuits
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ThrowExceptionComponent : IComponent
|
||||||
|
{
|
||||||
|
public void Configure(RenderHandle renderHandle)
|
||||||
|
=> throw new InvalidTimeZoneException();
|
||||||
|
|
||||||
|
public Task SetParametersAsync(ParameterCollection parameters)
|
||||||
|
=> Task.CompletedTask;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue