Dela via


Implementera elastiska Entity Framework Core SQL-anslutningar

Dricks

Det här innehållet är ett utdrag från eBook, .NET Microservices Architecture for Containerized .NET Applications, tillgängligt på .NET Docs eller som en kostnadsfri nedladdningsbar PDF som kan läsas offline.

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

För Azure SQL DB tillhandahåller Entity Framework (EF) Core redan intern databasanslutningsåterhämtning och logik för återförsök. Men du måste aktivera Entity Framework-körningsstrategin för varje DbContext anslutning om du vill ha elastiska EF Core-anslutningar.

Följande kod på EF Core-anslutningsnivån möjliggör till exempel elastiska SQL-anslutningar som görs om om anslutningen misslyckas.

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

Viktigt!

Microsoft rekommenderar att du använder det säkraste tillgängliga autentiseringsflödet. Om du ansluter till Azure SQL är hanterade identiteter för Azure-resurser den rekommenderade autentiseringsmetoden.

Körningsstrategier och explicita transaktioner med BeginTransaction och flera DbContexts

När återförsök aktiveras i EF Core-anslutningar blir varje åtgärd som du utför med EF Core en egen återförsöksbar åtgärd. Varje fråga och varje anrop till SaveChanges görs på nytt som en enhet om ett tillfälligt fel inträffar.

Men om koden initierar en transaktion med , BeginTransactiondefinierar du din egen grupp med åtgärder som måste behandlas som en enhet. Allt i transaktionen måste återställas om ett fel inträffar.

Om du försöker köra transaktionen när du använder en EF-körningsstrategi (återförsöksprincip) och du anropar SaveChanges från flera DbContexts får du ett undantag som det här:

System.InvalidOperationException: Den konfigurerade körningsstrategin "SqlServerRetryingExecutionStrategy" stöder inte användarinitierade transaktioner. Använd körningsstrategin som returneras av "DbContext.Database.CreateExecutionStrategy()" för att köra alla åtgärder i transaktionen som en enhet som kan göras om.

Lösningen är att manuellt anropa EF-körningsstrategin med ett ombud som representerar allt som behöver köras. Om det inträffar ett tillfälligt fel anropar körningsstrategin delegate igen. Följande kod visar till exempel hur den implementeras i eShopOnContainers med två flera DbContexts (_catalogContext och IntegrationEventLogContext) när du uppdaterar en produkt och sedan sparar objektet ProductPriceChangedIntegrationEvent, som måste använda en annan 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();
    }
}

Den första DbContext är _catalogContext och den andra DbContext finns i _catalogIntegrationEventService objektet. Incheckningsåtgärden utförs för alla DbContext objekt med hjälp av en EF-körningsstrategi.

För att uppnå denna flera DbContext incheckning SaveEventAndCatalogContextChangesAsync använder en ResilientTransaction klass, som visas i följande kod:

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

Metoden ResilientTransaction.ExecuteAsync börjar i princip en transaktion från den skickade DbContext (_catalogContext) och använder sedan transaktionen EventLogService för att spara ändringar från IntegrationEventLogContext och genomför sedan hela transaktionen.

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

Ytterligare resurser