entity-framework-corenpgsqldatabase-concurrency

Npgsql EF Core concurrency token property gets included in migrations


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?


Solution

  • 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.