Little wrapper for IExecutionStrategy
. This packages is intended to simplify workaround with IExecutionStrategy
by providing extension methods for DbContext
.
ExecutionStrategy.Extensions is available on NuGet and can be installed via the below commands:
$ Install-Package EntityFrameworkCore.ExecutionStrategy.Extensions
or via the .NET Core CLI:
$ dotnet add package EntityFrameworkCore.ExecutionStrategy.Extensions
Add this to your DbContextOptionsBuilder
:
// Here might be any provider you use with retry on failure enabled
builder.UseNpgsql(conn, builder =>
builder.EnableRetryOnFailure());
// Configure ExecutionStrategyExtended
builder.UseExecutionStrategyExtensions<AppDbContext>(
builder => builder.WithClearChangeTrackerOnRetry());
Once you've configured it, you can use it inside your controllers like this:
await context.ExecuteExtendedAsync(async () =>
{
await service.AddUser(user);
});
This is equivalent to the following manual approach:
var strategy = context.Database.CreateExecutionStrategy();
await strategy.ExecuteAsync(async () =>
{
context.ChangeTracker.Clear();
await service.AddUser(user);
});
The Microsoft documentation recommends recreating a new DbContext
on each retry since otherwise it could lead to those bugs:
strategy.ExecuteAsync(
async (context) =>
{
var user = new User(0, "asd");
context.Add(user);
// Transient exception could occure here and IExecutionStrategy will retry execution
// It will lead to adding a second user to change tracker of DbContext
var products = await context.Products.ToListAsync();
await context.SaveChangesAsync();
});
However, manually recreating DbContext
in every retry can be inconvenient since you need to recreate instances of services to provide them new DbContext
, instead you can clear change tracker on existing DbContext
and reuse it.
You can manage transactions yourself or by using extension method on action builder:
await context.ExecuteExtendedAsync(async () =>
{
context.Users.Add(new User(0, "asd"));
await context.SaveChangesAsync();
}, builder =>
{
builder.WithTransaction(IsolationLevel.Serializable);
});
If you need to customize the behavior of WithClearChangeTrackerOnRetry
, you can do so by providing custom middleware in the builder. In fact, WithTransaction
works on top of these middlewares too. Here's how WithClearChangeTrackerOnRetry
written internally:
builder.WithMiddleware(async (next, args) =>
{
args.Context.ChangeTracker.Clear();
return await next(args);
});
Options provided inside DbContextOptionsBuilder
are considered as defaults and will be applied for each execution. Besides WithClearChangeTrackerOnRetry
, you can provide any middleware to customize behavior within each context.ExecuteExtendedAsync
.