Resistencia de conexión

La resistencia de la conexión vuelve a inicialmente los comandos de base de datos con errores. La característica se puede usar con cualquier base de datos si se proporciona una "estrategia de ejecución", que encapsula la lógica necesaria para detectar errores y volver a intentar comandos. EF Core proveedores de recursos pueden proporcionar estrategias de ejecución adaptadas a sus condiciones de error de base de datos específicas y directivas de reintento óptimas.

Por ejemplo, el proveedor de SQL Server incluye una estrategia de ejecución específicamente adaptada a SQL Server (incluidos SQL Azure). Es consciente de los tipos de excepción que se pueden reintetrar y tiene valores predeterminados razonables para los reintentos máximos, el retraso entre reintentos, etc.

Se especifica una estrategia de ejecución al configurar las opciones para el contexto. Esto suele estar en OnConfiguring el método del contexto derivado:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder
        .UseSqlServer(
            @"Server=(localdb)\mssqllocaldb;Database=EFMiscellanous.ConnectionResiliency;Trusted_Connection=True",
            options => options.EnableRetryOnFailure());
}

o en Startup.cs para una ASP.NET Core aplicación:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<PicnicContext>(
        options => options.UseSqlServer(
            "<connection string>",
            providerOptions => providerOptions.EnableRetryOnFailure()));
}

Nota

Al habilitar el reintento en caso de error, EF almacena internamente en búfer el conjunto de resultados, lo que puede aumentar significativamente los requisitos de memoria para las consultas que devuelven conjuntos de resultados grandes. Consulte almacenamiento en búfer y streaming para obtener más detalles.

Estrategia de ejecución personalizada

Hay un mecanismo para registrar una estrategia de ejecución personalizada propia si desea cambiar cualquiera de los valores predeterminados.

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder
        .UseMyProvider(
            "<connection string>",
            options => options.ExecutionStrategy(...));
}

Estrategias de ejecución y transacciones

Una estrategia de ejecución que reintenta automáticamente los errores debe poder reproducir cada operación en un bloque de reintentos que produce un error. Cuando se habilitan los reintentos, cada operación que se realiza a través EF Core se convierte en su propia operación reintentable. Es decir, cada consulta y cada llamada a se reintetrán como una SaveChanges() unidad si se produce un error transitorio.

Sin embargo, si el código inicia una transacción mediante , está definiendo su propio grupo de operaciones que deben tratarse como una unidad, y todo lo que hay dentro de la transacción debe reproducirse en caso de que se produzca un BeginTransaction() error. Recibirá una excepción como la siguiente si intenta hacerlo al usar una estrategia de ejecución:

InvalidOperationException: la estrategia de ejecución configurada "SqlServerRetryingExecutionStrategy" no admite transacciones iniciadas por el usuario. Use la estrategia de ejecución que devuelve "DbContext.Database.CreateExecutionStrategy()" para ejecutar todas las operaciones en la transacción como una unidad que se puede reintentar.

La solución es invocar manualmente la estrategia de ejecución con un delegado que represente todo lo que se debe ejecutar. Si se produce un error transitorio, la estrategia de ejecución vuelve a invocar al delegado.

using (var db = new BloggingContext())
{
    var strategy = db.Database.CreateExecutionStrategy();

    strategy.Execute(
        () =>
        {
            using (var context = new BloggingContext())
            {
                using (var transaction = context.Database.BeginTransaction())
                {
                    context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
                    context.SaveChanges();

                    context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/visualstudio" });
                    context.SaveChanges();

                    transaction.Commit();
                }
            }
        });
}

Este enfoque también se puede usar con transacciones ambientales.

using (var context1 = new BloggingContext())
{
    context1.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/visualstudio" });

    var strategy = context1.Database.CreateExecutionStrategy();

    strategy.Execute(
        () =>
        {
            using (var context2 = new BloggingContext())
            {
                using (var transaction = new TransactionScope())
                {
                    context2.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
                    context2.SaveChanges();

                    context1.SaveChanges();

                    transaction.Complete();
                }
            }
        });
}

Error de confirmación de transacción y problema de idempotencia

