【问题标题】:EF Core AddRange and entities with duplicate keysEF Core AddRange 和具有重复键的实体
【发布时间】:2021-04-28 21:50:44
【问题描述】:

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

Id Account LastPayment
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”来查看冲突的键值。

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

【问题讨论】:

    标签: entity-framework-core


    【解决方案1】:

    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

    【讨论】:

      猜你喜欢
      • 2021-04-14
      • 1970-01-01
      • 1970-01-01
      • 2019-06-23
      • 2021-05-14
      • 2018-11-30
      • 1970-01-01
      • 2022-11-30
      • 1970-01-01
      相关资源
      最近更新 更多