dotnet / efcore

EF Core is a modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations.
https://docs.microsoft.com/ef/
MIT License
13.68k stars 3.17k forks source link

Deferred Dispose (maybe?) #33892

Open jodydonetti opened 4 months ago

jodydonetti commented 4 months ago

The Problem

When using EF Core with ASP.NET the typical way is to get a DbContext via DI, and this means inherently with a scoped lifetime: this, in turn, means that when the HTTP request finishes, the scope will be disposed.

Normally this is all good, but sometimes we need to let a query run after the HTTP request finishes.

An example of this scenario in which I run into (actually, some of my community members did) is when using EF with FusionCache with Soft/Hard Timeouts enabled: this feature allows for a slow query that is taking too much time to be finished in the background, while temporarily returning the stale value, to allow for faster response times without missing on a cache update.

A different but similar feature that has a similar behavior is Eager Refresh.

For example here's an issue opened for this.

The Current Solution (Workaround?)

The solution highlighted in the original consist in changing the way to use EF, from a scoped DbContext automatically injected via DI to using IServiceScopeFactory and manually create scopes.

It did work, and seemingly well, but it doesn't look spectacular so I'm looking for potential alternatives.

A Possible Solution

Something different I've been thinking about is the concept of deferred scopes: by enabling such a feature it would be possible to defer the actual disposal of a DbContext, so that when calling Dispose() (either manually or automatically by ASP.NET's DI scope handling) the underlying disposal logic will be executed ONLY if there is no query being executed.

If instead there's a query still running, the disposal will be executed as soon as the query will complete, either successfully or by failing/throwing.

I'm not sure this is the way to go, maybe it's a stupid idea and there may be potential consequences for acting in this way, but this is the reason I'm opening this issue: to talk about such possibility and see if something comes up.

Thanks in advance for your time!

mauroservienti commented 3 months ago

Something different I've been thinking about is the concept of deferred scopes: by enabling such a feature it would be possible to defer the actual disposal of a DbContext, so that when calling Dispose() (either manually or automatically by ASP.NET's DI scope handling) the underlying disposal logic will be executed ONLY if there is no query being executed.

If instead there's a query still running, the disposal will be executed as soon as the query will complete, either successfully or by failing/throwing.

It sounds magical, and magic can lead to bad results. I would prefer an explicit approach: I know this query runs longer than the incoming HTTP request, and I'm intentionally deferring the disposal of the resource.

jodydonetti commented 3 months ago

It sounds magical, and magic can lead to bad results.

Agree! But scoped lifetime itself is already relatively "magical", don't you think? I mean you declare a service as a ctor/method params and, if it's an IDisposable, it will get disposed automatically and the end of the request, after the TResult has been executed etc.

So much so that... (see below)

I would prefer an explicit approach: I know this query runs longer than the incoming HTTP request, and I'm intentionally deferring the disposal of the resource.

How would you explicitly defer the disposal when it is scoped?

Because if you meant not using a scoped DbContext but instead using something like a DbContextFactory/ServiceScopeFactory then yes, of course, but it implies changing the most frequent and suggested habit, and that is certainly doable (in fact it's the suggestion I gave to my users) but also a hard sell.

Otherwise I don't know of a diffent way to explicitly specify that with scoped DbContexts, and therefore this issue asking for alternative ideas 🙂

Any outside-the-box idea to play with?

jodydonetti commented 3 months ago

As an additional interesting point, here's an example of a scenario where the deferred dispose approach would still not work.

It seems like manual scope handling or similar is the only way to go.

Opinions?

mauroservienti commented 3 months ago

Let me look at the (potential) problem from a different perspective. You're describing a situation in which sometimes certain queries are slow, or can take longer than the HTTP timeout to execute. If that's the case, the "magical" deferred disposal will hide a potential problem and will also enable lazy or unaware people to execute excessively long-ish queries for no good reasons.

I lean toward a model in which I'm forced to explicitly design for long queries, executing them asynchronously in a background fashion.

jodydonetti commented 3 months ago

I lean toward a model in which I'm forced to explicitly design for long queries, executing the, asynchronously in a background fashion.

Yeah, being explicit via ServiceScopeFactory or similar seems to be the consensus (even though it would not follow the "standard" scoped DbContext way).

I'll keep this open a little bit more, maybe somebody from the team has something to say about this, for future memory.

Otherwise I'll close this in a week or so.

Thanks all!

ajcvickers commented 2 months ago

We discussed this in the team. While there might be something here that could be worthwhile, the danger is resource leaks from a fire-and-forget API. Nevertheless, I'm putting it on the backlog to think about a bit more.

jodydonetti commented 2 months ago

Awesome, thanks @ajcvickers ! That is exactly what I was looking for: for the team with all the background info available to just think about it, to see if some interesting idea comes up. Not necessarily a new feature but even just a best practice or advice would be nice.

Should I keep this open or close it?

Thanks!