Updating entity in EF Core application with SQLite gives DbUpdateConcurrencyException

后端 未结 2 1014
深忆病人
深忆病人 2021-02-09 09:00

I try to use optimistic concurrency check in EF Core with SQLite. The simplest positive scenario (even without concurrency itself) gives me Microsoft.EntityFrameworkCore.

相关标签:
2条回答
  • 2021-02-09 09:18

    Looks like EF Core SQLite provider does not handle properly [TimeStamp] (or IsRowVersion()) marked byte[] properties when binding them to SQL query parameters. It uses the default byte[] to hex string conversion which is not applicable in this case - the byte[] actually is a string.

    First consider reporting it to their issue tracker. Then, until it gets resolved (if ever), as a workaround you can use the following custom ValueConverter:

    class SqliteTimestampConverter : ValueConverter<byte[], string>
    {
        public SqliteTimestampConverter() : base(
            v => v == null ? null : ToDb(v),
            v => v == null ? null : FromDb(v))
        { }
        static byte[] FromDb(string v) =>
            v.Select(c => (byte)c).ToArray(); // Encoding.ASCII.GetString(v)
        static string ToDb(byte[] v) =>
            new string(v.Select(b => (char)b).ToArray()); // Encoding.ASCII.GetBytes(v))
    }
    

    Unfortunately there is no way to tell EF Core to use it only for parameters, so after assigning it with .HasConversion(new SqliteTimestampConverter()), now the db type is considered string, so you need to add .HasColumnType("BLOB").

    The final working mapping is

        modelBuilder.Entity<Blog>()
            .Property(p => p.Timestamp)
            .IsRowVersion()
            .HasConversion(new SqliteTimestampConverter())
            .HasColumnType("BLOB")
            .HasDefaultValueSql("CURRENT_TIMESTAMP");
    

    You can avoid all that by adding the following custom SQLite RowVersion "convention" at the end of your OnModelCreating:

    if (Database.IsSqlite())
    {
        var timestampProperties = modelBuilder.Model
            .GetEntityTypes()
            .SelectMany(t => t.GetProperties())
            .Where(p => p.ClrType == typeof(byte[])
                && p.ValueGenerated == ValueGenerated.OnAddOrUpdate
                && p.IsConcurrencyToken);
    
        foreach (var property in timestampProperties)
        {
            property.SetValueConverter(new SqliteTimestampConverter());
            property.Relational().DefaultValueSql = "CURRENT_TIMESTAMP";
        }
    }
    

    so your property configuration could be trimmed down to

    modelBuilder.Entity<Blog>()
        .Property(p => p.Timestamp)
        .IsRowVersion();
    

    or totally removed and replaced with data annotation

    public class Blog
    {
        public Guid Id { get; set; }
        public string Name { get; set; }
        [Timestamp]
        public byte[] Timestamp { get; set; }
    }
    
    0 讨论(0)
  • 2021-02-09 09:25

    Inspired by this thread on GitHub and the Ivan's answer I wrote this code to ensure on my unit testing to mimic the SQL Server concurrency.

    var connection = new SqliteConnection("DataSource=:memory:");
    
    var options = new DbContextOptionsBuilder<ActiveContext>()
                   .UseSqlite(connection)
                   .Options;
    
    var ctx = new ActiveContext(options);
    
    if (connection.State != System.Data.ConnectionState.Open)
    {
        connection.Open();
    
        ctx.Database.EnsureCreated();
    
        var tables = ctx.Model.GetEntityTypes();
    
        foreach (var table in tables)
        {
            var props = table.GetProperties()
                            .Where(p => p.ClrType == typeof(byte[])
                            && p.ValueGenerated == Microsoft.EntityFrameworkCore.Metadata.ValueGenerated.OnAddOrUpdate
                            && p.IsConcurrencyToken);
    
            var tableName = table.Relational().TableName;
    
            foreach (var field in props)
            {
                string[] SQLs = new string[] {
                    $@"CREATE TRIGGER Set{tableName}_{field.Name}OnUpdate
                    AFTER UPDATE ON {tableName}
                    BEGIN
                        UPDATE {tableName}
                        SET RowVersion = randomblob(8)
                        WHERE rowid = NEW.rowid;
                    END
                    ",
                    $@"CREATE TRIGGER Set{tableName}_{field.Name}OnInsert
                    AFTER INSERT ON {tableName}
                    BEGIN
                        UPDATE {tableName}
                        SET RowVersion = randomblob(8)
                        WHERE rowid = NEW.rowid;
                    END
                    "
                };
    
                foreach (var sql in SQLs)
                {
                    using (var command = connection.CreateCommand())
                    {
                        command.CommandText = sql;
                        command.ExecuteNonQuery();
                    }
                }
            }
        }
    }
    
    0 讨论(0)
提交回复
热议问题