Concurrent token
The property is configured to implement token concurrent optimistic concurrency control
NOTES
Annotation data using ConcurrencyCheckAttribute
the attribute token is configured to concurrently
public class Person
{
[Key]
public int Id { get; set; }
[ConcurrencyCheck]
[MaxLength(32)]
public string FirstName { get; set; }
[MaxLength(32)]
public string LastName { get; set; }
}
Fluent Api
Using Fluent Api concurrent token configuration properties
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<Person>().Property(s => s.FirstName).IsConcurrencyToken();
}
Timestamp / row version
Generated when adding or updating the database to a new value to an attribute configured timestamp, this attribute is also regarded as a token concurrently. This ensures that you are after (ChangeTracker) line of data query, try to update this line, but the data has been modified by someone else at this time, it will return an exception.
NOTES
Use annotation data TimestampAttribute
to a timestamp attribute flag
public class Person
{
[Key]
public int Id { get; set; }
[ConcurrencyCheck]
[MaxLength(32)]
public string FirstName { get; set; }
[MaxLength(32)]
public string LastName { get; set; }
[Timestamp]
public byte[] Timestamp { get; set; }
}
### Fluent Api
Using Fluent Api flag attribute timestamp
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<Person>()
.Property(s => s.FirstName).IsConcurrencyToken();
builder.Entity<Person>()
.Property(s => s.Timestamp).IsRowVersion();
}
Data migration script
Adding migration
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<byte[]>(
name: "Timestamp",
table: "People",
rowVersion: true,
nullable: true);
}
Take a look at ModelSnapshot
the migration generated
modelBuilder.Entity("LearningEfCore.Person", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<string>("FirstName")
.IsConcurrencyToken()
.HasMaxLength(32);
b.Property<string>("LastName")
.HasMaxLength(32);
b.Property<byte[]>("Timestamp")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate();
b.HasKey("Id");
b.ToTable("People");
});
ValueGeneratedOnAddOrUpdate
It represents the data generated automatically upon insertion and Update
Handle concurrency conflicts
How EF Core detect concurrency conflicts
Optimistic concurrency control configured to achieve concurrent entity attribute token: When the update or delete operation in SaveChanges
the event of the process, EF Core the database will be concurrent with the value of the token ChangeTracker
value is compared tracking.
- If the two values are identical, can be done
- If different, EF Core assumes that the user performs other operations with the current conflict, and will terminate the current transaction.
Other operations in conflict with the current user performed by the user is called concurrency conflicts .
Database providers to achieve more concurrent copy token.
In a relational database, EF Core update and delete operations will by WHERE
containing relatively concurrency token conditional statement, after the statement is executed, the number of rows affected reads EF Core, if no rows affected, will be detected concurrent conflict, EF Core also throwDbUpdateConcurrencyException
We adopted the following example to demonstrate this:
Api to create a controller:
[Route("api/[controller]")]
[ApiController]
public class DefaultController : ControllerBase
{
private readonly TestDbContext _db;
public DefaultController(TestDbContext db)
{
_db = db;
}
[HttpPost]
public async Task<Person> Add([FromBody]Person person)
{
var entry = await _db.People.AddAsync(person);
await _db.SaveChangesAsync();
return entry.Entity;
}
[HttpPut]
public async Task<Person> Update([FromBody]Person current)
{
var original = await _db.People.FindAsync(current.Id);
original.FirstName = current.FirstName;
original.LastName = current.LastName;
await _db.SaveChangesAsync();
return original;
}
}
POST method request http://localhost:5000/api/default
, body json string using:
{
"firstName": "James",
"lastName": "Rajesh"
}
return:
{
"id": 1,
"firstName": "James",
"lastName": "Rajesh",
"timestamp": "AAAAAAAAB9E="
}
We can see timestamp
as we wish, automatically generate a concurrency token
Here we try to SaveChanges
value when modifying the database:
In the Update
interface await _db.SaveChangesAsync();
breakpoint trip.
Modify Request Body as follows:
{
"id": 1,
"firstName": "James1",
"lastName": "Rajesh1",
"timestamp": "AAAAAAAAB9E="
}
A PUT request mode http://localhost:5000/api/default
, the breakpoint is hit,
Modify the database LastName
value Rajesh2, then F10, we get the following concurrency exception:
DbUpdateConcurrencyException: Database operation expected to affect 1 row(s) but actually affected 0 row(s). Data may have been modified or deleted since entities were loaded.
At this point one might wonder, how EF Core is detected LastName
changed it? In actual fact, we are modifying the data in the database when, RawVersion column Timestamp
automatically updated. And every time we use when EF Core updated statement produced like this (you can see through the console log):
Executed DbCommand (68ms) [Parameters=[@p2='?' (DbType = Int32), @p0='?' (Size = 32), @p3='?' (Size = 32), @p1='?' (Size = 32), @p4='?' (Size = 8) (DbType = Binary)], CommandType='Text', CommandTimeout='30'] SET NOCOUNT ON; UPDATE [People] SET [FirstName] = @p0, [LastName] = @p1 WHERE [Id] = @p2 AND [FirstName] = @p3 AND [Timestamp] = @p4;
Here we use WHERE
conditions to determine Timestamp
whether the same
Below remove Timestamp
columns, leaving the flag ConcurrencyToken
ofFirstName
PUT request using the embodiment http://localhost:5000/api/default
, body of:
{
"id": 1,
"firstName": "James6",
"lastName": "Rajesh11"
}
Again at SaveChanges
the time of modifying the database record corresponding to the LastName
value of Rajesh19, this case not being given, the return value:
{
"id": 1,
"firstName": "James6",
"lastName": "Rajesh11"
}
The value of the database can be modified to Rajesh11 explanation here is not detected concurrent, let's try to modify. FirstName
Is James12, at the same time SaveChanges
when amended to Rajesh13, this time it detects concurrency conflicts, we look at the console of the statement:
Executed DbCommand (63ms) [Parameters=[@p1='?' (DbType = Int32), @p0='?' (Size = 32), @p2='?' (Size = 32)], CommandType='Text', CommandTimeout='30'] SET NOCOUNT ON; UPDATE [People] SET [FirstName] = @p0 WHERE [Id] = @p1 AND [FirstName] = @p2;
See here will determine FirstName
and ChangeTracker
compare the value, after the execution of the line is not affected, so will detects concurrency conflicts.
How to handle concurrency conflicts EF Core
First to understand with the help of three sets of values to resolve concurrency conflicts:
- Current values Current value: the value of an application tries to write to the database
- Original values original value: EF Core value retrieved from the database is located in any of the value before the update operation
- Database values database value: The current value is actually stored in the database
When SaveChanges
, if captured DbUpdateConcurrencyException
, indicating the occurrence of a concurrent conflict, using DbUpdateConcurrencyException.Entries
to prepare a new set of values for the affected entities, retrieve database concurrency token value to refresh Original values
and try again until no conflicts.
[HttpPut]
public async Task<Person> Update([FromBody]Person current)
{
Person original = null;
try
{
original = await _db.People.FindAsync(current.Id);
original.FirstName = current.FirstName;
original.LastName = current.LastName;
await _db.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException e)
{
foreach (var entry in e.Entries)
{
var currentValues = entry.CurrentValues;
var databaseValues = await entry.GetDatabaseValuesAsync();
if (entry.Entity is Person person)
{
// 更新什么值取决于实际需要
person.FirstName = currentValues[nameof(Person.FirstName)]?.ToString();
person.LastName = currentValues[nameof(Person.LastName)]?.ToString();
// 这步操作是为了刷新当前 Tracker 的值, 为了通过下一次的并发检查
entry.OriginalValues.SetValues(databaseValues);
}
}
await _db.SaveChangesAsync();
}
return original;
}
This operation can also be added to retry strategy.
At this point, even in SaveChange
time to update the values in the database (or other user changes the value of the same entity), triggering a concurrent conflict, the conflict can be resolved as we want to modify the data.