gpt4 book ai didi

entity-framework - EF Core-在保存之前设置时间戳仍然使用旧值

转载 作者:行者123 更新时间:2023-12-04 07:19:35 28 4
gpt4 key购买 nike

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

  • 获取应通过HttpClient调用从Web api更新的实体。
  • 直接向上下文发出请求,并获得相同的实体
  • 从步骤2开始更改实体的属性。
  • 保存在步骤3中更新的实体。
  • 从步骤1更改实体的属性。
  • 将带有HttpClient的新实体的放置请求发送到Web Api。
  • 在我的Web API中,我首先从数据库中获取实体,并从我从客户端获得的实体和时间戳值中进行设置。现在,我在api Controller 中的实体对象具有与数据库中不同的Timestamp值。现在,我希望savechanges会失败,但是不会。而是将实体保存到数据库并生成新的时间戳值。我使用Sql Server Profiler进行了检查,以查看生成的查询,结果发现该查询仍在使用旧的Timestamp值,而不是我在api Controller 中分配给实体的值。

  • 这是什么原因呢?与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 repo 网站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票证,询问他们是否可以进行更改,以便在将该列标记为 TimestampIsConcurrencyToken时,生成的基础sql使用新值而不是原始值,以使其行为类似于的先前版本。 Entity Framework 。

    就目前而言,这似乎是使用分离的实体执行此操作的方法。

    编辑#3

    谢谢,我错过了。经过更多的调试后,我完全理解了这个问题,尽管不是为什么会发生。不过,我们可能应该从中删除Web API,减少事件部分,并且我认为EF Core和Web API之间没有直接的依赖关系。我用以下说明问题的测试重现了该问题。我不愿意将其称为错误,因为自EF6以来,强制EF Core使用传入的 timestamp值的约定已发生更改。

    我已经在该项目的GitHub网站上创建了一套完整的工作最小代码和 created an issue/question。我将在下面再次包含该测试以供引用。收到回复后,我会立即回覆此答案,并告诉您。

    依存关系
  • 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)
    {
    }
    }
    }
    }

    关于entity-framework - EF Core-在保存之前设置时间戳仍然使用旧值,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/35242461/

    28 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com