【问题标题】:Update parent and child collections on TEntity generic repository更新 TEntity 通用存储库上的父集合和子集合
【发布时间】:2020-08-11 05:48:56
【问题描述】:

我的基础存储库类

public class Repository<TEntity, TId> : IRepository<TEntity, TId> where TEntity : class, IEntity<TId>
{       
    protected readonly CBSContext _context;
    private DbSet<TEntity> _entities;
  
    public Repository(CBSContext context)
    {
        _context = context ?? throw new ArgumentNullException(nameof(context));
        _entities = _context.Set<TEntity>();
    }
    
    public async Task UpdateAsync(TEntity entity)
    {
        await Task.Run(() => _context.Entry(entity).State = EntityState.Modified);
       
    }

    //Update child enitity code added below 
}

还有我的实体接口

public interface IEntity<TId> 
{
    TId Id { get; set; }
}

public class Customer : IEntity<int>
{
    public int Id { get; set; } 
    public string CustomerNo { get; set; }
    public ICollection<Address> Addresses { get; set; } = new List<Address>();
}

我需要在断开连接的场景中添加/更新/删除子实体。

我参考了这个答案Add/update/delete child entities

该分析器基于自定义 BaseEnity,但我使用的是 IEnity..

完成的工作:

我已经用 Tentity 替换了 baseentity。但显示错误

以下是保存子元素的新代码

public async Task UpdateAsync(TEntity entity, params Expression<Func<TEntity, object>>[] navigations)
{
    var dbEntity = await _context.FindAsync<TEntity>(entity.Id);

    var dbEntry = _context.Entry(dbEntity);
    dbEntry.CurrentValues.SetValues(entity);

    foreach (Expression<Func<TEntity, object>> property in navigations)
    {
        var propertyName = property.GetPropertyAccess().Name;
        var dbItemsEntry = dbEntry.Collection(propertyName);
        var accessor = dbItemsEntry.Metadata.GetCollectionAccessor();

        await dbItemsEntry.LoadAsync();
        var dbItemsMap = ((IEnumerable<TEntity>)dbItemsEntry.CurrentValue)
            .ToDictionary(e => e.Id);

        var items = (IEnumerable<object>) accessor.GetOrCreate(entity);

        foreach (var item in items)
        {
            if (!dbItemsMap.TryGetValue(item.Id, out var oldItem))
                accessor.Add(dbEntity, item);
            else
            {
                _context.Entry(oldItem).CurrentValues.SetValues(item);
                dbItemsMap.Remove(item.Id);
            }
        }

        foreach (var oldItem in dbItemsMap.Values)
            accessor.Remove(dbEntity, oldItem);
    }

    await Task.Run(() => _context.SaveChangesAsync());
}

下面显示错误:

是否有任何替代方法..我是 .net 核心的新手..如果有任何替代方法,请提出建议。

【问题讨论】:

  • 不相关,但你为什么要卸载这样的东西await Task.Run(() =&gt; _context.SaveChangesAsync())
  • 谢谢..我需要一个添加/更新/删除子实体的解决方案..有没有其他方法?
  • 请将错误显示为文本而不是图像。
  • 我的代码不适用于刚刚通过@ivon 确认的帐篷类(在链接问题中回答)..我需要另一种方法来保存父实体和子实体..
  • 请尝试dbItemsEntry.Query().AsEnumerable() 而不是accessor.GetOrCreate(entity)

标签: c# entity-framework-core


【解决方案1】:

更新(EF Core 5.0):由于元数据接口更改和跳过导航的引入,更新了部分代码。请注意,代码不处理多对多跳过导航属性。

// process navigations
foreach (var navEntry in dbEntry.Navigations)
{
    if (navEntry.Metadata is not INavigation navigation) continue; // skip navigation pproperty         
    if (!visited.Add(navigation.ForeignKey)) continue; // already processed
    await navEntry.LoadAsync();
    if (!navigation.IsCollection)
    {
        // reference type navigation property
        var refValue = navigation.GetGetter().GetClrValue(entity);
        navEntry.CurrentValue = refValue == null ? null :
            await context.UpdateGraphAsync(navEntry.CurrentValue, refValue, visited);
    }
    else
    {
        // collection type navigation property
        var accessor = navigation.GetCollectionAccessor();
        var items = (IEnumerable<object>)accessor.GetOrCreate(entity, false);
        var dbItems = (IEnumerable<object>)accessor.GetOrCreate(dbEntity, false);
        var itemType = navigation.TargetEntityType;
        var keyProperties = itemType.FindPrimaryKey().Properties
            .Select((p, i) => (Index: i, Getter: p.GetGetter(), Comparer: p.GetKeyValueComparer()))
            .ToList();
        var keyValues = new object[keyProperties.Count];
        void GetKeyValues(object sourceItem)
        {
            foreach (var p in keyProperties)
                keyValues[p.Index] = p.Getter.GetClrValue(sourceItem);
        }
        object FindItem(IEnumerable<object> targetCollection, object sourceItem)
        {
            GetKeyValues(sourceItem);
            foreach (var targetItem in targetCollection)
            {
                bool keyMatch = true;
                foreach (var p in keyProperties)
                {
                    (var keyA, var keyB) = (p.Getter.GetClrValue(targetItem), keyValues[p.Index]);
                    keyMatch = p.Comparer?.Equals(keyA, keyB) ?? object.Equals(keyA, keyB);
                    if (!keyMatch) break;
                }
                if (keyMatch) return targetItem;
            }
            return null;
        }
        // Remove db items missing in the current list
        foreach (var dbItem in dbItems.ToList())
            if (FindItem(items, dbItem) == null) accessor.Remove(dbEntity, dbItem);
        // Add current items missing in the db list, update others
        var existingItems = dbItems.ToList();
        foreach (var item in items)
        {
            var dbItem = FindItem(existingItems, item);
            if (dbItem == null)
                accessor.Add(dbEntity, item, false);
            await context.UpdateGraphAsync(dbItem, item, visited);
        }
    }
}

