Share via


Tolerante Entity Framework Core SQL-verbindingen implementeren

Tip

Deze inhoud is een fragment uit het eBook, .NET Microservices Architecture for Containerized .NET Applications, beschikbaar op .NET Docs of als een gratis downloadbare PDF die offline kan worden gelezen.

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

Voor Azure SQL DB biedt Entity Framework (EF) Core al tolerantie voor interne databaseverbindingen en logica voor opnieuw proberen. Maar u moet de uitvoeringsstrategie van Entity Framework voor elke DbContext verbinding inschakelen als u flexibele EF Core-verbindingen wilt hebben.

De volgende code op het niveau van de EF Core-verbinding maakt bijvoorbeeld tolerante SQL-verbindingen mogelijk die opnieuw worden geprobeerd als de verbinding mislukt.

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

Uitvoeringsstrategieën en expliciete transacties met Behulp van BeginTransaction en meerdere DbContexts

Wanneer nieuwe pogingen zijn ingeschakeld in EF Core-verbindingen, wordt elke bewerking die u uitvoert met EF Core een eigen bewerking die opnieuw kan worden geprobeerd. Elke query en elke aanroep worden SaveChanges opnieuw geprobeerd als een eenheid als er een tijdelijke fout optreedt.

Als uw code echter een transactie initieert met behulp BeginTransactionvan, definieert u uw eigen groep bewerkingen die als een eenheid moeten worden behandeld. Alles binnen de transactie moet worden teruggedraaid als er een fout optreedt.

Als u die transactie probeert uit te voeren wanneer u een EF-uitvoeringsstrategie (beleid voor opnieuw proberen) gebruikt en u aanroept SaveChanges vanuit meerdere DbContexts, krijgt u een uitzondering zoals deze:

System.InvalidOperationException: De geconfigureerde uitvoeringsstrategie 'SqlServerRetryingExecutionStrategy' biedt geen ondersteuning voor door de gebruiker geïnitieerde transacties. Gebruik de uitvoeringsstrategie die wordt geretourneerd door DbContext.Database.CreateExecutionStrategy()) om alle bewerkingen in de transactie uit te voeren als een ophaalbare eenheid.

De oplossing is om de EF-uitvoeringsstrategie handmatig aan te roepen met een gemachtigde die alles vertegenwoordigt dat moet worden uitgevoerd. Als er een tijdelijke fout optreedt, roept de uitvoeringsstrategie de gemachtigde opnieuw aan. De volgende code laat bijvoorbeeld zien hoe deze wordt geïmplementeerd in eShopOnContainers met twee meerdere DbContexts (_catalogContext en integrationEventLogContext) bij het bijwerken van een product en vervolgens het object ProductPriceChangedIntegrationEvent opslaat, dat een andere DbContext moet gebruiken.

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

De eerste DbContext is _catalogContext en de tweede DbContext bevindt zich binnen het _catalogIntegrationEventService object. De doorvoeractie wordt uitgevoerd voor alle DbContext objecten met behulp van een EF-uitvoeringsstrategie.

Voor deze meervoudige DbContext doorvoer maakt de SaveEventAndCatalogContextChangesAsync toepassing gebruik van een ResilientTransaction klasse, zoals wordt weergegeven in de volgende code:

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

De ResilientTransaction.ExecuteAsync methode begint in feite een transactie van de doorgegeven DbContext (_catalogContext) en maakt vervolgens het EventLogService gebruik van die transactie om wijzigingen van de IntegrationEventLogContext transactie op te slaan en vervolgens de hele transactie door te voeren.

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

Aanvullende bronnen