`NonRetryingExecutionStrategy` does not set `ExecutionStrategy.Current` and is ignored.
- Dominant language
- C#
- Stars
- 14.8k
- Forks
- 3.4k
- PR merge metrics
- PR metrics pending
Description
## Background
[Connection Resiliency](https://learn.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency) is implemented in EF Core by setting the execution strategy used by EF Core queries. Usually `.EnableRetryOnFailure()` is called at startup to enable a provider specialized retrying execution strategy, which is made available in the DBContext dependencies.
For example, on the [Pomelo.EntityFrameworkCore.MySql](https://github.com/PomeloFoundation/Pomelo.EntityFrameworkCore.MySql) provider this is the [MySqlRetryingExecutionStrategy](https://github.com/PomeloFoundation/Pomelo.EntityFrameworkCore.MySql/blob/main/src/EFCore.MySql/MySqlRetryingExecutionStrategy.cs). On [Npgsql](https://github.com/npgsql/efcore.pg) this is the [NpgsqlRetryingExecutionStrategy](https://github.com/npgsql/efcore.pg/blob/a8677e4f9afe4e5955bc4d810cc0665c7a49d4f3/src/EFCore.PG/NpgsqlRetryingExecutionStrategy.cs).
Note that both of these classes derive from the abstract [ExecutionStrategy](https://github.com/dotnet/efcore/blob/main/src/EFCore/Storage/ExecutionStrategy.cs) class, which in turn implements the [IExecutionStrategy](https://github.com/dotnet/efcore/blob/main/src/EFCore/Storage/IExecutionStrategy.cs) interface.
## The issue
`ExecutionStrategy.Current` is a static property of the abstract type `ExecutionStrategy` backed by an `AsyncLocal` that is set to the current execution strategy when `ExecutionStrategy.Execute()` or `ExecutionStrategy.ExecuteAsync()` is called, such that code within the delegate passed to the execute methods can observe the `ExecutionStrategy.Current` value and use it appropriately.
Unfortunately, this property can only contain the abstract [ExecutionStrategy](https://github.com/dotnet/efcore/blob/main/src/EFCore/Storage/ExecutionStrategy.cs) class, not an instance of the [IExecutionStrategy](https://github.com/dotnet/efcore/blob/main/src/EFCore/Storage/IExecutionStrategy.cs) interface.
[NonRetryingExecutionStrategy](https://github.com/dotnet/efcore/blob/main/src/EFCore/Storage/NonRetryingExecutionStrategy.c) only implements [IExecutionStrategy](https://github.com/dotnet/efcore/blob/main/src/EFCore/Storage/IExecutionStrategy.cs). It does not derive from [ExecutionStrategy](https://github.com/dotnet/efcore/blob/main/src/EFCore/Storage/ExecutionStrategy.cs). Therefore it does not, and cannot, set `ExecutionStrategy.Current` to itself when `NonRetryingExecutionStrategy.Execute()` is called, so `ExecutionStrategy.Current` will be `null` when observed within the executed delegate:
```csharp
var nonRetryingExecutionStrategy = new NonRetryingExecutionStrategy(dbContext);
await nonRetryingExecutionStrategy.ExecuteAsync(async () =>
{
// ExecutionStrategy.Current is null here!
// Any query you run here will use the default execution strategy
Debug.Assert(ExecutionStrategy.Current != null); // Boom!
Debug.Assert(ExecutionStrategy.Current == nonRetryingExecutionStrategy);
}
```
Several features rely on `ExecutionStrategy.Current`, notably `QueryCompilationContext.IsBuffering`, which is set based on `ExecutionStrategy.Current.RetriesOnFailure`. If `ExecutionStrategy.Current` is null, it will fall back to the value given by `QueryCompilationContextDependencies`, which usually contains the "globally" configured execution strategy set during application startup:
https://github.com/dotnet/efcore/blob/cb9bb05e7ec9a245faf8d1e0d0224e9bebb43d85/src/EFCore/Query/QueryCompilationContext.cs#L120
https://github.com/dotnet/efcore/blob/cb9bb05e7ec9a245faf8d1e0d0224e9bebb43d85/src/EFCore.Relational/Query/RelationalCompiledQueryCacheKeyGenerator.cs#L44
# The issue conclusion
If the user has configured a retrying execution strategy using `.EnableRetryOnFailure()` (eg. `MySqlRetryingExecutionStrategy`), but wishes to disable this execution strategy for a single query to avoid query response buffering, they will likely attempt to do so by running the query within `NonRetryingExecutionStrategy.Execute()`.
`ExecutionStrategy.Current` will be set to `null`, and the query will instead run as if it is executed within the default retrying query strategy, which causes the query response to buffer.
## Workaround
It is currently possible to work around this issue either by creating a custom execution strategy that derives from `ExecutionStrategy`, or by abusing the existing execution strategies. For example, `MySqlRetryingExecutionStrategy` can be constructed with `maxRetryCount = 0`, which causes it to return [ExecutionStrategy.RetriesOnFailure](https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.storage.executionstrategy.retriesonfailure?view=efcore-8.0#microsoft-entityframeworkcore-storage-executionstrategy-retriesonfailure) as `false`.
```csharp
var mySqlNonRetryingExecutionStrategy = new MySqlRetryingExecutionStrategy(dbContext, 0);
await mySqlNonRetryingExecutionStrategy.ExecuteAsync(async () =>
{
// ExecutionStrategy.Current is successfully set to our mySqlNonRetryingExecutionStrategy instance.
// Any code that uses ExecutionStrategy.Current?.RetriesOnFailure will read it as false.
Debug.Assert(ExecutionStrategy.Current != null); // OK!
Debug.Assert(ExecutionStrategy.Current == mySqlNonRetryingExecutionStrategy); // OK!
}
```
### Include provider and version information
EF Core version: release/8.0
Target framework: .NET 8.0
Contributor guide
Assessment
This issue has not been assessed yet.