En general, cuando se produce un error de conexión, se revierte la transacción actual. Sin embargo, si se descarta la conexión mientras se confirma la transacción, se desconoce el estado resultante de la transacción.

De forma predeterminada, la estrategia de ejecución volverá a intentar la operación como si la transacción se revierte, pero si no es así, se producirá una excepción si el nuevo estado de la base de datos es incompatible o podría provocar daños en los datos si la operación no se basa en un estado determinado, por ejemplo, al insertar una nueva fila con valores de clave generados automáticamente.

Hay varias maneras de abordar esto.

Opción 1: hacer (casi) nada

La probabilidad de que se produzca un error de conexión durante la confirmación de la transacción es baja, por lo que puede ser aceptable que la aplicación simplemente produzca un error si se produce realmente esta condición.

Sin embargo, debe evitar el uso de claves generadas por el almacén para asegurarse de que se produce una excepción en lugar de agregar una fila duplicada. Considere la posibilidad de usar un valor GUID generado por el cliente o un generador de valores del lado cliente.

Opción 2: Recompilar el estado de la aplicación

  1. Descarte el objeto DbContext actual.
  2. Cree un nuevo y DbContext restaure el estado de la aplicación desde la base de datos.
  3. Informe al usuario de que es posible que la última operación no se haya completado correctamente.

Opción 3: Agregar comprobación de estado

Para la mayoría de las operaciones que cambian el estado de la base de datos, es posible agregar código que comprueba si se ha hecho correctamente. EF proporciona un método de extensión para facilitar esto: IExecutionStrategy.ExecuteInTransaction .

Este método comienza y confirma una transacción y también acepta una función en el parámetro que se invoca cuando se produce un error transitorio verifySucceeded durante la confirmación de la transacción.

using (var db = new BloggingContext())
{
    var strategy = db.Database.CreateExecutionStrategy();

    var blogToAdd = new Blog { Url = "http://blogs.msdn.com/dotnet" };
    db.Blogs.Add(blogToAdd);

    strategy.ExecuteInTransaction(
        db,
        operation: context => { context.SaveChanges(acceptAllChangesOnSuccess: false); },
        verifySucceeded: context => context.Blogs.AsNoTracking().Any(b => b.BlogId == blogToAdd.BlogId));

    db.ChangeTracker.AcceptAllChanges();
}

Nota

Aquí se invoca con establecido en para evitar cambiar el estado SaveChanges de la entidad a si se realiza acceptAllChangesOnSuccessfalseBlogUnchangedSaveChanges correctamente. Esto permite reintentar la misma operación si se produce un error en la confirmación y se revierte la transacción.

Opción 4: realizar un seguimiento manual de la transacción

Si necesita usar claves generadas por el almacén o necesita una manera genérica de controlar los errores de confirmación que no dependan de la operación realizada, a cada transacción se le podría asignar un identificador que se comprueba cuando se produce un error en la confirmación.

  1. Agregue una tabla a la base de datos utilizada para realizar un seguimiento del estado de las transacciones.
  2. Inserte una fila en la tabla al principio de cada transacción.
  3. Si se produce un error en la conexión durante la confirmación, compruebe la presencia de la fila correspondiente en la base de datos.
  4. Si la confirmación se realiza correctamente, elimine la fila correspondiente para evitar el crecimiento de la tabla.
using (var db = new BloggingContext())
{
    var strategy = db.Database.CreateExecutionStrategy();

    db.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });

    var transaction = new TransactionRow { Id = Guid.NewGuid() };
    db.Transactions.Add(transaction);

    strategy.ExecuteInTransaction(
        db,
        operation: context => { context.SaveChanges(acceptAllChangesOnSuccess: false); },
        verifySucceeded: context => context.Transactions.AsNoTracking().Any(t => t.Id == transaction.Id));

    db.ChangeTracker.AcceptAllChanges();
    db.Transactions.Remove(transaction);
    db.SaveChanges();
}

Nota

Asegúrese de que el contexto usado para la comprobación tiene una estrategia de ejecución definida, ya que es probable que la conexión vuelva a producir un error durante la comprobación si se ha dado error durante la confirmación de la transacción.

Recursos adicionales