Saturday, 28 June 2025

Setting up connection resiliency for Entity Framework

In Entity Framework, it is possible to add more connection resiliency. This can be done for example if you are working against a more unstable database connection, maybe because the database is served in the Cloud and/or is not scaled properly to its load. Whatever reason, it is possible to add more connection resiliency. The connection resiliency can be used in other scenarios that just SQL servers hosted in Azure, such as On-Premise databases. It should add more resiliency and stability for scenarios where connections to database needs to be improved. This could also be due to mobile clients being moved in and out of areas with good network access, such as within buildings and factories on different levels trying to access a wireless connection that connects to a database. The code in this article is available in my Github repo here:

https://github.com/toreaurstadboss/BulkOperationsEntityFramework

first off, the ExecutionStrategy is set up. A DbConfiguration is added to set this up.

ApplicationDbModelConfiguration.cs



using System;
using System.Data.Entity;
using System.Data.Entity.SqlServer;

namespace BulkOperationsEntityFramework
{
    public class ApplicationDbConfiguration : DbConfiguration
    {

        public ApplicationDbConfiguration()
        {
            SetExecutionStrategy(SqlProviderServices.ProviderInvariantName, () =>
             new CustomSqlAzureExecutionStrategy(maxRetryCount: 10, maxDelay: TimeSpan.FromSeconds(5))); //note : max total delay of retries is 30 seconds per default in SQL Server
        }

    }

}


In EF Core 8 (using Entity Framework with .NET 8, you can set it up like this :

Program.cs




    public class ApplicationDbContext : DbContext
    {
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(
                "DefaultConnection",
                sqlOptions =>
                {
                    sqlOptions.EnableRetryOnFailure(
                        maxRetryCount: 10,
                        maxRetryDelay: TimeSpan.FromSeconds(5),
                        errorNumbersToAdd: null
                    );
                });
        }



Setting up the interval strategy

The CustomSqlAzureExecutionStrategy inherits from the SqlAzureExecutionStrategy. delay = min ( maxDelay , random × ( 2 retryCount 1 ) × baseDelay ) The default base delay in Entity Framework is to wait one second, so the next wait time will be about 2 seconds, then the next delays will quickly grow up the max wait time of five seconds. The custom sql azure execution strategy implementation inherits from SqlAzureExecutionStrategy.

CustomSqlAzureExecutionStrategy.cs



using System;
using System.Data.Entity.SqlServer;

namespace BulkOperationsEntityFramework
{

    public class CustomSqlAzureExecutionStrategy : SqlAzureExecutionStrategy
    {

        [ThreadStatic]
        private static int _currentRetryCount = 0;

        public CustomSqlAzureExecutionStrategy(int maxRetryCount, TimeSpan maxDelay)
        : base(maxRetryCount, maxDelay) { }

        protected override bool ShouldRetryOn(Exception ex)
        {
            _currentRetryCount++;
            Console.WriteLine($"{nameof(CustomSqlAzureExecutionStrategy)}: Retry-count within thread: {_currentRetryCount}");
            Log.Information("{Class}: Retry-count within thread: {RetryCount} {ExceptionType}", nameof(CustomSqlAzureExecutionStrategy), _currentRetryCount, ex.GetType().Name);

            return base.ShouldRetryOn(ex) || ex is SimulatedTransientSqlException;
        }

    }

}


Of course, just logging out to console probably is not a very elegant solution, and it could instead be logged out to for example SeriLog, which is used in the line with the Log.Information call. The SimulatedTransientSqlException looks like this:

SimulatedTransientSqlException.cs



 public class SimulatedTransientSqlException : Exception
 {
     public SimulatedTransientSqlException()
     : base("Simulated transient SQL exception.") { }
 }
 

The following db interceptor is added to simulate transient failures happening, at 10% chance of it happening.

TransientFailureInterceptor.cs



using System;
using System.Data.Common;
using System.Data.Entity.Infrastructure.Interception;
using System.Diagnostics;

namespace BulkOperationsEntityFramework
{

    public class TransientFailureInterceptor : DbCommandInterceptor
    {
        private static readonly Random _random = new Random();

        public override void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
        {
            SimulateTransientFailure(interceptionContext);
            base.ReaderExecuting(command, interceptionContext);
        }

        public override void ScalarExecuting(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
        {
            SimulateTransientFailure(interceptionContext);
            base.ScalarExecuting(command, interceptionContext);
        }

        public override void NonQueryExecuting(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
        {
            SimulateTransientFailure(interceptionContext);
            base.NonQueryExecuting(command, interceptionContext);
        }

        private void SimulateTransientFailure<TResult>(DbCommandInterceptionContext<TResult> context)
        {
            // Simulate a transient failure 10% of the time
            double r = _random.NextDouble();
            if (r < 0.1)
            {
                var ex = new SimulatedTransientSqlException();
                string info = "Throwing a transient SqlException. ";
                Trace.WriteLine($"{info} {ex.ToString()}");
                context.Exception = ex;
            }
        }
    }

    public class SimulatedTransientSqlException : Exception
    {
        public SimulatedTransientSqlException()
        : base("Simulated transient SQL exception.") { }
    }

}


Next up connecting the dots in the DbContext, setting up the db configuration

ApplicationDbContext.cs



[DbConfigurationType(typeof(ApplicationDbConfiguration))]
public class ApplicationDbContext : DbContext
{

    static ApplicationDbContext()
    {
        if (!AppDomain.CurrentDomain.GetAssemblies().Any(a => a.FullName.StartsWith("Effort")))
        {
            DbInterception.Add(new TransientFailureInterceptor()); //add an interceptor that simulates a transient connection error occuring (30% chance of it happening)
            DbInterception.Add(new SerilogCommandInterceptor()); //do not add logging if EF6 Effor is used (for unit testing)
        }
    }
    
    //more code..


Also note that you usually do not want to add the TransientFailureInterceptor, it is just added for testing. You could for example add a boolean property on your DbContext to set if you are testing out connection resiliency and add the TransientFailureInterceptor when you can to test it, or provide a public method to add the TransientfailureInterceptor, and remove it afterwards if desired. Within a Test-project, you should be able to test out connection resiliency.

Share this article on LinkedIn.

No comments:

Post a Comment