EF 코어 - 저장하기 전에 타임 스탬프를 설정하면 여전히 이전 값이 사용됩니다.

entity-framework entity-framework-core

문제

타임 스탬프 (동시성 토큰) 열이있는 모델이 있습니다. 내가 기대했던대로 작동하지만 성공하지 않았는지 확인하는 통합 테스트를 작성하려고합니다. 내 검사는 다음과 같이 보입니다.

  1. HttpClient 호출을 사용하여 웹 API에서 업데이트해야하는 엔티티를 가져옵니다.
  2. 컨텍스트에 직접 요청하고 동일한 엔티티를 가져옵니다.
  3. 엔터티의 속성을 2 단계에서 변경하십시오.
  4. 3 단계에서 업데이트 된 엔티티를 저장하십시오.
  5. 엔터티의 속성을 1 단계에서 변경하십시오.
  6. HttpClient를 사용하여 웹 애플리케이션에 새로운 엔터티와 함께 ​​put 요청을 보냅니다.
  7. 내 웹 API에서는 먼저 데이터베이스에서 엔티티를 가져오고 클라이언트에서 가져온 속성 및 타임 스탬프 값을 설정합니다. 이제 API 컨트롤러의 엔티티 개체에 데이터베이스의 타임 스탬프 값과 다른 타임 스탬프 값이 있습니다. 이제 savechanges가 실패 할 것이라고 기대하지만 그렇지 않습니다. 대신 엔티티를 데이터베이스에 저장하고 새 타임 스탬프 값을 생성합니다. 생성 된 쿼리를 보려면 Sql Server Profiler를 확인했는데이 API는 이전 Timestamp 값을 사용하고 있으며 내 API 컨트롤러에서 엔티티에 할당 한 값이 아닙니다.

이것에 대한 이유는 무엇입니까? EF가 비즈니스 계층에서 변경 한 내용을 무시하게 만드는 데이터베이스 생성 값인 Timestamp와 아무런 관련이 있습니까?

전체 테스트 응용 프로그램은 다음에서 찾을 수 있습니다. 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 사이트에있는 회원으로부터 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 표시 될 때 생성 된 기본 sql이 원래 값 대신 새 값을 사용하도록 변경하여 이전 버전과 유사한 동작을하도록 할 것인지 IsConcurrencyToken 엔티티 프레임 워크.

당분간 이것은 분리 된 실재물로 그것을하는 방법에가는 것처럼 보입니다.


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 with ASP.NET Core MVC 자습서를 업데이트했습니다 . 특히 업데이트에 대해 다음과 같이 명시합니다.

SaveChanges 를 호출하기 전에 원래 RowVersion 속성 값을 엔터티의 OriginalValues 컬렉션에 넣어야합니다.

_context.Entry(entityToUpdate).Property("RowVersion").OriginalValue = rowVersion;

그런 다음 Entity Framework에서 SQL UPDATE 명령을 만들면이 명령에는 원래 RowVersion 값을 가진 행을 찾는 WHERE 절이 포함됩니다. UPDATE 명령의 영향을받는 행이 없으면 (원래 RowVersion 값을 가진 행이없는 경우) Entity Framework는 DbUpdateConcurrencyException 예외를 발생시킵니다.




아래 라이선스: CC-BY-SA with attribution
와 제휴하지 않음 Stack Overflow
이 KB는 합법적입니까? 예, 이유를 알아보십시오.
아래 라이선스: CC-BY-SA with attribution
와 제휴하지 않음 Stack Overflow
이 KB는 합법적입니까? 예, 이유를 알아보십시오.