В EF Core можно задействовать тип данных timestamp из SQL Server, реализуя внутри сущности свойство TimeStamp (представляемое в C# как byte[]). Свойства сущностей, определенные с применением атрибута TimeStamp либо Fluent API, предназначены для добавления в конструкцию where при обновлении или удалении записей. Вместо того чтобы просто использовать значение (значения) первичного ключа, в конструкцию where генерируемого оператора SQL добавляется значение свойства timestamp, что ограничивает результаты записями, у которых совпадают значения первичного ключа и отметки времени. Если запись была обновлена другим пользователем (или системой), тогда значения отметок времени не совпадут, так что оператор update не обновит, а оператор delete не удалит запись. Вот пример запроса обновления, в котором применяется столбец TimeStamp:
UPDATE [Dbo].[Inventory] SET [Color] = N'Yellow'
WHERE [Id] = 1 AND [TimeStamp] = 0x000000000000081F;
Когда хранилище сообщает о количестве затронутых записей, отличающемся от количества записей, изменения которых ожидает ChangeTracker, исполняющая среда EF Core генерирует исключение DbUpdateConcurrencyException и выполняет откат всей транзакции. Экземпляр DbUpdateConcurrencyException содержит информацию о записях, которые не были сохранены, куда входят первоначальные значения (полученные в результате загрузки из базы данных) и текущие значения (после их обновления пользователем/системой). Кроме того, существует метод для получения текущих значений в базе данных (требующий еще одного обращения к серверу). Располагая настолько большим количеством информации, разработчик затем может обработать ошибку параллелизма так, как того требует приложение. Ниже приведен пример:
try
{
// Получить запись для автомобиля (неважно какую).
var car = Context.Cars.First();
// Обновить базу данных извне контекста.
Context.Database.ExecuteSqlInterpolated($"Update dbo.Inventory set Color='Pink' where Id = {car.Id}");
// Обновить запись для автомобиля в ChangeTracker
// и попробовать сохранить изменения.
car.Color = "Yellow";
Context.SaveChanges();
}
catch (DbUpdateConcurrencyException ex)
{
// Получить сущность, которую не удалось обновить.
var entry = ex.Entries[0];
/// Получить первоначальные значения (когда сущность была загружена).
PropertyValues originalProps = entry.OriginalValues;
// Получить текущие значения (обновленные кодом выше).
PropertyValues currentProps = entry.CurrentValues;
// Получить текущие значения из хранилища данных.
// Примечание: это требует еще одного обращения к базе данных
//PropertyValues databaseProps = entry.GetDatabaseValues();
}
Устойчивость подключений
Кратковременные ошибки трудны в отладке и еще более трудны в воспроизведении. К счастью, многие поставщики баз данных имеют внутренний механизм повтора для сбоев в системе баз данных (проблемы с tempdb, ограничения пользователей и т.д.), который может быть задействован EF Core. Для SQL Server кратковременные ошибки (согласно определению команды разработчиков СУБД) перехватываются экземпляром класса SqlServerRetryingExecutionStrategy, и если он включен в объекте производного от DbContext класса через DbContextOptions, то EF Core автоматически повторяет операцию до тех пор, пока не достигнет максимального предела повторов.
При работе с SQL Server доступен сокращенный метод, который можно использовать для включения SqlServerRetryingExecutionStrategy со всеми стандартными параметрами. Метод, который применяется с SqlServerOptions — это EnableRetryOnFailure():
public ApplicationDbContext CreateDbContext(string[] args)
{
var optionsBuilder = new DbContextOptionsBuilder