私は、タイムスタンプ(同時実行トークン)列を持つモデルを持っています。私は期待通りに動作することを確認する統合テストを書こうとしていますが、成功しません。私のテストは以下のように見えます
この理由は何ですか?タイムスタンプとは、ビジネス層からの変更を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チケットを更新して、列がTimestamp
またはIsConcurrencyToken
としてマークされたときに元の値の代わりに新しい値が使用されるように変更できるかどうかを尋ねて、以前のバージョンと同様に動作するようにします。エンティティフレームワーク。
今のところ、これは分離されたエンティティでそれをやり遂げる方法のようです。
編集#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')
エンティティ
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)
{
}
}
}
}
マイクロソフトでは、このためにチュートリアルを更新して、 同時処理の競合の処理(EFコアとASP.NETコアMVCチュートリアル)を更新しました。更新については、特に次のように述べています。
SaveChanges
を呼び出す前に、元のRowVersion
プロパティ値をエンティティのOriginalValues
コレクションに配置する必要があります。
_context.Entry(entityToUpdate).Property("RowVersion").OriginalValue = rowVersion;
エンティティフレームワークがSQL UPDATEコマンドを作成すると、そのコマンドには元の
RowVersion
値を持つ行を探すWHERE句が含まれます。 UPDATEコマンドの影響を受ける行がない場合(行に元のRowVersion
値がない場合)、Entity FrameworkはDbUpdateConcurrencyException
例外をスローします。