更新:

cmets 提出了一些其他问题。引用导航属性怎么办,如果相关实体没有实现这样的泛型接口怎么办,以及编译器在使用这样的泛型方法签名时无法推断出泛型类型参数。

经过一番思考,我得出结论根本不需要基类/接口(甚至是通用实体类型),因为 EF Core 元数据包含使用 PK 所需的所有信息(Find/例如FindAsync 方法和更改跟踪器)。

以下是一种仅使用 EF Core 元数据信息/服务递归应用断开连接的实体图修改的方法。如果需要,可以对其进行修改以接收“排除”过滤器,以防某些实体/集合被跳过:

public static class EntityGraphUpdateHelper
{
    public static async ValueTask<object> UpdateGraphAsync(this DbContext context, object entity) =>
        await context.UpdateGraphAsync(await context.FindEntityAsync(entity), entity, new HashSet<IForeignKey>());

    private static async ValueTask<object> UpdateGraphAsync(this DbContext context, object dbEntity, object entity, HashSet<IForeignKey> visited)
    {
        bool isNew = dbEntity == null;
        if (isNew) dbEntity = entity;
        var dbEntry = context.Entry(dbEntity);
        if (isNew)
            dbEntry.State = EntityState.Added;
        else
        {
            // ensure is attached (tracked)
            if (dbEntry.State == EntityState.Detached)
                dbEntry.State = EntityState.Unchanged;
            // update primitive values
            dbEntry.CurrentValues.SetValues(entity);
        }
        // process navigations
        foreach (var navEntry in dbEntry.Navigations)
        {
            if (!visited.Add(navEntry.Metadata.ForeignKey)) continue; // already processed
            await navEntry.LoadAsync();
            if (!navEntry.Metadata.IsCollection())
            {
                // reference type navigation property
                var refValue = navEntry.Metadata.GetGetter().GetClrValue(entity);
                navEntry.CurrentValue = refValue == null ? null :
                    await context.UpdateGraphAsync(navEntry.CurrentValue, refValue, visited);
            }
            else
            {
                // collection type navigation property
                var accessor = navEntry.Metadata.GetCollectionAccessor();
                var items = (IEnumerable<object>)accessor.GetOrCreate(entity, false);
                var dbItems = (IEnumerable<object>)accessor.GetOrCreate(dbEntity, false);
                var itemType = navEntry.Metadata.GetTargetType();
                var keyProperties = itemType.FindPrimaryKey().Properties
                    .Select((p, i) => (Index: i, Getter: p.GetGetter(), Comparer: p.GetKeyValueComparer()))
                    .ToList();
                var keyValues = new object[keyProperties.Count];
                void GetKeyValues(object sourceItem)
                {
                    foreach (var p in keyProperties)
                        keyValues[p.Index] = p.Getter.GetClrValue(sourceItem);
                }
                object FindItem(IEnumerable<object> targetCollection, object sourceItem)
                {
                    GetKeyValues(sourceItem);
                    foreach (var targetItem in targetCollection)
                    {
                        bool keyMatch = true;
                        foreach (var p in keyProperties)
                        {
                            (var keyA, var keyB) = (p.Getter.GetClrValue(targetItem), keyValues[p.Index]);
                            keyMatch = p.Comparer?.Equals(keyA, keyB) ?? object.Equals(keyA, keyB);
                            if (!keyMatch) break;
                        }
                        if (keyMatch) return targetItem;
                    }
                    return null;
                }
                // Remove db items missing in the current list
                foreach (var dbItem in dbItems.ToList())
                    if (FindItem(items, dbItem) == null) accessor.Remove(dbEntity, dbItem);
                // Add current items missing in the db list, update others
                var existingItems = dbItems.ToList();
                foreach (var item in items)
                {
                    var dbItem = FindItem(existingItems, item);
                    if (dbItem == null)
                        accessor.Add(dbEntity, item, false);
                    await context.UpdateGraphAsync(dbItem, item, visited);
                }
            }
        }
        return dbEntity;
    }

