Implementowanie odpornych połączeń SQL platformy Entity Framework Core

Napiwek

Ta zawartość jest fragmentem książki eBook, architektury mikrousług platformy .NET dla konteneryzowanych aplikacji platformy .NET dostępnych na platformie .NET Docs lub jako bezpłatnego pliku PDF, który można odczytać w trybie offline.

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

W przypadku usługi Azure SQL DB platforma Entity Framework (EF) Core już zapewnia wewnętrzną odporność połączenia z bazą danych i logikę ponawiania prób. Należy jednak włączyć strategię wykonywania programu Entity Framework dla każdego DbContext połączenia, jeśli chcesz mieć odporne połączenia platformy EF Core.

Na przykład poniższy kod na poziomie połączenia platformy EF Core umożliwia odporne połączenia SQL, które są ponawiane, jeśli połączenie zakończy się niepowodzeniem.

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

Strategie wykonywania i jawne transakcje przy użyciu funkcji BeginTransaction i wielu obiektów DbContexts

Po włączeniu ponownych prób w połączeniach platformy EF Core każda operacja wykonywana przy użyciu programu EF Core staje się własną operacją z możliwością ponawiania prób. Każde zapytanie i każde wywołanie SaveChanges metody będą ponawiane jako jednostka, jeśli wystąpi błąd przejściowy.

Jeśli jednak kod inicjuje transakcję przy użyciu metody BeginTransaction, definiujesz własną grupę operacji, które muszą być traktowane jako jednostka. Wszystko wewnątrz transakcji musi zostać wycofane, jeśli wystąpi awaria.

Jeśli spróbujesz wykonać tę transakcję podczas korzystania ze strategii wykonywania ef (zasady ponawiania prób) i wywołasz SaveChanges z wielu obiektów DbContexts, otrzymasz wyjątek podobny do następującego:

System.InvalidOperationException: skonfigurowana strategia wykonywania "SqlServerRetryingExecutionStrategy" nie obsługuje transakcji inicjowanych przez użytkownika. Użyj strategii wykonywania zwróconej przez polecenie "DbContext.Database.CreateExecutionStrategy()", aby wykonać wszystkie operacje w transakcji jako jednostkę, którą można pobrać.

Rozwiązaniem jest ręczne wywołanie strategii wykonywania ef z delegatem reprezentującym wszystko, co należy wykonać. Jeśli wystąpi błąd przejściowy, strategia wykonywania ponownie wywoła delegata. Na przykład poniższy kod pokazuje, jak jest implementowany w eShopOnContainers z dwoma wieloma elementami DbContext (_catalogContext i IntegrationEventLogContext) podczas aktualizowania produktu, a następnie zapisywania obiektu ProductPriceChangedIntegrationEvent, który musi używać innej wartości DbContext.

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

DbContext Pierwszy element to _catalogContext , a drugi DbContext znajduje się w _catalogIntegrationEventService obiekcie . Akcja Zatwierdź jest wykonywana we wszystkich DbContext obiektach przy użyciu strategii wykonywania ef.

Aby osiągnąć to wiele DbContext zatwierdzeń, SaveEventAndCatalogContextChangesAsync klasa używa ResilientTransaction klasy, jak pokazano w poniższym kodzie:

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

Metoda ResilientTransaction.ExecuteAsync w zasadzie rozpoczyna transakcję z przekazanego DbContext (_catalogContext), a następnie używa EventLogService tej transakcji do zapisywania zmian z IntegrationEventLogContext , a następnie zatwierdza całą transakcję.

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

Dodatkowe zasoby