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.Http;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Http.Features;
namespace Microsoft.AspNetCore.Components.Server.Circuits
{
internal class CircuitPrerenderer : IComponentPrerenderer
{
private static object CircuitHostKey = new object();
private static object NavigationStatusKey = new object();
private static object CancellationStatusKey = new object();
private readonly CircuitFactory _circuitFactory;
private readonly CircuitRegistry _registry;
@ -29,15 +28,15 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
public async Task<ComponentPrerenderResult> PrerenderComponentAsync(ComponentPrerenderingContext prerenderingContext)
{
var context = prerenderingContext.Context;
var navigationStatus = GetOrCreateNavigationStatus(context);
if (navigationStatus.Navigated)
var cancellationStatus = GetOrCreateCancellationStatus(context);
if (cancellationStatus.Canceled)
{
// 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>());
}
var circuitHost = GetOrCreateCircuitHost(context, navigationStatus);
ComponentRenderedText renderResult = default;
var circuitHost = GetOrCreateCircuitHost(context, cancellationStatus);
ComponentRenderedText renderResult;
try
{
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.
// 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.
if (prerenderingContext.Context.Response.HasStarted)
@ -64,6 +63,12 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
context.Response.Redirect(navigationException.Location);
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
{
@ -81,28 +86,28 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
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
{
var navigationStatus = new CircuitNavigationStatus();
context.Items[NavigationStatusKey] = navigationStatus;
return navigationStatus;
var cancellationStatus = new PrerenderingCancellationStatus();
context.Items[CancellationStatusKey] = cancellationStatus;
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);
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))
{
@ -120,7 +125,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
context.Response.OnCompleted(() =>
{
result.UnhandledException -= CircuitHost_UnhandledException;
if (!navigationStatus.Navigated)
if (!cancellationStatus.Canceled)
{
_registry.RegisterDisconnectedCircuit(result);
}
@ -164,9 +169,9 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
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));
}
[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
{
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
{
private RenderHandle _renderHandle;
@ -151,5 +188,14 @@ namespace Microsoft.AspNetCore.Components.Server.Tests.Circuits
return Task.CompletedTask;
}
}
class ThrowExceptionComponent : IComponent
{
public void Configure(RenderHandle renderHandle)
=> throw new InvalidTimeZoneException();
public Task SetParametersAsync(ParameterCollection parameters)
=> Task.CompletedTask;
}
}
}