    public static ValueTask<object> FindEntityAsync(this DbContext context, object entity)
    {
        var entityType = context.Model.FindRuntimeEntityType(entity.GetType());
        var keyProperties = entityType.FindPrimaryKey().Properties;
        var keyValues = new object[keyProperties.Count];
        for (int i = 0; i < keyValues.Length; i++)
            keyValues[i] = keyProperties[i].GetGetter().GetClrValue(entity);
        return context.FindAsync(entityType.ClrType, keyValues);
    }
}

请注意,与 EF Core 方法类似,SaveChangesAsync 调用不属于上述方法,应在之后单独调用。

原文:

处理实现此类通用接口的实体集合需要稍微不同的方法,因为没有非通用基类/接口可用于提取Id

一种可能的解决方案是将集合处理代码移至单独的通用方法并动态或通过反射调用它。

例如(使用VS确定必要的usings):

public static class EntityUpdateHelper
{
    public static async Task UpdateEntityAsync<TEntity, TId>(this DbContext context, TEntity entity, params Expression<Func<TEntity, object>>[] navigations)
        where TEntity : class, IEntity<TId>
    {
        var dbEntity = await context.FindAsync<TEntity>(entity.Id);
        var dbEntry = context.Entry(dbEntity);
        dbEntry.CurrentValues.SetValues(entity);
        foreach (var property in navigations)
        {
            var propertyName = property.GetPropertyAccess().Name;
            var dbItemsEntry = dbEntry.Collection(propertyName);
            var dbItems = dbItemsEntry.CurrentValue;
            var items = dbItemsEntry.Metadata.GetGetter().GetClrValue(entity);
            // Determine TEntity and TId, and call UpdateCollection<TEntity, TId>
            // via reflection
            var itemType = dbItemsEntry.Metadata.GetTargetType().ClrType;
            var idType = itemType.GetInterfaces()
                .Single(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEntity<>))
                .GetGenericArguments().Single();
            var updateMethod = typeof(EntityUpdateHelper).GetMethod(nameof(UpdateCollection))
                .MakeGenericMethod(itemType, idType);
            updateMethod.Invoke(null, new[] { dbItems, items });
        }

        await context.SaveChangesAsync();
    }

    public static void UpdateCollection<TEntity, TId>(this DbContext context, ICollection<TEntity> dbItems, ICollection<TEntity> items)
        where TEntity : class, IEntity<TId>
    {
        var dbItemsMap = dbItems.ToDictionary(e => e.Id);
        foreach (var item in items)
        {
            if (!dbItemsMap.TryGetValue(item.Id, out var oldItem))
                dbItems.Add(item);
            else
            {
                context.Entry(oldItem).CurrentValues.SetValues(item);
                dbItemsMap.Remove(item.Id);
            }
        }
        foreach (var oldItem in dbItemsMap.Values)
            dbItems.Remove(oldItem);
    }
}

并从Customer 存储库调用它:

return await _context.UpdateEntityAsync(entity, e => e.Addresses);

如果是通用存储库(无导航参数),以及实现该接口的所有子集合实体,只需迭代 dbEntry.Collections 属性,例如

//foreach (var property in navigations)
foreach (var dbItemsEntry in dbEntry.Collections)
{
    //var propertyName = property.GetPropertyAccess().Name;
    //var dbItemsEntry = dbEntry.Collection(propertyName);
    var dbItems = dbItemsEntry.CurrentValue;
    var items = dbItemsEntry.Metadata.GetGetter().GetClrValue(entity);
    // ...
}

【讨论】:

  • 你可以传递多个集合访问器,例如_context.UpdateEntityAsync(entity, e =&gt; e.Addresses, e =&gt; e.Documens, ...) 如果是这个问题。
  • @HarishMashetty 不幸的是,上面的泛型方法签名不允许推断泛型类型参数,因此必须显式提供它们,例如_context.UpdateEntityAsync&lt;Customer, int&gt;(...)。如果您的实体使用int Id,则考虑使用链接帖子中的方法,但不要使用基类,而是使用非通用接口,如public interface IEntity { int Id { get; set; }。它允许删除第二个通用 arg 并在调用它时推断第一个。
  • &Ajt 链接帖子中的原始问题仅针对集合类型属性,因此我没有考虑引用类型导航属性。很可能它们可以/必须合并到相同的方法中。给我一些时间,我会尽快回复您。
  • 你去。新方法(至少在想法上)应该处理所有类型的导航。
  • @IvanStoev 似乎 Metadata.ForeignKey 属性在 EF Core 6.x 中不可用?您熟悉替代方案吗?
猜你喜欢
  • 1970-01-01
  • 2012-07-27
  • 1970-01-01
  • 2016-01-04
  • 1970-01-01
  • 2022-11-16
  • 2020-02-27
  • 2017-01-28
  • 2010-11-24
相关资源
最近更新 更多