保存之前设置时间戳仍然使用旧值

我有一个带时间戳(并发令牌)列的模型。 我试图编写一个集成测试,我检查它是否按我的预期工作,但没有成功。 我的测试看起来像下面

  • 使用HttpClient调用从网页api获取应该更新的实体。
  • 直接向Context发出请求并获得相同的实体
  • 更改第2步中实体的属性。
  • 保存第3步中更新的实体。
  • 更改第1步中实体的属性。
  • 用HttpClient向Web Api发送一个带有新实体的放置请求。
  • 在我的Web API中,我首先从数据库中获取实体,然后从我从客户端获得的值中设置属性和时间戳值。 现在我的api控制器中的实体对象具有与数据库中不同的Timestamp值。 现在我预计savechanges会失败,但事实并非如此。 相反,它将实体保存到数据库并生成新的Timestamp值。 我检查了Sql Server Profiler查看生成的查询,结果仍然使用旧的Timestamp值,而不是我分配给我的api控制器中的实体的值。
  • 这是什么原因? 它是否与Timestamp是一个数据库生成的值有关,使EF忽略从业务层对其所做的更改?

    完整的测试应用程序可以在这里找到:https://github.com/Abrissirba/EfTimestampBug

        public class BaseModel
        {
            [Timestamp]
            public byte[] Timestamp { get; set; }
        }
    
        public class Person : BaseModel
        {
            public int Id { get; set; }
    
            public String Title { get; set; }
        }
    
        public class Context : DbContext
        {
            public Context()
            {}
    
            public Context(DbContextOptions options) : base(options)
            {}
    
            public DbSet<Person> Persons{ get; set; }
        }
    
        protected override void BuildModel(ModelBuilder modelBuilder)
        {
            modelBuilder
                .HasAnnotation("ProductVersion", "7.0.0-rc1-16348")
                .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
    
            modelBuilder.Entity("EFTimestampBug.Models.Person", b =>
                {
                    b.Property<int>("Id")
                        .ValueGeneratedOnAdd();
    
                    b.Property<byte[]>("Timestamp")
                        .IsConcurrencyToken()
                        .ValueGeneratedOnAddOrUpdate();
    
                    b.Property<string>("Title");
    
                    b.HasKey("Id");
                });
        }
    
        // PUT api/values/5
        [HttpPut("{id}")]
        public Person Put(int id, [FromBody]Person personDTO)
        {
            // 7
            var person = db.Persons.SingleOrDefault(x => x.Id == id);
            person.Title = personDTO.Title;
            person.Timestamp = personDTO.Timestamp;
            db.SaveChanges();
            return person;
        }
    
        [Fact]
        public async Task Fail_When_Timestamp_Differs()
        {
            using (var client = server.CreateClient().AcceptJson())
            {
                await client.PostAsJsonAsync(ApiEndpoint, Persons[0]);
                // 1
                var getResponse = await client.GetAsync(ApiEndpoint);
                var fetched = await getResponse.Content.ReadAsJsonAsync<List<Person>>();
    
                Assert.True(getResponse.IsSuccessStatusCode);
                Assert.NotEmpty(fetched);
    
                var person = fetched.First();
                // 2
                var fromDb = await db.Persons.SingleOrDefaultAsync(x => x.Id == person.Id);
                // 3
                fromDb.Title = "In between";
                // 4
                await db.SaveChangesAsync();
    
    
                // 5
                person.Title = "After - should fail";
                // 6
                var postResponse = await client.PutAsJsonAsync(ApiEndpoint + person.Id, person);
                var created = await postResponse.Content.ReadAsJsonAsync<Person>();
    
                Assert.False(postResponse.IsSuccessStatusCode);
            }
        }
    
    
        // generated sql - @p1 has the original timestamp from the entity and not the assigned and therefore the save succeed which was not intended
        exec sp_executesql N'SET NOCOUNT OFF;
        UPDATE[Person] SET[Title] = @p2
        OUTPUT INSERTED.[Timestamp]
        WHERE [Id] = @p0 AND[Timestamp] = @p1;
        ',N'@p0 int,@p1 varbinary(8),@p2 nvarchar(4000)',@p0=21,@p1=0x00000000000007F4,@p2=N'After - should fail'
    

    编辑4 - 修复

    我从GitHub回购站点的成员那里听到了问题4512.您必须更新实体的原始值。 这可以像这样完成。

    var passedInTimestamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 120 };  // a hard coded value but normally included in a postback
    var entryProp = db.Entry(person).Property(u => u.Timestamp);
    entryProp.OriginalValue = passedInTimestamp;
    

    我已经更新了原始单元测试失败的地方,您无法获得DbUpdateConcurrencyException ,现在它按预期工作。

    我将更新GitHub票证以询问他们是否可以进行更改,以便当列标记为TimestampIsConcurrencyToken时,生成的基础SQL使用新值而不是原始值,以便它的行为与以前版本的实体框架。

    目前,尽管这似乎是与分离实体一起做的一种方式。


    编辑#3

    谢谢,我错过了。 经过更多的调试后,我完全理解了这个问题,尽管不是为什么会发生。 我们或许应该将Web API从中移除,但移动较少的部分,我认为EF Core和Web API之间没有直接的依赖关系。 我用以下说明问题的测试重现了这个问题。 我很犹豫,把它称为一个错误,因为也许强制EF Core使用传入的timestamp值的惯例自EF6以来已经改变。

    我创建了一套完整的工作最小代码,并在项目的GitHub站点上创建了一个问题/问题。 我将再次将这个测试包括在内以供参考。 只要我听到回复,我会回复这个答案,并让你知道。

    依赖

  • Sql Server 2012
  • EF核心
  • EntityFramework.Commands 7.0.0-rc1-final
  • EntityFramework.MicrosoftSqlServer 7.0.0-rc1-final
  • DDL

    CREATE TABLE [dbo].[Person](
        [Id] [int] IDENTITY NOT NULL,
        [Title] [varchar](50) NOT NULL,
        [Timestamp] [rowversion] NOT NULL,
     CONSTRAINT [PK_Person] PRIMARY KEY CLUSTERED 
    (
        [Id] ASC
    ))
    INSERT INTO Person (title) values('user number 1')
    

    实体

    public class Person
    {
        public int Id { get; set; }
    
        public String Title { get; set; }
    
        // [Timestamp], tried both with & without annotation
        public byte[] Timestamp { get; set; }
    }
    

    Db上下文

    public class Context : DbContext
    {
        public Context(DbContextOptions options)
            : base(options)
        {
        }
    
        public DbSet<Person> Persons { get; set; }
    
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Person>().HasKey(x => x.Id);
    
            modelBuilder.Entity<Person>().Property(x => x.Id)
                .UseSqlServerIdentityColumn()
                .ValueGeneratedOnAdd()
                .ForSqlServerHasColumnName("Id");
    
            modelBuilder.Entity<Person>().Property(x => x.Title)
                .ForSqlServerHasColumnName("Title");
    
            modelBuilder.Entity<Person>().Property(x => x.Timestamp)
                .IsConcurrencyToken(true)
                .ValueGeneratedOnAddOrUpdate()
                .ForSqlServerHasColumnName("Timestamp");
    
            base.OnModelCreating(modelBuilder);
        }
    }
    

    单元测试

    public class UnitTest
    {
        private string dbConnectionString = "DbConnectionStringOrConnectionName";
        public EFTimestampBug.Models.Context CreateContext()
        {
            var options = new DbContextOptionsBuilder();
            options.UseSqlServer(dbConnectionString);
            return new EFTimestampBug.Models.Context(options.Options);
        }
    
        [Fact] // this test passes
        public async Task TimestampChangedExternally()
        {
            using (var db = CreateContext())
            {
                var person = await db.Persons.SingleAsync(x => x.Id == 1);
                person.Title = "Update 2 - should fail";
    
                // update the database manually after we have a person instance
                using (var connection = new System.Data.SqlClient.SqlConnection(dbConnectionString))
                {
                    var command = connection.CreateCommand();
                    command.CommandText = "update person set title = 'changed title' where id = 1";
                    connection.Open();
                    await command.ExecuteNonQueryAsync();
                    command.Dispose();
                }
    
                // should throw exception
                try
                {
                    await db.SaveChangesAsync();
                    throw new Exception("should have thrown exception");
                }
                catch (DbUpdateConcurrencyException)
                {
                }
            }
        }
    
        [Fact]
        public async Task EmulateAspPostbackWhereTimestampHadBeenChanged()
        {
            using (var db = CreateContext())
            {
                var person = await db.Persons.SingleAsync(x => x.Id == 1);
                person.Title = "Update 2 - should fail " + DateTime.Now.Second.ToString();
    
                // This emulates post back where the timestamp is passed in from the web page
                // the Person entity attached dbcontext does have the latest timestamp value but
                // it needs to be changed to what was posted
                // this way the user would see that something has changed between the time that their screen initially loaded and the time they posted the form back
                var passedInTimestamp = new byte[] { 0, 0, 0, 0, 0, 0, 0, 120 };  // a hard coded value but normally included in a postback
                //person.Timestamp = passedInTimestamp;
                var entry = db.Entry(person).Property(u => u.Timestamp);
                entry.OriginalValue = passedInTimestamp;
                try
                {
                    await db.SaveChangesAsync(); // EF ignores the set Timestamp value and uses its own value in the outputed sql
                    throw new Exception("should have thrown DbUpdateConcurrencyException");
                }
                catch (DbUpdateConcurrencyException)
                {
                }
            }
        }
    }
    

    微软已经在处理并发冲突 - EF Core with ASP.NET Core MVC教程中更新了他们的教程。 它特别声明关于更新的以下内容:

    在调用SaveChanges之前,必须将该原始RowVersion属性值放入实体的OriginalValues集合中。

    _context.Entry(entityToUpdate).Property("RowVersion").OriginalValue = rowVersion;
    

    然后,当Entity Framework创建SQL UPDATE命令时,该命令将包含一个WHERE子句,该子句将查找具有原始RowVersion值的行。 如果没有行受到UPDATE命令的影响(没有行具有原始RowVersion值),Entity Framework将抛出DbUpdateConcurrencyException异常。

    链接地址: http://www.djcxy.com/p/90281.html

    上一篇: Set Timestamp before save still uses the old value

    下一篇: How to parse JSON format date string into date format