dotnet / dotnet/efcore

`NonRetryingExecutionStrategy` does not set `ExecutionStrategy.Current` and is ignored.

Open
#34,953 2 comments 1 reaction 1 assignee Claimed by @AndriySvyryd View on GitHub
area-execution-strategies customer-reported
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.