C# 当行可能不存在时,如何增加原子增量?

C# 当行可能不存在时,如何增加原子增量?,c#,.net,entity-framework,entity-framework-6,C#,.net,Entity Framework,Entity Framework 6,假设我有一个DbSet Items的MyContext派生自DbContext,其中Item定义如下 public class Item { [Key] public string Key { get; set; } [ConcurrencyCheck] public int Value { get; set; } } 给定一个键,我希望以原子方式递增相应的值,其中表中尚未包含的键的隐式值为0。也就是说,对表中不存在的键进行原子递增将得到值1。以下是我目前的做

假设我有一个
DbSet Items
MyContext
派生自
DbContext
,其中
Item
定义如下

public class Item
{
    [Key]
    public string Key { get; set; }

    [ConcurrencyCheck]
    public int Value { get; set; }
}
给定一个键,我希望以原子方式递增相应的值,其中表中尚未包含的键的隐式值为0。也就是说,对表中不存在的键进行原子递增将得到值1。以下是我目前的做法:

public static async Task IncrementAsync(string key)
{
    using (var context = new MyContext())
    {
        while (true)
        {
            var item = await context.Items.FindAsync(key);
            if (item == null)
            {
                context.Items.Add(new Item { Key = key, Value = 1 });
            }
            else
            {
                item.Value++;
            }
            try
            {
                await context.SaveChangesAsync();
                break;
            }
            catch (DbUpdateException)
            {
                continue;
            }
        }
    }
}
当对
IncrementAsync
的多个调用同时运行时,如果处于活动锁定状态,则此操作将失败

  • 正确的方法是什么
  • while
    循环是否应该在使用
    之外,以便每次尝试都获得一个新的上下文?我试过这个,它让一切都正常,但我觉得我在创建和破坏这么多上下文方面效率低下
  • 我是否遗漏了上下文跟踪变化的方式
我的实体框架经验基本上只是查询,所以如果您能解释我在这段代码中做错了什么,我将非常感激

编辑
因为选择的答案不明确,所以我将把更正后的代码放在这里。请注意,在发生
DbUpdateException
之后,
上下文如何从不被重用

public static async Task IncrementAsync(string key)
{
    while (true)
    {
        using (var context = new MyContext())
        {
            var item = await context.Items.FindAsync(key);
            if (item == null)
            {
                context.Items.Add(new Item { Key = key, Value = 1 });
            }
            else
            {
                item.Value++;
            }
            try
            {
                await context.SaveChangesAsync();
                break;
            }
            catch (DbUpdateException)
            {
                continue;
            }
        }
    }
}

您需要在尝试之间不共享上下文。如果可能,请不要对具有
DbUpdateException
的上下文执行任何操作,因为如果不显式清理上下文,它可能永远不会返回正常状态

我希望外部环境会引起问题。如果对单个键的并发调用取决于时间,则可能会创建错误的上下文设置(由于错误处理程序,该设置将被不断忽略)

除非我弄错了,否则数据库中存在
这一事实不会删除“待添加”版本。您将得到以下上下文之一:

Add "1", 2

这取决于您的第二次迭代是抓住第一次迭代的对象还是一个新对象


这两种方法都不可能成功,因此最终会出现一个连续的错误。

给出答案的另一种方法是使用存储过程来完成所有工作,如以下示例所示。然后,您可以在应用程序中用一行代码而不是上面的代码来调用它

CREATE PROCEDURE SP_IncrementValue
    @ValueKey NVARCHAR(100)
AS
BEGIN
    BEGIN TRAN
       UPDATE Item WITH (SERIALIZABLE) SET Value = Value + 1
       WHERE [Key] = @ValueKey

       IF @@ROWCOUNT = 0
       BEGIN
          INSERT Item ([Key], Value) VALUES (@ValueKey, 1)
       END
    COMMIT TRAN
END
GO
这种方法提供了更好的性能和更少的错误倾向

编辑: 要从C#调用存储过程,请在EF ApplicationDbContext类中添加以下方法

    public int IncrementValue(string valueKey)
    {
        return this.Database.ExecuteSqlCommand("EXEC SP_IncrementValue @ValueKey", new SqlParameter("@ValueKey", valueKey));
    }

然后您可以从DBContext类的任何实例调用它

我会尝试一个较短的上下文,看看这是否有帮助。我必须四处玩,看看当并发更新发生时会发生什么情况,可能是某些不兼容的更新一起排队(最简单的示例是一个值不是1的add).您是否考虑过使用存储过程将同步卸载到SQL机器?@Guvante如果可能的话,我想留在纯EF的世界中(我可以在
之间切换
)除非像存储过程这样的操作真的是一个巨大的优化。这个特定的操作不会被大量调用。让它以适度的性能损失正常工作是可以接受的。如果您使用相同的
键多次调用
IncrementAsync
,则会有一个p问题,因为
FindAsync
在转到数据库之前会在上下文中查找现有实体。您能否将如何从C#调用存储过程添加到这个答案中?
    public int IncrementValue(string valueKey)
    {
        return this.Database.ExecuteSqlCommand("EXEC SP_IncrementValue @ValueKey", new SqlParameter("@ValueKey", valueKey));
    }