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.