Abr*_*ris 8 entity-framework entity-framework-core
我有一个带有时间戳(并发令牌)列的模型.我正在尝试编写集成测试,我会检查它是否按预期工作但没有成功.我的测试看起来如下
这是什么原因?它是否与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'
Run Code Online (Sandbox Code Playgroud)
编辑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;
Run Code Online (Sandbox Code Playgroud)
我已经更新了原来的单元测试失败,你和我无法DbUpdateConcurrencyException被抛出,它现在按预期工作.
我将更新GitHub票证,询问他们是否可以进行更改,以便当列标记为Timestamp或者IsConcurrencyToken其行为类似于先前版本的Entity Framework 时,生成的基础sql使用新值而不是原始值.
目前虽然这似乎是分离实体的方法.
编辑#3
谢谢,我错过了.经过更多的调试后,我完全理解了这个问题,尽管不是为什么会发生.我们应该从中获取Web API,减少移动部件,我不认为EF Core和Web API之间存在直接依赖关系.我通过以下测试重现了这个问题,这些测试说明了这个问题.我犹豫是否称它为一个错误,因为强制EF Core使用传入的timestamp值的约定自EF6以来已经改变.
我创建了一套完整的工作最小代码,并在项目的GitHub站点上创建了一个问题/问题.我将在下面再次提供测试以供参考.我一听到后就会回复这个答案并告诉你.
依赖
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')
Run Code Online (Sandbox Code Playgroud)
实体
public class Person
{
public int Id { get; set; }
public String Title { get; set; }
// [Timestamp], tried both with & without annotation
public byte[] Timestamp { get; set; }
}
Run Code Online (Sandbox Code Playgroud)
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);
}
}
Run Code Online (Sandbox Code Playgroud)
单元测试
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)
{
}
}
}
}
Run Code Online (Sandbox Code Playgroud)
Microsoft 已在处理并发冲突 - EF Core 与 ASP.NET Core MVC 教程中更新了他们的教程。它具体说明了以下有关更新的内容:
在调用 之前
SaveChanges,您必须将该原始RowVersion属性值放入OriginalValues实体的集合中。
_context.Entry(entityToUpdate).Property("RowVersion").OriginalValue = rowVersion;
Run Code Online (Sandbox Code Playgroud)
然后,当实体框架创建 SQL UPDATE 命令时,该命令将包含一个 WHERE 子句,用于查找具有原始
RowVersion值的行。如果没有行受到 UPDATE 命令的影响(没有行具有原始RowVersion值),实体框架将引发DbUpdateConcurrencyException异常。
| 归档时间: |
|
| 查看次数: |
6540 次 |
| 最近记录: |