Ensure DI scope is disposed if prerendering fails

This commit is contained in:
Steve Sanderson 2019-07-04 11:16:10 +01:00
parent ee1cbda155
commit b31cd35f92
2 changed files with 71 additions and 20 deletions

View File

@ -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; }
} }
} }
} }

View File

@ -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;
}
} }
} }