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

entity-framework entity-framework-core

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

  1. 获取具有该实体的实体应该通过HttpClient调用从web api更新。
  2. 直接向Context发出请求并获得相同的实体
  3. 从步骤2更改实体的属性。
  4. 保存在步骤3中更新的实体。
  5. 从步骤1更改实体的属性。
  6. 使用带有HttpClient的新实体向Web Api发送put请求。
  7. 在我的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)
            {
            }
        }
    }
}

热门答案

Microsoft已经在处理并发冲突 - EF Core与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异常。



Related

许可下: CC-BY-SA with attribution
不隶属于 Stack Overflow
许可下: CC-BY-SA with attribution
不隶属于 Stack Overflow