EF Core AddRange 和具有重复键的实体

时间:2021-04-15 14:06:22

标签: entity-framework-core

我有一个用例,其中第三方提供了我希望使用 EF Core 合并到数据库中的项目的枚举。在某些用例中,第三方在枚举中多次提供具有相同键的项

<头>
ID 帐号 上次付款
12345 ABC123 1/1/2021
23456 BCD234 2/1/2021
12345 ABC123 2/1/2021

理想情况下,我希望对 12345 进行两次更新(我们在数据层审核历史记录)。

尝试将 12345 添加到同一上下文两次时出现错误。代码 POC 是:

[Fact]
public async Task HandlesDuplicateKeys()
{
    var services = new ServiceCollection()
        .AddDbContext<ItemContext>(options => options.UseInMemoryDatabase(Guid.NewGuid().ToString()))
        .BuildServiceProvider();

    var items = new List<ItemA>()
    {
        new ItemA() { Id = 1, A = "Foo" },
        new ItemA() { Id = 1, A = "Bar" }
    };

    using (var context = services.GetRequiredService<ItemContext>())
    {
        context.AList.AddRange(items);
        await context.SaveChangesAsync();
    }
}

public class ItemA
{
    public int Id { get; set; }
    public string? A { get; set; }
}

public class ItemContext : DbContext
{
    public RepositoryContext(DbContextOptions<RepositoryContext> options) : base(options)
    { }

    public DbSet<ItemA> AList { get; set; }
}

产量:

<块引用>

留言: System.InvalidOperationException :无法跟踪实体类型“ItemA”的实例,因为已跟踪具有相同 {'Id'} 键值的另一个实例。附加现有实体时,请确保仅附加一个具有给定键值的实体实例。考虑使用“DbContextOptionsBuilder.EnableSensitiveDataLogging”来查看冲突的键值。

管理此用例的适当方法是什么?

1 个答案:

答案 0 :(得分:0)

EF 团队对 provide a solution 非常友好,我在下面对其进行了调整。

/// <summary>
/// Saves a range of items, handing duplicates. Returns the number of items saved.
/// <see href="https://github.com/dotnet/efcore/issues/24780"/>
/// </summary>
/// <param name="dbset">DbSet to save <paramref name="items"/> to.</param>
/// <param name="items">Items to save. May contains items with same key(s).</param>
/// <param name="context">DbContext that DbSet belongs to. If not specified, it will be fetched via <see cref="ICurrentDbContext"/>.</param>
/// <param name="cancellationToken">Cancellation token.</param>
public static async ValueTask<int> SaveRangeAsync<T>(this DbSet<T> dbset, IEnumerable<T> items, DbContext? context = null, CancellationToken cancellationToken = default) where T: class
{
    var count = 0;
    context = context ?? dbset.GetService<ICurrentDbContext>().Context;

    var keys = context.Model.FindEntityType(typeof(T)).FindPrimaryKey().Properties.Select(e => e.Name);

    foreach (var item in items)
    {
        var existing = context.SameOrDefault(item, keys);

        // If we hit a duplicate key, we need to save and then resume adding.
        if (existing != null)
        {
            count += await context.SaveChangesAsync();
            existing.CurrentValues.SetValues(item);
        }
        else
            context.Add(item);
        if (cancellationToken.IsCancellationRequested)
            break;
    }
    count += await context.SaveChangesAsync();
    return count;
}

/// <summary>
/// Finds the first <see cref="EntityEntry"/> with keys matching <paramref name="item"/>.
/// </summary>
public static EntityEntry<T>? SameOrDefault<T>(this DbContext context, T item, IEnumerable<string> keys) where T: class
{
    var entry = context.Entry(item);
    foreach (var entity in context.ChangeTracker.Entries<T>())
    {
        bool mismatch = false;
        foreach (var key in keys)
        {
            if (!Equals(entity.Property(key).CurrentValue, entry.Property(key).CurrentValue))
            {
                mismatch = true;
                break;
            }
        }
        if (!mismatch)
            return entity;
    }
    return default;
}

用法:

[Fact]
public async Task HandlesDuplicateKeys()
{
    var services = new ServiceCollection()
        .AddDbContext<RepositoryContext>(options => options.UseInMemoryDatabase(Guid.NewGuid().ToString()))
        .BuildServiceProvider();

    var items = new List<ItemA>()
    {
        new ItemA() { Id = 1, A = "Foo" },
        new ItemA() { Id = 1, A = "Bar" }
    };

    using (var context = services.GetRequiredService<RepositoryContext>())
    {
        var count = await context.AList.SaveRangeAsync(items);
        Assert.Equal(2, count);
    }
}

谢谢ajcvickers

相关问题