EF Core - 保存前设置时间戳仍使用旧值

时间:2016-02-06 15:01:05

标签: entity-framework entity-framework-core

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

  1. 获取具有该实体的实体应该通过HttpClient调用从web api更新。
  2. 直接向Context发出请求并获取相同的实体
  3. 从第2步更改实体的属性。
  4. 保存在步骤3中更新的实体。
  5. 从第1步更改实体的属性。
  6. 使用带有HttpClient的新实体向Web Api发送放置请求。
  7. 在我的Web API中,我首先从数据库中获取实体,从我从客户端获取的值设置属性和时间戳值。现在,我在api控制器中的实体对象具有与数据库中的实体对象不同的Timestamp值。现在我预计保存更改会失败,但事实并非如此。而是将实体保存到数据库并生成新的Timestamp值。我检查了Sql Server Profiler以查看生成的查询,结果是仍然使用旧的Timestamp值而不是我在api控制器中分配给实体的值。
  8. 这是什么原因?它是否与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'
    

2 个答案:

答案 0 :(得分:5)

修改4 - 修复

我从GitHub回购网站issue 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票证,询问他们是否可以进行更改,以便当列标记为Timestamp或{{1}时,生成的基础sql使用新值而不是原始值这样它的行为类似于先前版本的Entity Framework。

目前虽然这似乎是分离实体的方法。

编辑#3

谢谢,我错过了。经过更多的调试后,我完全理解了这个问题,尽管不是为什么会这样。我们应该从中获取Web API,减少移动部件,我不认为EF Core和Web API之间存在直接依赖关系。我通过以下测试重现了这个问题,这些测试说明了这个问题。 我犹豫是否将其称为错误,因为强制EF Core使用传入的IsConcurrencyToken值的惯例自EF6以来已发生变化。

我在项目的GitHub网站上创建了一套完整的工作最小代码和created an issue/question。我将在下面再次提供测试以供参考。我收到回复后会立即回复此答案并告知您。

依赖

  • Sql Server 2012
  • EF Core
    • EntityFramework.Commands 7.0.0-rc1-final
    • EntityFramework.MicrosoftSqlServer 7.0.0-rc1-final

<强> DDL

timestamp

<强>实体

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')

Db背景

public class Person
{
    public int Id { get; set; }

    public String Title { get; set; }

    // [Timestamp], tried both with & without annotation
    public byte[] Timestamp { get; set; }
}

单元测试

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);
    }
}

答案 1 :(得分:1)

Microsoft已在Handling concurrency conflicts - EF Core with ASP.NET Core MVC tutorial更新了他们的教程。它具体说明了有关更新的内容:

  

在致电SaveChanges之前,您必须放置原件   RowVersion集合中的OriginalValues属性值   实体。

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

然后当Entity Framework创建一个SQL UPDATE命令时   命令将包含一个WHERE子句,用于查找具有该行的行   原始RowVersion值。如果UPDATE不影响任何行   命令(没有行具有原始RowVersion值),实体   框架抛出DbUpdateConcurrencyException例外。