[英]Entity Framework boolean Concurrency Token unexpected behaviour
我正在尝试实现一个 EF 事务,它在内部必须管理一个实体的ConcurrencyToken
,它是一个bool
属性。
这个想法是每个与Registry
实体关联的IdChain
只能使用一次。 因此,只有从第一个并发调用开始, MovementsChain
实体的InUse
bool
属性才必须设置为true
。 在尝试将InUse
属性设置为true
然后调用Context.SaveChangesAsync()
,所有其他方法都应该失败,所以我希望抛出DbUpdateConcurrencyException
。
public partial class MovementsChain
{
public Guid IdChain { get; set; }
public int IdRegistry { get; set; }
public DateTime Creation { get; set; }
public bool InUse { get; set; }
}
public void Configure(EntityTypeBuilder<MovementsChain> entity)
{
entity.HasKey(e => e.IdChain);
entity.Property(e => e.IdChain).HasDefaultValueSql("(newid())");
entity.Property(e => e.Creation).HasDefaultValueSql("(getutcdate())");
entity.Property(e => e.InUse)
.IsConcurrencyToken();
}
// I tried to set different isolation levels for the transaction, like: ReadCommitted and ReadUncommitted, but with no changes in the final behaviour
using (var transaction = await Context.Database.BeginTransactionAsync())
{
try
{
// Get and lock the IdChain
var register = await Context.Registers.FirstOrDefaultAsync(/* Predicate... */);
if (register == null)
return new TransactionResult(Error.InvalidRegisterIdentifier);
var movementChain = await Context.MovementsChain
.OrderByDescending(e => e.Creation)
.FirstOrDefaultAsync(e => e.IdRegister == register.IdRegister);
if (movementChain == null)
return new TransactionResult(Error.IdChainNotFound);
if (movementChain.InUse)
return new TransactionResult(Error.IdChainAlreadyInUse);
// The "core" part of the transaction where the concurrency is managed...
try
{
movementChain.InUse = true;
await Context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException ex)
{
// During the tests I never get there...
return new TransactionResult(Error.IdChainAlreadyInUse); // The specified IdChain is being used by another operation. Cannot proceed.
}
// Validation for the client given Guid
if (request.IdChain == Guid.Empty)
return new TransactionResult(Error.IdChainNotSpecified);
if (movementChain.IdChain != request.IdChain)
return new TransactionResult(Error.InvalidIdChain); // The specified IdChain is not the last one generated by the system. Cannot proceed.
/* The transaction proceeds doing some other stuff...
* ...
*/
/* Now that the transaction is almost completed and everything has gone well so far,
* I generate the new Idchain, valid for the next operation
* and set the current IdChain back to InUse = false.
*/
// Create the new IdChain
EntityFramework.Models.MovementsChain newMovementChain = new EntityFramework.Models.MovementsChain
{
IdRegister = register.IdRegister,
};
await Context.MovementsChain.AddAsync(newMovementChain);
// Unlock the current used IdChain
movementChain.InUse = false;
// Save and commit
await Context.SaveChangesAsync();
await transaction.CommitAsync();
return new TransactionResult(Ok);
}
catch (Exception ex)
{
await transaction.RollbackAsync();
throw;
}
}
[TestMethod]
public async Task CreateMovements_WithSameIdChainInParallel_ReturnsOkJustOne()
{
int maxConcurrentRequests = 20;
string idChain = GetLastIdChain(50).ToString();
int counterStart = 343;
string payload = @"[
{{
""counter"": {0},
""somOtherData"": ""data""
}}
]";
var contents = new List<StringContent>();
for (int i = 0; i < maxConcurrentRequests; i++)
{
var content = new StringContent(string.Format(payload, counterStart), Encoding.UTF8, "application/json");
content.Headers.Add("X-MyApp-IdChain", idChain);
contents.Add(content);
counterStart++;
}
var responses = new ConcurrentBag<HttpResponseMessage>();
// Multiple Clients using Tasks
var tasks = contents.Select(async content =>
{
using (var client = await CreateClientAsync(content))
{
var response = await client.PostAsync(api, content);
responses.Add(response);
string responseString = await response.Content.ReadAsStringAsync();
}
}).ToList();
await Task.WhenAll(tasks);
Assert.IsTrue(responses.Count(r => r.IsSuccessStatusCode) == 1);
}
这个测试总是失败,因为我通常从 20 个请求中得到 6-7 个请求,这些请求“正确地”完成了 HTTP 200 OK 状态代码。 所以同一个IdChain被多次使用。
相反,正确的行为应该是只有一个(第一个能够锁定 IdChain 的)以 200 OK 完成,而所有其他的都必须失败。
最后,问题是负责管理ConcurrencyToken
的代码在Transaction
using
块内。 将那部分代码移到Transaction
之外,会导致正确(和预期)的行为。
基本上,当try/catch
块位于Transaction
块内时,似乎不会触发DbUpdateConcurrencyException
。
单元测试方法现在可以成功执行 20、50 甚至 100 个并行调用。
这是Transaction
的更新代码。
// Get and lock the IdChain
var register = await Context.Registers.FirstOrDefaultAsync(/* Predicate... */);
if (register == null)
return new TransactionResult(Error.InvalidRegisterIdentifier);
var movementChain = await Context.MovementsChain
.OrderByDescending(e => e.Creation)
.FirstOrDefaultAsync(e => e.IdRegister == register.IdRegister);
if (movementChain == null)
return new TransactionResult(Error.IdChainNotFound);
if (movementChain.InUse)
return new TransactionResult(Error.IdChainAlreadyInUse);
// Validation for the client given Guid
if (request.IdChain == Guid.Empty)
return new TransactionResult(Error.IdChainNotSpecified);
if (movementChain.IdChain != request.IdChain)
return new TransactionResult(Error.InvalidIdChain); // The specified IdChain is not the last one generated by the system. Cannot proceed.
// The "core" part of the transaction where the concurrency is managed...
try
{
movementChain.InUse = true;
await Context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException ex)
{
// During the tests I never get there...
return new TransactionResult(Error.IdChainAlreadyInUse); // The specified IdChain is being used by another operation. Cannot proceed.
}
using (var transaction = await Context.Database.BeginTransactionAsync())
{
try
{
// The transaction starts here, doing its stuff...
/* Now that the transaction is almost completed and everything has gone well so far,
* I generate the new Idchain, valid for the next operation
* and set the current IdChain back to InUse = false.
*/
// Create the new IdChain
EntityFramework.Models.MovementsChain newMovementChain = new EntityFramework.Models.MovementsChain
{
IdRegister = register.IdRegister,
};
await Context.MovementsChain.AddAsync(newMovementChain);
// Unlock the current used IdChain
movementChain.InUse = false;
// Save and commit
await Context.SaveChangesAsync();
await transaction.CommitAsync();
return new TransactionResult(Ok);
}
catch (Exception ex)
{
await transaction.RollbackAsync();
throw;
}
}
在这种特殊情况下,将这部分代码移到Transaction
之外不会有任何问题和/或后果,并且保证一切都是健壮的。
无论如何,对我来说,有些事情需要进一步调查:
Transaction
和ConcurrencyToken
对象相互交互时,这真的是微软(.NET 团队)预期的行为吗?
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.