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

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;

實體

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;

Db上下文

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;

單元測試

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;

熱門答案

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異常。




許可下: CC-BY-SA with attribution
不隸屬於 Stack Overflow
這個KB合法嗎? 是的,了解原因
許可下: CC-BY-SA with attribution
不隸屬於 Stack Overflow
這個KB合法嗎? 是的,了解原因