Partilhar via


Implementar conexões resilientes do Entity Framework Core SQL

Gorjeta

Este conteúdo é um trecho do eBook, .NET Microservices Architecture for Containerized .NET Applications, disponível no .NET Docs ou como um PDF para download gratuito que pode ser lido offline.

.NET Microservices Architecture for Containerized .NET Applications eBook cover thumbnail.

Para o Banco de Dados SQL do Azure, o Entity Framework (EF) Core já fornece resiliência de conexão de banco de dados interno e lógica de repetição. Mas você precisa habilitar a estratégia de execução do Entity Framework para cada DbContext conexão se quiser ter conexões EF Core resilientes.

Por exemplo, o código a seguir no nível de conexão EF Core permite conexões SQL resilientes que são repetidas se a conexão falhar.

// Program.cs from any ASP.NET Core Web API
// Other code ...
builder.Services.AddDbContext<CatalogContext>(options =>
    {
        options.UseSqlServer(builder.Configuration["ConnectionString"],
        sqlServerOptionsAction: sqlOptions =>
        {
            sqlOptions.EnableRetryOnFailure(
            maxRetryCount: 10,
            maxRetryDelay: TimeSpan.FromSeconds(30),
            errorNumbersToAdd: null);
        });
    });

Estratégias de execução e transações explícitas usando BeginTransaction e vários DbContexts

Quando novas tentativas são habilitadas em conexões EF Core, cada operação executada usando o EF Core se torna sua própria operação retryable. Cada consulta e cada chamada para SaveChanges será repetida como uma unidade se ocorrer uma falha transitória.

No entanto, se o código iniciar uma transação usando BeginTransactiono , você está definindo seu próprio grupo de operações que precisam ser tratadas como uma unidade. Tudo dentro da transação tem que ser revertido se ocorrer uma falha.

Se você tentar executar essa transação ao usar uma estratégia de execução do EF (política de repetição) e chamar SaveChanges de vários DbContexts, obterá uma exceção como esta:

System.InvalidOperationException: A estratégia de execução configurada 'SqlServerRetryingExecutionStrategy' não suporta 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 recuperável.

A solução é invocar manualmente a estratégia de execução do EF com um delegado representando tudo o que precisa ser executado. Se ocorrer uma falha transitória, a estratégia de execução invocará o delegado novamente. Por exemplo, o código a seguir mostra como ele é implementado no eShopOnContainers com dois vários DbContexts (_catalogContext e o IntegrationEventLogContext) ao atualizar um produto e, em seguida, salvar o objeto ProductPriceChangedIntegrationEvent, que precisa usar um DbContext diferente.

public async Task<IActionResult> UpdateProduct(
    [FromBody]CatalogItem productToUpdate)
{
    // Other code ...

    var oldPrice = catalogItem.Price;
    var raiseProductPriceChangedEvent = oldPrice != productToUpdate.Price;

    // Update current product
    catalogItem = productToUpdate;

    // Save product's data and publish integration event through the Event Bus
    // if price has changed
    if (raiseProductPriceChangedEvent)
    {
        //Create Integration Event to be published through the Event Bus
        var priceChangedEvent = new ProductPriceChangedIntegrationEvent(
          catalogItem.Id, productToUpdate.Price, oldPrice);

       // Achieving atomicity between original Catalog database operation and the
       // IntegrationEventLog thanks to a local transaction
       await _catalogIntegrationEventService.SaveEventAndCatalogContextChangesAsync(
           priceChangedEvent);

       // Publish through the Event Bus and mark the saved event as published
       await _catalogIntegrationEventService.PublishThroughEventBusAsync(
           priceChangedEvent);
    }
    // Just save the updated product because the Product's Price hasn't changed.
    else
    {
        await _catalogContext.SaveChangesAsync();
    }
}

O primeiro DbContext é _catalogContext e o segundo DbContext está dentro do _catalogIntegrationEventService objeto. A ação Confirmar é executada em todos os DbContext objetos usando uma estratégia de execução do EF.

Para obter essa confirmação múltipla DbContext , o SaveEventAndCatalogContextChangesAsync usa uma ResilientTransaction classe, conforme mostrado no código a seguir:

public class CatalogIntegrationEventService : ICatalogIntegrationEventService
{
    //…
    public async Task SaveEventAndCatalogContextChangesAsync(
        IntegrationEvent evt)
    {
        // Use of an EF Core resiliency strategy when using multiple DbContexts
        // within an explicit BeginTransaction():
        // https://learn.microsoft.com/ef/core/miscellaneous/connection-resiliency
        await ResilientTransaction.New(_catalogContext).ExecuteAsync(async () =>
        {
            // Achieving atomicity between original catalog database
            // operation and the IntegrationEventLog thanks to a local transaction
            await _catalogContext.SaveChangesAsync();
            await _eventLogService.SaveEventAsync(evt,
                _catalogContext.Database.CurrentTransaction.GetDbTransaction());
        });
    }
}

O ResilientTransaction.ExecuteAsync método basicamente inicia uma transação a partir do passado DbContext (_catalogContext) e, em seguida, faz o EventLogService uso dessa transação para salvar as alterações do IntegrationEventLogContext e, em seguida, confirma toda a transação.

public class ResilientTransaction
{
    private DbContext _context;
    private ResilientTransaction(DbContext context) =>
        _context = context ?? throw new ArgumentNullException(nameof(context));

    public static ResilientTransaction New (DbContext context) =>
        new ResilientTransaction(context);

    public async Task ExecuteAsync(Func<Task> action)
    {
        // Use of an EF Core resiliency strategy when using multiple DbContexts
        // within an explicit BeginTransaction():
        // https://learn.microsoft.com/ef/core/miscellaneous/connection-resiliency
        var strategy = _context.Database.CreateExecutionStrategy();
        await strategy.ExecuteAsync(async () =>
        {
            await using var transaction = await _context.Database.BeginTransactionAsync();
            await action();
            await transaction.CommitAsync();
        });
    }
}

Recursos adicionais