-
Notifications
You must be signed in to change notification settings - Fork 10.4k
Ensure DI scope is disposed #11894
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
Ensure DI scope is disposed #11894
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 |
---|---|---|
|
@@ -8,14 +8,13 @@ | |
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 @@ public CircuitPrerenderer(CircuitFactory circuitFactory, CircuitRegistry registr | |
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. | ||
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. nit, mixture of cancellation and cancelation 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. Good spot. I'll fix the comment typo in an unrelated PR just so I don't have to wait for CI checks here again. |
||
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 @@ public async Task<ComponentPrerenderResult> PrerenderComponentAsync(ComponentPre | |
{ | ||
// 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 @@ public async Task<ComponentPrerenderResult> PrerenderComponentAsync(ComponentPre | |
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 @@ public async Task<ComponentPrerenderResult> PrerenderComponentAsync(ComponentPre | |
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 @@ private CircuitHost GetOrCreateCircuitHost(HttpContext context, CircuitNavigatio | |
context.Response.OnCompleted(() => | ||
{ | ||
result.UnhandledException -= CircuitHost_UnhandledException; | ||
if (!navigationStatus.Navigated) | ||
if (!cancellationStatus.Canceled) | ||
{ | ||
_registry.RegisterDisconnectedCircuit(result); | ||
} | ||
|
@@ -164,9 +169,9 @@ private string GetFullBaseUri(HttpRequest request) | |
return result; | ||
} | ||
|
||
private class CircuitNavigationStatus | ||
private class PrerenderingCancellationStatus | ||
{ | ||
public bool Navigated { get; set; } | ||
public bool Canceled { get; set; } | ||
} | ||
} | ||
} |
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.
Most of the noise in the diff here is just because I renamed the concept of
NavigationStatus
toCancellationStatus
.The code was never really interested in the concept of "did we navigate or not" - it was only interested in "should the circuit continue to exist", so this is a more precise name. It's relevant to change this now because there's now a second case where the circuit should not continue to exist (i.e., some component threw during prerendering, so we gave up on prerendering and released the DI scope immediately).
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.
I'm not sure what the E2E behavior is, but when multiple components get prerendered at the same time they share the same circuit, so isn't it bad to prevent other prerendered components from working? (limiting the error to the current prerendered component)
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.
When a component throws an unhandled exception in prerendering, we return a 500 error to the browser, not a partially-complete HTML response.
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.
I still have some doubts about this because people could write a try catch for these scenarios to keep other components in the page working and we are taking away that option, I'm not sure if that is something we want to consider.
(For example, don't dispose the circuit if some other component rendered successfully and only do the cleanup for the circuit on response started if we got a 500 out)
Uh oh!
There was an error while loading. Please reload this page.
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.
That's a good question to raise, but it makes me think maybe we shouldn't be putting all prerendered components into the same circuit, or should give some other way to control how they are grouped into circuits so those groups can fail independently.
This is because in non-prerendering cases, we don't allow circuits/renderers/etc to continue to exist and be used once they have got into an undefined state through an unhandled exception during rendering or lifecycle methods. So in that sense it's good that we prevent it for prerendering too, and don't allow people to try/catch and then continue using that circuit state.
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.
I think that's a fair answer, given that we are moving to a single circuit per connection model (independent of whether its prerendered or not)
I think that makes sense and leaves things in a more manageable state, so I'm ok with it.
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.
I don't think we should work hard to make life better when something is crashing. We should make sure that it fails hard, if users have multiple components on a page and they want to degrade gracefully they need to do it in their own code.
So I agree with the conclusion here.