在TEntity通用存储库上更新父集合和子集合 [英] Update parent and child collections on TEntity generic repository

查看:86
本文介绍了在TEntity通用存储库上更新父集合和子集合的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我的基本存储库类

 公共类存储库< TEntity,TId> :IRepository< TEntity,TId>其中,TEntity:类,IEntity< TId>。 
{
受保护的只读CBSContext _context;
private DbSet< TEntity> _实体;

公共存储库(CBSContext上下文)
{
_context =上下文??抛出新的ArgumentNullException(nameof(context));
_entities = _context.Set< TEntity>();
}

公共异步任务UpdateAsync(TEntity实体)
{
等待Task.Run(()=> _context.Entry(entity).State = EntityState 。改性);

}

//更新添加在
之下的儿童实体代码}

和我的实体接口

 公共接口IEntity< TId> 
{
TId ID {组; }
}

公共类客户:IEntity< int>
{
public ind Id {get;组; }
公用字串CustomerNo {get;组; }
public ICollection< Address>地址{get;组; } = new List< Address>();
}

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


AM引用此答案


.net核心是否有其他替代方法..am是新的..请建议是否有其他替代方法。

解决方案

更新


评论中还出现了一些其他问题。如何处理引用导航属性以及相关实体未实现此类通用接口怎么办,以及使用此类通用方法签名时编译器无法推断通用类型参数。


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


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

 公共静态类EntityGraphUpdateHelper 
{
公共静态异步ValueTask< object> UpdateGraphAsync(此DbContext上下文,对象实体)=>
await context.UpdateGraphAsync(await context.FindEntityAsync(entity),Entity,new HashSet< IForeignKey>());

私有静态异步ValueTask< object> UpdateGraphAsync(此DbContext上下文,对象dbEntity,对象实体,已访问HashSet< IForeignKey>)
{
bool isNew = dbEntity == null;
如果(isNew)dbEntity =实体;
var dbEntry = context.Entry(dbEntity);
if(isNew)
dbEntry.State = EntityState.Added;
else
{
//确保已附加(跟踪)
if(dbEntry.State == EntityState.Detached)
dbEntry.State = EntityState.Unchanged;
//更新原始值
dbEntry.CurrentValues.SetValues(entity);
}
//处理导航
foreach(dbEntry.Navigations中的var navEntry)
{
if(!visited.Add(navEntry.Metadata.ForeignKey))继续; //已经处理过的
等待navEntry.LoadAsync();
if(!navEntry.Metadata.IsCollection())
{
//引用类型导航属性
var refValue = navEntry.Metadata.GetGetter()。GetClrValue(entity);
navEntry.CurrentValue = refValue == null吗? null:
等待上下文。UpdateGraphAsync(navEntry.CurrentValue,refValue,访问);
}
else
{
//集合类型导航属性
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)=>(索引:i,Getter:p.GetGetter(),比较器:p.GetKeyValueComparer()) )
.ToList();
var keyValues =新对象[keyProperties.Count];
void GetKeyValues(object sourceItem)
{
foreach(keyProperties中的var p)
keyValues [p.Index] = p.Getter.GetClrValue(sourceItem);
}
对象FindItem(IEnumerable< object> targetCollection,对象sourceItem)
{
GetKeyValues(sourceItem);
foreach(targetCollection中的targettarget变量)
{
bool keyMatch = true;
foreach(keyProperties中的var p)
{
(var keyA,var keyB)=(p.Getter.GetClrValue(targetItem),keyValues [p.Index]);
keyMatch = p.Comparer?.Equals(keyA,keyB)? object.Equals(keyA,keyB);
如果(!keyMatch)中断;
}
如果(keyMatch)返回targetItem;
}
返回null;
}
//删除当前列表中缺少的db项
foreach(dbItems.ToList()中的var dbItem)
if(FindItem(items,dbItem)== null) accessor.Remove(dbEntity,dbItem);
//添加数据库列表中缺少的当前项目,更新其他项目
var existingItems = dbItems.ToList();
foreach(项目中的可变项目)
{
var dbItem = FindItem(existingItems,item);
if(dbItem == null)
accessor.Add(dbEntity,item,false);
等待context.UpdateGraphAsync(dbItem,item,访问);
}
}
}
return dbEntity;
}

