Sdílet prostřednictvím


Implementace odolných připojení Entity Framework Core SQL

Tip

Tento obsah je výňatek z eBooku, architektury mikroslužeb .NET pro kontejnerizované aplikace .NET, které jsou k dispozici na .NET Docs nebo jako zdarma ke stažení PDF, které lze číst offline.

Architektura mikroslužeb .NET pro kontejnerizované eBooky aplikací .NET

Pro Azure SQL DB už Entity Framework (EF) Core poskytuje interní odolnost připojení k databázi a logiku opakování. Pokud ale chcete mít odolná připojení EF Core, musíte pro každé DbContext připojení povolit strategii provádění entity Framework.

Například následující kód na úrovni připojení EF Core umožňuje odolná připojení SQL, která se budou opakovat, pokud se připojení nezdaří.

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

Důležité

Microsoft doporučuje používat nejbezpečnější dostupný tok ověřování. Pokud se připojujete k Azure SQL, spravované identity pro prostředky Azure se doporučují metodou ověřování.

Strategie provádění a explicitní transakce pomocí BeginTransaction a více DbContexts

Pokud se v připojeních EF Core povolí opakování, každá operace, kterou provádíte pomocí EF Core, se stane vlastní retryable operací. Každý dotaz a každé volání SaveChanges se bude opakovat jako jednotka, pokud dojde k přechodnému selhání.

Pokud však kód inicializuje transakci pomocí BeginTransaction, definujete vlastní skupinu operací, které je potřeba považovat za jednotku. Vše uvnitř transakce se musí vrátit zpět, pokud dojde k selhání.

Pokud se pokusíte tuto transakci spustit při použití strategie provádění EF (zásady opakování) a zavoláte SaveChanges z více dbContexts, zobrazí se výjimka podobná této:

System.InvalidOperationException: Nakonfigurovaná strategie spouštění SqlServerRetryingExecutionStrategy nepodporuje transakce iniciované uživatelem. Použijte strategii provádění vrácenou dbContext.Database.CreateExecutionStrategy() ke spuštění všech operací v transakci jako opakovatelné jednotky.

Řešením je ručně vyvolat strategii provádění EF s delegátem představujícím vše, co je potřeba provést. Pokud dojde k přechodnému selhání, strategie provádění znovu vyvolá delegáta. Následující kód například ukazuje, jak se implementuje v eShopOnContainers se dvěma více DbContexts (_catalogContext a IntegrationEventLogContext) při aktualizaci produktu a uložení objektu ProductPriceChangedIntegrationEvent, který potřebuje použít jiný 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();
    }
}

První DbContext je a druhý DbContext je uvnitř objektu _catalogIntegrationEventService _catalogContext. Akce Potvrzení se provádí napříč všemi DbContext objekty pomocí strategie provádění EF.

K dosažení tohoto více DbContext potvrzení SaveEventAndCatalogContextChangesAsync používá ResilientTransaction třídu, jak je znázorněno v následujícím kódu:

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 v podstatě zahájí transakci z předaného DbContext (_catalogContext) a pak provede EventLogService použití této transakce k uložení změn z IntegrationEventLogContext a pak potvrdí celou transakci.

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

Další materiály