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.
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);
});
});
Ważne
Firma Microsoft zaleca korzystanie z najbezpieczniejszego dostępnego przepływu uwierzytelniania. Jeśli łączysz się z usługą Azure SQL, tożsamości zarządzane dla zasobów platformy Azure to zalecana metoda uwierzytelniania.
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
Odporność połączenia i przechwytywanie poleceń za pomocą platformy EF w aplikacji ASP.NET MVC
https://learn.microsoft.com/aspnet/mvc/overview/getting-started/getting-started-with-ef-using-mvc/connection-resiliency-and-command-interception-with-the-entity-framework-in-an-asp-net-mvc-applicationCesar de la Torre. Korzystanie z odpornych połączeń i transakcji SQL Platformy Entity Framework Core
https://devblogs.microsoft.com/cesardelatorre/using-resilient-entity-framework-core-sql-connections-and-transactions-retries-with-exponential-backoff/