公共静态ValueTask< object> FindEntityAsync(此DbContext上下文,对象实体)
{
var entityType = context.Model.FindRuntimeEntityType(entity.GetType());
var keyProperties = entityType.FindPrimaryKey()。Properties;
var keyValues =新对象[keyProperties.Count];
for(int i = 0; i keyValues [i] = keyProperties [i] .GetGetter()。GetClrValue(entity);
返回context.FindAsync(entityType.ClrType,keyValues);
}
}

请注意,与EF Core方法类似, SaveChangesAsync 调用不是上述方法的一部分,应在以后单独调用。


原始:


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


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


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

 公共静态类EntityUpdateHelper 
{
公共静态异步任务UpdateEntityAsync< TEntity,TId>(此DbContext上下文,TEntity实体,params表达式< Func< TEntity,object>>> []导航离子)
其中TEntity:class,IEntity< TId>
{
var dbEntity =等待上下文。FindAsync< TEntity>(entity.Id);
var dbEntry = context.Entry(dbEntity);
dbEntry.CurrentValues.SetValues(entity);
foreach(导航中的var属性)
{
var propertyName = property.GetPropertyAccess()。Name;
var dbItemsEntry = dbEntry.Collection(propertyName);
var dbItems = dbItemsEntry.CurrentValue;
var items = dbItemsEntry.Metadata.GetGetter()。GetClrValue(entity);
//确定TEntity和TId,并调用UpdateCollection< TEntity,TId>
//通过反射
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});
}

等待上下文。SaveChangesAsync();
}

public static void UpdateCollection< TEntity,TId>(此DbContext上下文,ICollection< TEntity> dbItems,ICollection< TEntity>项)
其中TEntity:class,IEntity< d
{
var dbItemsMap = dbItems.ToDictionary(e => e.Id);
foreach(项目中的可变项目)
{
if(!dbItemsMap.TryGetValue(item.Id,out var oldItem))
dbItems.Add(item);
else
{
context.Entry(oldItem).CurrentValues.SetValues(item);
dbItemsMap.Remove(item.Id);
}
}
foreach(dbItemsMap.Values中的var oldItem)
dbItems.Remove(oldItem);
}
}

并从 Customer 存储库:

 返回等待状态_context.UpdateEntityAsync(entity,e => e.Addresses); 

如果是通用存储库(没有导航参数),并且所有子集合实体都实现了该接口,则简单地迭代 dbEntry.Collections 属性,例如

  // foreach(var属性在导航中)
foreach(dbEntry.Collections中的var dbItemsEntry)
{
// var propertyName = property.GetPropertyAccess()。Name;
// var dbItemsEntry = dbEntry.Collection(propertyName);
var dbItems = dbItemsEntry.CurrentValue;
var items = dbItemsEntry.Metadata.GetGetter()。GetClrValue(entity);
// ...
}


My Base repository Class

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 
}

And my Entity Interface

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>();
}

I need to Add/update/delete child entities in disconnected scenarious.

AM referencing this answer Add/update/delete child entities

And that anser is based on custom BaseEnity but i am using IEnity..

Works done:

i have replace baseentity with Tentity. but Showing error

Below is new Code for save child elements

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());
}

Below Showing Error:

Is There any alternate approach..am new to .net core ..Please suggest if any alternate approach.

解决方案

Update:

Some additional questions arose from the comments. What to do with reference navigation properties and what to do if the related entities do not implement such generic interface, also the inability of compiler inferring the generic type arguments when using such generic method signature.

After thinking a bit I came to conclusion that no base class/interface (and even generic entity type) is needed at all, since EF Core metadata contains all information needed to work with PK (which is used by Find / FindAsync methods and change tracker for instance).

Following is a method which recursively applies disconnected entity graph modifications using only the EF Core metadata information/services. If needed, it could be modified to receive "exclusion" filter in case some entities/collections should be skipped:

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);
    }
}

Please note that similar to EF Core methods, SaveChangesAsync call is not part of the above method, and it should be called separately afterwards.

Original:

Handling collections of entities implementing such generic interface requires slightly different approach, since there is no non generic base class / interface to be used for extracting the Id.

One possible solution is to move the collection handling code to a separate generic method and call it dynamically or via reflection.

For instance (use the VS to determine the necessary 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);
    }
}

and call it from Customer repository:

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

In case of generic repository (no navigations argument), and all child collection entities implementing that interface, simple iterate the dbEntry.Collections property, e.g.

//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);
    // ...
}

这篇关于在TEntity通用存储库上更新父集合和子集合的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