Resiliency during SaveChanges in EntityFrameworkCore

991 Views Asked by At

I want to ensure that when I do a context.SaveChanges(), this is retried because the database might be temporarily down.

So far all I've found involves writing a lot of code that I'd then need to maintain, so is there something ready, an out-of-the-box tool, that I can use for resiliency?

3

There are 3 best solutions below

14
On BEST ANSWER

I've created a small library called ResilientSaveChanges.EFCore that allows resilient context.SaveChanges / SaveChangesAsync in Entity Framework Core, logging of long-running transactions and limiting of concurrent SaveChanges. It's straight to the point.

Available on GitHub and NuGet. Tried and tested in production on multiple private projects.

4
On

Yes, connection resiliency is available in EF Core. For MySQL, it's available through the Pomelo driver's EnabelRetryOnFailure() option. The Github Blame shows this was added 5 years ago which is a bit surprising. An overload added 3 years ago allows specifying extra errors to retry.

This code taken from one of the integration tests shows how it's used:

        services.AddDbContextPool<AppDb>(
            options => options.UseMySql(
                GetConnectionString(),
                AppConfig.ServerVersion,
                mysqlOptions =>
                {
                    mysqlOptions.MaxBatchSize(AppConfig.EfBatchSize);
                    mysqlOptions.UseNewtonsoftJson();

                    if (AppConfig.EfRetryOnFailure > 0)
                    {
                        mysqlOptions.EnableRetryOnFailure(AppConfig.EfRetryOnFailure, TimeSpan.FromSeconds(5), null);
                    }
                }
        ));

Without parameters EnableRetryOnFailure() uses the default retry count and maximum delay which are 6 and 30 seconds.

The third parameter is an ICollection<int> of additional errors to retry.

By default, the MySqlTransientExceptionDetector class specifies that only transient exceptions are retried, ie those that have the IsTransient property set, or timeout exceptions.

    public static bool ShouldRetryOn([NotNull] Exception ex)
        => ex is MySqlException mySqlException
            ? mySqlException.IsTransient
            : ex is TimeoutException;
0
On

As @PanagiotisKanavos already pointed out, Pomelo already has connection resiliency support.

The simplest way to use it, is to enable the default strategy:

dbContextOptions.UseMySql(
    connectionString,
    serverVersion,
    mySqlOptions => mySqlOptions.EnableRetryOnFailure())

It will retry up to six times, incrementally waiting for longer periods between retries (but not longer than 30 seconds).


If you want to configure the retry strategy, use the following overload instead:

dbContextOptions.UseMySql(
    connectionString,
    serverVersion,
    mySqlOptions => mySqlOptions
        .EnableRetryOnFailure(
            maxRetryCount: 3,
            maxRetryDelay: TimeSpan.FromSeconds(15),
            errorNumbersToAdd: null))

If you want full control over all aspects of the execution strategy, you can inject your own implementation (that either inherits from MySqlExecutionStrategy or directly implements IExecutionStrategy):

dbContextOptions.UseMySql(
    connectionString,
    serverVersion,
    mySqlOptions => mySqlOptions
        .ExecutionStrategy(dependencies => new YourExecutionStrategy(dependencies))