我有一个带有时间戳(并发令牌)列的模型。我正在尝试编写集成测试,我会检查它是否按预期工作但没有成功。我的测试看起来像是
这是什么原因?它是否与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'
答案 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。我将在下面再次提供测试以供参考。我收到回复后会立即回复此答案并告知您。
依赖
<强> 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
例外。