diff --git a/EntityFrameworkCore.Console/Program.cs b/EntityFrameworkCore.Console/Program.cs index 2ddd15e..6849369 100644 --- a/EntityFrameworkCore.Console/Program.cs +++ b/EntityFrameworkCore.Console/Program.cs @@ -22,6 +22,23 @@ context.Database.EnsureCreated(); // Rather than raise: "System.InvalidOperationException: Sequence contains no elements." // var firstCoach = await context.Coaches.FirstOrDefaultAsync(); +async Task ConcurrencyCheckExample() +{ + var team = context.Teams.Find(1); + team.Name = "New name"; + + // To simulate a concurrency change, add a breakpoint and + // manually update the GUID in the database before SaveChangesAsync is called. + // See also: https://learn.microsoft.com/en-us/ef/core/saving/concurrency?tabs=data-annotations + try + { + await context.SaveChangesAsync(); + } + catch (DbUpdateConcurrencyException ex) + { + Console.WriteLine(ex.Message); + } +} async Task TransactionWithSavepoint() { diff --git a/EntityFrameworkCore.Data/Configurations/TeamConfiguration.cs b/EntityFrameworkCore.Data/Configurations/TeamConfiguration.cs index 9dc1899..3ac38a0 100644 --- a/EntityFrameworkCore.Data/Configurations/TeamConfiguration.cs +++ b/EntityFrameworkCore.Data/Configurations/TeamConfiguration.cs @@ -13,6 +13,12 @@ public class TeamConfiguration : IEntityTypeConfiguration // example composite key configuration. // builder.HasIndex(λ => new { λ.CoachId, λ.LeagueId }).IsUnique(); + // SQL Server method to setup concurrency. + // builder.Property(λ => λ.Version).IsRowVersion(); + + // SQLite (and other) method to setup concurrency. See also BaseDomain model. + builder.Property(λ => λ.Version).IsConcurrencyToken(); + builder.Property(λ => λ.Name).HasMaxLength(100).IsRequired(); builder.HasMany(m => m.HomeMatches) // A team has many home matches. diff --git a/EntityFrameworkCore.Data/DeadBallZoneLeagueDbContext.cs b/EntityFrameworkCore.Data/DeadBallZoneLeagueDbContext.cs index b54e3e5..1510287 100644 --- a/EntityFrameworkCore.Data/DeadBallZoneLeagueDbContext.cs +++ b/EntityFrameworkCore.Data/DeadBallZoneLeagueDbContext.cs @@ -65,6 +65,9 @@ public class DeadBallZoneLeagueDbContext : DbContext entry.Entity.CreatedDate = DateTime.UtcNow; entry.Entity.CreatedBy = "Sample User"; } + + // Update the concurrency token. + entry.Entity.Version = Guid.NewGuid(); } return base.SaveChangesAsync(cancellationToken); } diff --git a/EntityFrameworkCore.Domain/BaseDomainModel.cs b/EntityFrameworkCore.Domain/BaseDomainModel.cs index 5aa641c..6115f8b 100644 --- a/EntityFrameworkCore.Domain/BaseDomainModel.cs +++ b/EntityFrameworkCore.Domain/BaseDomainModel.cs @@ -1,4 +1,6 @@ -namespace EntityFrameworkCore.Domain; +using System.ComponentModel.DataAnnotations; + +namespace EntityFrameworkCore.Domain; // Since C#11, you do not need to wrap in accolades anymore. public abstract class BaseDomainModel { @@ -7,4 +9,12 @@ public abstract class BaseDomainModel public DateTime ModifiedDate { get; set;} public string? CreatedBy { get; set; } public string? ModifiedBy { get; set; } + + // SQL Server method to setup concurrency column. + // [Timestamp] + // public byte[] Version { get; set; } + + // SQLite (and other) method to setup concurrency column. + [ConcurrencyCheck] + public Guid Version { get; set; } }