I have a case where I would like to add a concurrency token handling in my EntityFrameworkCore 8+ application. I have an entity named User
(stripped down for simplicity purposes) to which I added a Version
:
[Table("users")]
public class User
{
[Key]
[Column("id")]
public long Id { get; set; }
[Column("name")]
public required string Name { get; set; }
[Column("password_hash")]
public required string PasswordHash { get; set; }
/// <remarks>
/// This property gets mapped to the PostgreSQL xmin system column for the sake of enabling concurrency on the given entity.
/// </remarks>
[Timestamp]
public uint Version { get; set; }
}
From what I managed to understand, the Version
column should be mapped to the xmin
PostgreSQL column and it should NOT be included in the EF Core migrations. However, if I create a new migration, the migration contains a migrationBuilder.AddColumn
call:
migrationBuilder.AddColumn<uint>(
name: "xmin",
table: "users",
type: "xid",
rowVersion: true,
nullable: false,
defaultValue: 0u);
The Npgsql documentation doesn't state that I need to do anything else beside adding the column when using Data annotations. It does state that for older versions of the provider, one should use modelBuilder.Entity<User>().UseXminAsConcurrencyToken();
instead, but that is not the case for me.
Should I mark the property as modelBuilder.Entity<User>().Ignore(b => b.Version);
or decorate it with a [NotMapped]
attribute? Would that be the correct approach or am I missing something?
After looking into this a bit deeper, it looks like if the Version
property is decorated with a [NotMapped]
attribute, the whole thing just doesn't work. So I had to leave things as they are in the upper example (aka the xmin
columns had to be explicitly added through the migrations - somehow the migration(s) did not fail even though the xmin
column is added to every table by PostgreSQL by default from what I know).
So, all in all, the correct solution was the initial one:
[Table("users")]
public class User
{
// Other properties
/// <remarks>
/// This property gets mapped to the PostgreSQL xmin system column for the sake of enabling concurrency on the given entity.
/// </remarks>
[Timestamp]
public uint Version { get; set; }
}
// In my DbContext.cs:
protected override void OnModelCreating(ModelBuilder builder)
{
builder.Entity<User>().Property(e => e.Version).IsRowVersion();
}
// In an EF Core migration created after the property was added
migrationBuilder.AddColumn<uint>(
name: "xmin",
table: "users",
type: "xid",
rowVersion: true,
nullable: false,
defaultValue: 0u);
The way I confirmed it works fine was using this code snippet, querying for some random user record, putting a breakpoint before I call SaveChanges()
, manually changing something on that given record and then letting the .NET application continue. It threw a DbUpdateConcurrencyException
which means everything worked fine.
try
{
var user = _dbContext.Users.FirstOrDefault(c => c.Id == 1);
// Here I put a breakpoint and executed `UPDATE users SET name = 'whatever' WHERE id = 1;`
// Afterwards, I let the application continue and it threw a DbUpdateConcurrencyException as expected.
_dbContext.Users.Update(user);
_dbContext.SaveChanges();
}
catch (DbUpdateConcurrencyException ex)
{
// An exception was thrown because the user row version changed since the last time I queried it via EF Core.
throw;
}
P. S It's been confirmed via Github How to properly set up IsRowVersion and should it be included in the migrations that the migrations should indeed contain the code that was auto-generated, so that is proper and expected behavior.