Menerapkan sambungan SQL Core Kerangka Kerja Entitas yang tangguh

Tip

Konten ini adalah kutipan dari eBook, .NET Microservices Architecture for Containerized .NET Applications, tersedia di .NET Docs atau sebagai PDF yang dapat diunduh gratis dan dapat dibaca secara offline.

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

Untuk Azure SQL Database, Entity Kerangka Kerja (EF) Core sudah menyediakan ketahanan sambungan database internal dan logika coba lagi. Tetapi Anda perlu mengaktifkan strategi eksekusi Kerangka Kerja Entitas untuk setiap DbContext sambungan jika Anda ingin memiliki sambungan Inti EF yang tangguh.

Misalnya, kode berikut di tingkat sambungan Inti EF memungkinkan sambungan SQL tangguh yang dicoba kembali jika sambungan gagal.

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

Strategi eksekusi dan transaksi eksplisit menggunakan BeginTransaction dan beberapa DbContexts

Ketika percobaan ulang diaktifkan dalam sambungan Inti EF, setiap operasi yang Anda lakukan menggunakan Inti EF menjadi operasi yang dapat diulang sendiri. Setiap kueri dan setiap panggilan ke SaveChanges akan dicoba kembali sebagai unit jika terjadi kegagalan sementara.

Tetapi, jika kode Anda memulai transaksi menggunakan BeginTransaction, Anda menentukan grup operasi Anda sendiri yang perlu ditangani sebagai unit. Segala sesuatu di dalam transaksi harus digulung balik jika terjadi kegagalan.

Jika Anda mencoba menjalankan transaksi tersebut saat menggunakan strategi eksekusi EF (kebijakan coba lagi) dan Anda memanggil SaveChanges dari beberapa DbContexts, Anda akan mendapatkan pengecualian seperti ini:

System.InvalidOperationException: Strategi eksekusi yang dikonfigurasi 'SqlServerRetryingExecutionStrategy' tidak mendukung transaksi yang dimulai pengguna. Gunakan strategi eksekusi yang dikembalikan oleh 'DbContext.Database.CreateExecutionStrategy()' untuk menjalankan semua operasi dalam transaksi sebagai unit yang dapat dicoba kembali.

Solusinya adalah memanggil strategi eksekusi EF secara manual dengan delegasi yang mewakili semua yang perlu dijalankan. Jika terjadi kegagalan sementara, strategi eksekusi akan memanggil delegasi lagi. Misalnya, kode berikut menunjukkan bagaimana penerapannya di eShopOnContainers dengan dua beberapa DbContexts (_catalogContext dan IntegrationEventLogContext) saat memperbarui produk lalu menyimpan objek ProductPriceChangedIntegrationEvent, yang perlu menggunakan DbContext berbeda.

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 pertama adalah _catalogContext dan DbContext kedua berada dalam objek _catalogIntegrationEventService. Tindakan Penerapan dilakukan di semua objek DbContext menggunakan strategi eksekusi EF.

Untuk mencapai beberapa penerapan DbContext ini, SaveEventAndCatalogContextChangesAsync menggunakan kelas ResilientTransaction, seperti yang ditunjukkan dalam kode berikut:

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

Metode ResilientTransaction.ExecuteAsync ini pada dasarnya memulai transaksi dari yang diteruskan DbContext (_catalogContext) lalu menjadikan EventLogService menggunakan transaksi tersebut untuk menyimpan perubahan dari IntegrationEventLogContext dan kemudian melakukan seluruh transaksi.

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

Sumber daya tambahan