Данное руководство устарело. Актуальное руководство: Руководство по Entity Framework Core 7
В ситуации, когда множество пользователей одновременно имеют доступ к одинаковому набору данных и могут эти данные изменять, мы можем столкнуться с проблемой параллелизма. Например, два пользователя независимо друг от друга начнут редактировать один и тот же объект. И после сохранения объекта первым пользователем второй пользователь уже будет работать с неактуальными данными.
Для решения проблемы параллелизма Entity Framework Core предлагает использовать токены параллелизма или concurrency token.
Если свойство установлено в качестве concurrency token (токен параллелизма), Entity Framework перед сохранением изменений будет проверять его значение. Если значение отличается, то какой-то другой пользователь уже произвел изменения над данными. В этом случае EF Core применяет принцип оптимистичного параллелизма (optimistic concurrency), при котором, если сохраняемые данные уже были кем-то изменены, то выбрасывается ошибка.
Атрибут ConcurrencyCheck позволяет решить проблему параллелизма, когда с одной и той же записью в таблице могут работать одновременно несколько пользователей. Например:
public class User { public int Id { get; set; } [ConcurrencyCheck] public string Name { get; set; } } public class ApplicationContext : DbContext { public DbSet<User> Users { get; set; } public ApplicationContext() { Database.EnsureCreated(); } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=helloappdb;Trusted_Connection=True;"); } }
В обычном режиме Entity Framework Core при обновлении смотрит на Id и если Id записи в таблице совпадает с Id в передаваемой модели User, то строка в таблице обновляется. При использовании атрибута ConcurrencyCheck EF смотрит не только на Id, но и на исходное значение свойства Name. И если оно совпадает с тем, что имеется в таблице, то запись обновляется. Если же не совпадает (то есть кто-то уже успел обновить), то EF генерирует исключение DbUpdateConcurrencyException:
using (ApplicationContext db = new ApplicationContext()) { try { User user = db.Users.FirstOrDefault(); if(user !=null) { user.Name = "Bob"; db.SaveChanges(); } } catch(DbUpdateConcurrencyException ex) { } }
В Fluent API токен параллелизма настраивается с помощью метода IsConcurrencyToken():
public class ApplicationContext : DbContext { public DbSet<User> Users { get; set; } public ApplicationContext() { Database.EnsureCreated(); } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<User>().Property(b => b.Name).IsConcurrencyToken(); } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=helloappdb;Trusted_Connection=True;"); } } public class User { public int Id { get; set; } public string Name { get; set; } }
Другой механизм по отслеживанию изменений объекта в БД представляет атрибут Timestamp.
public class User { public int Id { get; set; } public string Name { get; set; } [Timestamp] public byte[] Timestamp { get; set; } }
Атрибут Timestamp указывает, что значение свойства Timestamp будет включаться в создаваемое Entity Frameworkом SQL-выражение WHERE при отправке в базу данных команд на обновление и удаление. То есть значение данного свойства генерируется при добавлении объекта, а также каждый раз, когда пользователь будет обновлять его. В качестве типа для свойства используется массив байтов.
И если два пользователя одновременно начнут редактировать одну и ту же строку, то после сохранения модели первым пользователем, второй пользователь получит исключение DbUpdateConcurrencyException, которое соответственно надо обработать.
Вместо атрибута мы можем использовать Fluent API:
public class User { public int Id { get; set; } public string Name { get; set; } public byte[] Timestamp { get; set; } } public class ApplicationContext : DbContext { public DbSet<User> Users { get; set; } public ApplicationContext() { Database.EnsureCreated(); } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<User>().Property(b => b.Timestamp).IsRowVersion(); } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=helloappdb;Trusted_Connection=True;"); } }