EF Core - set timestamp before saving, still using old value

I have a time stamped model (Concurrency Token). I am trying to write an integration test, where I verify that it works as I expect, but without success. My test is as follows

  • To get an object with this should be updated from the web api using an HttpClient call.
  • Make a request directly to the Context and get the same object
  • Change the property to entities from step 2.
  • Save the object updated in step 3.
  • Change the property to entities from step 1.
  • Submit a hosting request with a new HttpClient object in Web Api.
  • In my web API, I first get the object from the database, sets the property value and timestamp from the one I received from the client. Now my object object in api controller has a different timestamp value than the value specified in the database. Now I expect savechanges to fail, but it is not. Instead, it stores the object in the database and generates a new timestamp value. I checked with the Sql Server Profiler to see the generated query, and it turns out that the old Timestamp value is still used, and not the one that I assigned the entities in my api controller.

What is the reason for this? Does this have anything to do with Timestamp resulting from creating a database that forces EF to ignore changes made to it from the business layer?

The full test application can be found here: 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' 
+7
entity-framework entity-framework-core
source share
2 answers

Change 4 - Fix

I heard an answer from a member of the GitHub repository site, question 4512 . You must update the original value for the object. It can be done like this.

 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; 

I updated the original unit test, which DbUpdateConcurrencyException when you and I could not get a DbUpdateConcurrencyException throw, now it works as expected.

I am updating my GitHub ticket to ask if they can make a change so that the generated sql code uses the new value instead of the original value when the column is marked as Timestamp or IsConcurrencyToken so that it behaves similar to previous versions of the Entity Framework.

So far, it seems like a way to do this with individual objects.


Edit # 3

Thanks, I missed it. After additional debugging, once again I fully understand the problem, although not why this happens. We should probably get the Web API out of it, although less mobile, and I don't think there is a direct relationship between the EF Core and the Web API. I reproduced this question with the following tests that illustrate the problem. I hesitate to call this a mistake, because perhaps the agreement to force EF Core to use the value passed to Timestamp has changed from EF6.

I created a complete set of working minimum codes and created a problem / question on the GitHub project site. I will include the test again below for reference. As soon as I hear the answer, I will send an answer to this answer and let you know.

Dependencies

  • Sql Server 2012
  • Ef core
    • 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') 

Entity

 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 context

 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); } } 

Unit test

 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) { } } } } 
+4
source share

Microsoft has updated its tutorial for this in Concurrency - EF Core Conflict Handling with the ASP.NET Core MVC tutorial . It specifically refers to the following updates:

Before calling SaveChanges , you must place this original RowVersion property value in the OriginalValues collection for the object.

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

Then, when the Entity Framework creates the SQL UPDATE command, this command will include a WHERE clause that looks for a row that has the original RowVersion value. If no rows are affected by the UPDATE command (no rows have the original RowVersion value), the Entity Framework throws a DbUpdateConcurrencyException .

0
source share

All Articles