繁体   English   中英

实体框架布尔并发令牌意外行为

[英]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之外不会有任何问题和/或后果,并且保证一切都是健壮的。

无论如何,对我来说,有些事情需要进一步调查:

  • TransactionConcurrencyToken对象相互交互时,这真的是微软(.NET 团队)预期的行为吗?
  • 如果上述问题的答案是“是”,我认为该论点确实需要解释这些模式的深入文档。
  • 如果答案是“否”/“不确定”,我们可能正在处理值得调查的奇怪/意外行为。 所以我正在考虑在 .NET 5 GitHub 页面上打开一个问题。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM