我有一个用例,其中第三方提供了我希望使用 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”来查看冲突的键值。
管理此用例的适当方法是什么?
答案 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!