Resiliência da conexão

A resiliência de conexão repete automaticamente os comandos de banco de dados com falha. O recurso pode ser usado com qualquer banco de dados fornecendo uma "estratégia de execução", que encapsula a lógica necessária para detectar falhas e repetir comandos. Os provedores de EF Core podem fornecer estratégias de execução adaptadas às suas condições específicas de falha do banco de dados e às políticas de repetição ideais.

Por exemplo, o provedor de SQL Server inclui uma estratégia de execução que é especificamente adaptada para SQL Server (incluindo SQL Azure). Ele reconhece os tipos de exceção que podem ser repetidos e tem padrões sensíveis para tentativas máximas, atraso entre repetições, etc.

Uma estratégia de execução é especificada ao configurar as opções para seu contexto. Normalmente, isso está no OnConfiguring método de seu contexto derivado:

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

ou em Startup.cs para um aplicativo ASP.NET Core:

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

Observação

Habilitar a repetição de falha faz com que o EF faça o buffer interno do ResultSet, o que pode aumentar significativamente os requisitos de memória para consultas que retornam conjuntos de resultados grandes. Consulte buffering e streaming para obter mais detalhes.

Estratégia de execução personalizada

Há um mecanismo para registrar uma estratégia de execução personalizada de sua preferência se você quiser alterar qualquer um dos padrões.

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

Estratégias e transações de execução

Uma estratégia de execução que repete automaticamente as falhas precisa ser capaz de reproduzir cada operação em um bloco de repetição que falha. Quando novas tentativas são habilitadas, cada operação executada por meio de EF Core se torna sua própria operação passível. Ou seja, cada consulta e cada chamada para SaveChanges() será repetida como uma unidade se ocorrer uma falha transitória.

No entanto, se o seu código iniciar uma transação usando BeginTransaction() você estiver definindo seu próprio grupo de operações que precisam ser tratadas como uma unidade, e tudo dentro da transação precisará ser reproduzido, ocorrerá uma falha. Você receberá uma exceção como a seguinte se tentar fazer isso ao usar uma estratégia de execução:

InvalidOperationException: a estratégia de execução configurada ' SqlServerRetryingExecutionStrategy ' não oferece suporte a transações iniciadas pelo usuário. Use a estratégia de execução retornada por 'DbContext.Database.CreateExecutionStrategy()' para executar todas as operações na transação como uma unidade repetível.

A solução é invocar manualmente a estratégia de execução com um delegado representando tudo o que precisa ser executado. Se ocorrer uma falha transitória, a estratégia de execução invocará o representante novamente.

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();
                }
            }
        });
}

Essa abordagem também pode ser usada com transações de ambiente.

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();
                }
            }
        });
}

Falha na confirmação da transação e o problema de Idempotência

Em geral, quando há uma falha de conexão, a transação atual é revertida. No entanto, se a conexão for descartada enquanto a transação estiver sendo confirmada, o estado resultante da transação será desconhecido.

Por padrão, a estratégia de execução tentará novamente a operação como se a transação fosse revertida, mas se não for o caso, isso resultará em uma exceção se o novo estado do banco de dados for incompatível ou puder levar à corrupção de dados se a operação não depender de um estado específico, por exemplo, ao inserir uma nova linha com valores de chave gerados automaticamente.

Há várias maneiras de lidar com isso.

Opção 1-fazer (quase) nada

A probabilidade de uma falha de conexão durante a confirmação da transação é baixa, portanto, pode ser aceitável para o aplicativo falhar apenas se essa condição realmente ocorrer.

No entanto, você precisa evitar o uso de chaves geradas pela loja para garantir que uma exceção seja lançada em vez de adicionar uma linha duplicada. Considere usar um valor GUID gerado pelo cliente ou um gerador de valor do lado do cliente.

Opção 2 – recriar o estado do aplicativo

  1. Descartar a atual DbContext .
  2. Crie um novo DbContext e restaure o estado do seu aplicativo a partir do banco de dados.
  3. Informe ao usuário que a última operação pode não ter sido concluída com êxito.

Opção 3 – adicionar verificação de estado

Para a maioria das operações que alteram o estado do banco de dados, é possível adicionar código que verifica se foi bem-sucedido. O EF fornece um método de extensão para tornar isso mais fácil IExecutionStrategy.ExecuteInTransaction .

Esse método começa e confirma uma transação e também aceita uma função no verifySucceeded parâmetro que é invocado quando ocorre um erro transitório durante a confirmação da transação.

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();
}

Observação

Aqui SaveChanges é invocado com acceptAllChangesOnSuccess definido como false para evitar a alteração do estado da Blog entidade para Unchanged se SaveChanges tiver sucesso. Isso permite repetir a mesma operação se a confirmação falhar e a transação for revertida.

Opção 4-rastrear manualmente a transação

Se você precisar usar chaves geradas pelo repositório ou precisar de uma maneira genérica de lidar com falhas de confirmação que não dependem da operação executada, cada transação poderá receber uma ID verificada quando a confirmação falhar.

  1. Adicione uma tabela ao banco de dados usado para acompanhar o status das transações.
  2. Insira uma linha na tabela no início de cada transação.
  3. Se a conexão falhar durante a confirmação, verifique a presença da linha correspondente no banco de dados.
  4. Se a confirmação for bem-sucedida, exclua a linha correspondente para evitar o crescimento da tabela.
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();
}

Observação

Certifique-se de que o contexto usado para a verificação tenha uma estratégia de execução definida, pois a conexão provavelmente falhará novamente durante a verificação se ela falhar durante a confirmação da transação.

Recursos adicionais