C# 在EF中更新父实体时如何添加/更新子实体

C# 在EF中更新父实体时如何添加/更新子实体,c#,asp.net-mvc,entity-framework,asp.net-web-api,C#,Asp.net Mvc,Entity Framework,Asp.net Web Api,这两个实体是一对多关系(由CodeFirstFluentAPI构建) 目前我有两个想法: 通过model.Id获取名为existing的跟踪父实体,并将model中的值逐个分配给该实体。这听起来很愚蠢。在model.Children中,我不知道哪个孩子是新的,哪个孩子被修改(甚至被删除) 通过模型创建新的父实体,并将其附加到DbContext并保存。但是DbContext如何知道子级的状态(新增/删除/修改) 实现此功能的正确方法是什么?因为发布到WebApi控制器的模型与任何实体框架(EF)上

这两个实体是一对多关系(由CodeFirstFluentAPI构建)

目前我有两个想法:

  • 通过
    model.Id
    获取名为
    existing
    的跟踪父实体,并将
    model
    中的值逐个分配给该实体。这听起来很愚蠢。在
    model.Children
    中,我不知道哪个孩子是新的,哪个孩子被修改(甚至被删除)

  • 通过
    模型
    创建新的父实体,并将其附加到DbContext并保存。但是DbContext如何知道子级的状态(新增/删除/修改)


  • 实现此功能的正确方法是什么?

    因为发布到WebApi控制器的模型与任何实体框架(EF)上下文分离,所以唯一的选择是从数据库加载对象图(父对象包括其子对象),并比较添加、删除或更新的子对象。(除非您在分离状态(在浏览器中或任何地方)期间使用自己的跟踪机制跟踪更改,我认为这比以下情况更复杂。)它可能如下所示:

    public void Update(UpdateParentModel model)
    {
        var existingParent = _dbContext.Parents
            .Where(p => p.Id == model.Id)
            .Include(p => p.Children)
            .SingleOrDefault();
    
        if (existingParent != null)
        {
            // Update parent
            _dbContext.Entry(existingParent).CurrentValues.SetValues(model);
    
            // Delete children
            foreach (var existingChild in existingParent.Children.ToList())
            {
                if (!model.Children.Any(c => c.Id == existingChild.Id))
                    _dbContext.Children.Remove(existingChild);
            }
    
            // Update and Insert children
            foreach (var childModel in model.Children)
            {
                var existingChild = existingParent.Children
                    .Where(c => c.Id == childModel.Id && c.Id != default(int))
                    .SingleOrDefault();
    
                if (existingChild != null)
                    // Update child
                    _dbContext.Entry(existingChild).CurrentValues.SetValues(childModel);
                else
                {
                    // Insert child
                    var newChild = new Child
                    {
                        Data = childModel.Data,
                        //...
                    };
                    existingParent.Children.Add(newChild);
                }
            }
    
            _dbContext.SaveChanges();
        }
    }
    

    …CurrentValues.SetValues
    可以获取任何对象,并根据属性名称将属性值映射到附着的实体。如果模型中的属性名称与实体中的名称不同,则不能使用此方法,必须逐个赋值。

    我一直在处理类似的问题

    protected void UpdateChildCollection<Tparent, Tid , Tchild>(Tparent dbItem, Tparent newItem, Func<Tparent, IEnumerable<Tchild>> selector, Func<Tchild, Tid> idSelector) where Tchild : class
        {
            var dbItems = selector(dbItem).ToList();
            var newItems = selector(newItem).ToList();
    
            if (dbItems == null && newItems == null)
                return;
    
            var original = dbItems?.ToDictionary(idSelector) ?? new Dictionary<Tid, Tchild>();
            var updated = newItems?.ToDictionary(idSelector) ?? new Dictionary<Tid, Tchild>();
    
            var toRemove = original.Where(i => !updated.ContainsKey(i.Key)).ToArray();
            var removed = toRemove.Select(i => DbContext.Entry(i.Value).State = EntityState.Deleted).ToArray();
    
            var toUpdate = original.Where(i => updated.ContainsKey(i.Key)).ToList();
            toUpdate.ForEach(i => DbContext.Entry(i.Value).CurrentValues.SetValues(updated[i.Key]));
    
            var toAdd = updated.Where(i => !original.ContainsKey(i.Key)).ToList();
            toAdd.ForEach(i => DbContext.Set<Tchild>().Add(i.Value));
        }
    
    不幸的是,如果子类型上有同样需要更新的集合属性,这种情况就会发生。考虑通过传递一个IRepository(带有基本CRUD方法)来解决这个问题,它将负责自己调用UpdateChildCollection。将调用repo,而不是直接调用DbContext.Entry


    不知道这一切将如何在规模上执行,但不确定如何处理此问题。

    只是概念验证
    控制器。UpdateModel
    无法正常工作

    全班:

    const string PK=“Id”;
    受保护的模型;
    受保护的System.Data.Entity.DbSet模型;
    私有void TestUpdate(对象项)
    {
    var props=item.GetType().GetProperties();
    foreach(道具中的var道具)
    {
    对象值=道具获取值(项目);
    if(prop.PropertyType.IsInterface&&value!=null)
    {
    foreach(变量iItem in(System.Collections.IEnumerable)值)
    {
    测试更新(iItem);
    }
    }
    }
    int id=(int)item.GetType().GetProperty(PK).GetValue(item);
    如果(id==0)
    {
    con.Entry(item).State=System.Data.Entity.EntityState.Added;
    }
    其他的
    {
    con.Entry(item).State=System.Data.Entity.EntityState.Modified;
    }
    }
    
    有一些项目可以简化客户机和服务器之间的交互,因为它们涉及到保存整个对象图

    这里有两个你想看的:


    上述两个项目都在断开连接的实体返回到服务器时识别这些实体,检测并保存更改,并返回到受客户端影响的数据。

    如果使用EntityFrameworkCore,则可以在控制器post操作中执行以下操作(递归附加导航属性,包括集合):

    \u context.Attach(modelPostedToController);
    IEnumerable unchangedEntities=_context.ChangeTracker.Entries()。其中(x=>x.State==EntityState.Unchanged);
    foreach(实体进入未更改的ee){
    ee.State=EntityState.Modified;
    }
    wait_context.SaveChangesAsync();
    
    假设更新的每个实体都设置了所有属性,并在来自客户端的post数据中提供(例如,不适用于实体的部分更新)


    您还需要确保为此操作使用了新的/专用的实体框架数据库上下文。

    好的。我曾经有过这样的答案,但一路上都忘了。当你知道有更好的方法但却记不住或找不到时,绝对是折磨!这很简单。我只是用多种方法测试了它

    var parent = _dbContext.Parents
      .Where(p => p.Id == model.Id)
      .Include(p => p.Children)
      .FirstOrDefault();
    
    parent.Children = _dbContext.Children.Where(c => <Query for New List Here>);
    _dbContext.Entry(parent).State = EntityState.Modified;
    
    _dbContext.SaveChanges();
    
    var parent=\u dbContext.Parents
    .Where(p=>p.Id==model.Id)
    .包括(p=>p.儿童)
    .FirstOrDefault();
    parent.Children=_dbContext.Children.Where(c=>);
    _dbContext.Entry(parent.State=EntityState.Modified;
    _dbContext.SaveChanges();
    

    您可以用新列表替换整个列表!SQL代码将根据需要删除和添加实体。你不必担心那件事。一定要包括儿童收集或没有骰子。祝你好运

    @查尔斯·麦金托什(Charles McIntosh)确实为我的情况给出了答案,因为传入的模型是分离的。对我来说,最终起作用的是首先保存传入的模型。。。然后继续添加子项,就像我以前一样:

    public async Task<IHttpActionResult> GetUPSFreight(PartsExpressOrder order)
    {
        db.Entry(order).State = EntityState.Modified;
        db.SaveChanges();
      ...
    }
    
    公共异步任务GetUpsCargo(PartsExpressOrder)
    {
    db.Entry(order).State=EntityState.Modified;
    db.SaveChanges();
    ...
    }
    
    公共异步任务PutParent(int-id,Parent-Parent)
    {
    如果(!ModelState.IsValid)
    {
    返回请求(ModelState);
    }
    if(id!=parent.id)
    {
    返回请求();
    }
    db.Entry(parent.State=EntityState.Modified;
    foreach(parent.Children中的子项)
    {
    db.Entry(child.State=child.Id==0?EntityState.Added:EntityState.Modified;
    }
    尝试
    {
    等待db.saveChangesSync();
    }
    catch(DbUpdateConcurrencyException)
    {
    如果(!ParentExists(id))
    {
    返回NotFound();
    }
    其他的
    {
    投掷;
    }
    
    UpdateChildCollection(dbCopy, detached, p => p.MyCollectionProp, collectionItem => collectionItem.Id)
    
    const string PK = "Id";
    protected Models.Entities con;
    protected System.Data.Entity.DbSet<T> model;
    
    private void TestUpdate(object item)
    {
        var props = item.GetType().GetProperties();
        foreach (var prop in props)
        {
            object value = prop.GetValue(item);
            if (prop.PropertyType.IsInterface && value != null)
            {
                foreach (var iItem in (System.Collections.IEnumerable)value)
                {
                    TestUpdate(iItem);
                }
            }
        }
    
        int id = (int)item.GetType().GetProperty(PK).GetValue(item);
        if (id == 0)
        {
            con.Entry(item).State = System.Data.Entity.EntityState.Added;
        }
        else
        {
            con.Entry(item).State = System.Data.Entity.EntityState.Modified;
        }
    
    }
    
    _context.Attach(modelPostedToController);
    
    IEnumerable<EntityEntry> unchangedEntities = _context.ChangeTracker.Entries().Where(x => x.State == EntityState.Unchanged);
    
    foreach(EntityEntry ee in unchangedEntities){
         ee.State = EntityState.Modified;
    }
    
    await _context.SaveChangesAsync();
    
    var parent = _dbContext.Parents
      .Where(p => p.Id == model.Id)
      .Include(p => p.Children)
      .FirstOrDefault();
    
    parent.Children = _dbContext.Children.Where(c => <Query for New List Here>);
    _dbContext.Entry(parent).State = EntityState.Modified;
    
    _dbContext.SaveChanges();
    
    public async Task<IHttpActionResult> GetUPSFreight(PartsExpressOrder order)
    {
        db.Entry(order).State = EntityState.Modified;
        db.SaveChanges();
      ...
    }
    
    public async Task<IHttpActionResult> PutParent(int id, Parent parent)
            {
                if (!ModelState.IsValid)
                {
                    return BadRequest(ModelState);
                }
    
                if (id != parent.Id)
                {
                    return BadRequest();
                }
    
                db.Entry(parent).State = EntityState.Modified;
    
                foreach (Child child in parent.Children)
                {
                    db.Entry(child).State = child.Id == 0 ? EntityState.Added : EntityState.Modified;
                }
    
                try
                {
                    await db.SaveChangesAsync();
                }
                catch (DbUpdateConcurrencyException)
                {
                    if (!ParentExists(id))
                    {
                        return NotFound();
                    }
                    else
                    {
                        throw;
                    }
                }
    
                return Ok(db.Parents.Find(id));
            }
    
    updatechild(objCas.ECC_Decision, PromatCon.ECC_Decision.Where(Function(c) c.rid = objCas.rid And Not objCas.ECC_Decision.Select(Function(x) x.dcid).Contains(c.dcid)).toList)
    
    Sub updatechild(Of Ety)(amList As ICollection(Of Ety), rList As ICollection(Of Ety))
            If amList IsNot Nothing Then
                For Each obj In amList
                    Dim x = PromatCon.Entry(obj).GetDatabaseValues()
                    If x Is Nothing Then
                        PromatCon.Entry(obj).State = EntityState.Added
                    Else
                        PromatCon.Entry(obj).State = EntityState.Modified
                    End If
                Next
            End If
    
            If rList IsNot Nothing Then
                For Each obj In rList.ToList
                    PromatCon.Entry(obj).State = EntityState.Deleted
                Next
            End If
    End Sub
    
    PromatCon.SaveChanges()
    
    var parent = context.Parent.FirstOrDefault(x => x.Id == modelParent.Id);
    if (parent != null)
    {
      parent.Childs = modelParent.Childs;
    }
    
    public async Task<bool> UpdateDeviceShutdownAsync(Guid id, DateTime shutdownAtTime, int areaID, decimal mileage,
            decimal motohours, int driverID, List<int> commission,
            string shutdownPlaceDescr, int deviceShutdownTypeID, string deviceShutdownDesc,
            bool isTransportation, string violationConditions, DateTime shutdownStartTime,
            DateTime shutdownEndTime, string notes, List<Guid> faultIDs )
            {
                try
                {
                    using (var db = new GJobEntities())
                    {
                        var isExisting = await db.DeviceShutdowns.FirstOrDefaultAsync(x => x.ID == id);
    
                        if (isExisting != null)
                        {
                            isExisting.AreaID = areaID;
                            isExisting.DriverID = driverID;
                            isExisting.IsTransportation = isTransportation;
                            isExisting.Mileage = mileage;
                            isExisting.Motohours = motohours;
                            isExisting.Notes = notes;                    
                            isExisting.DeviceShutdownDesc = deviceShutdownDesc;
                            isExisting.DeviceShutdownTypeID = deviceShutdownTypeID;
                            isExisting.ShutdownAtTime = shutdownAtTime;
                            isExisting.ShutdownEndTime = shutdownEndTime;
                            isExisting.ShutdownStartTime = shutdownStartTime;
                            isExisting.ShutdownPlaceDescr = shutdownPlaceDescr;
                            isExisting.ViolationConditions = violationConditions;
    
                            // Delete children
                            foreach (var existingChild in isExisting.DeviceShutdownFaults.ToList())
                            {
                                db.DeviceShutdownFaults.Remove(existingChild);
                            }
    
                            if (faultIDs != null && faultIDs.Any())
                            {
                                foreach (var faultItem in faultIDs)
                                {
                                    var newChild = new DeviceShutdownFault
                                    {
                                        ID = Guid.NewGuid(),
                                        DDFaultID = faultItem,
                                        DeviceShutdownID = isExisting.ID,
                                    };
    
                                    isExisting.DeviceShutdownFaults.Add(newChild);
                                }
                            }
    
                            // Delete all children
                            foreach (var existingChild in isExisting.DeviceShutdownComissions.ToList())
                            {
                                db.DeviceShutdownComissions.Remove(existingChild);
                            }
    
                            // Add all new children
                            if (commission != null && commission.Any())
                            {
                                foreach (var cItem in commission)
                                {
                                    var newChild = new DeviceShutdownComission
                                    {
                                        ID = Guid.NewGuid(),
                                        PersonalID = cItem,
                                        DeviceShutdownID = isExisting.ID,
                                    };
    
                                    isExisting.DeviceShutdownComissions.Add(newChild);
                                }
                            }
    
                            await db.SaveChangesAsync();
    
                            return true;
                        }
                    }
                }
                catch (Exception ex)
                {
                    logger.Error(ex);
                }
    
                return false;
            }
    
    public async Task UpdateAsync(TempOrder order)
    {
        order.CheckNotNull(nameof(order));
        order.OrderId.CheckNotNull(nameof(order.OrderId));
    
        order.DateModified = _dateService.UtcNow;
    
        if (_context.Entry(order).State == EntityState.Modified)
        {
            await _context.SaveChangesAsync().ConfigureAwait(false);
        }
        else // Detached.
        {
            var existing = await SelectAsync(order.OrderId!.Value).ConfigureAwait(false);
            if (existing != null)
            {
                order.DateModified = _dateService.UtcNow;
                _context.TrackChildChanges(order.Products, existing.Products, (a, b) => a.OrderProductId == b.OrderProductId);
                await _context.SaveChangesAsync(order, existing).ConfigureAwait(false);
            }
        }
    }
    
    /// <summary>
    /// Tracks changes on childs models by comparing with latest database state.
    /// </summary>
    /// <typeparam name="T">The type of model to track.</typeparam>
    /// <param name="context">The database context tracking changes.</param>
    /// <param name="childs">The childs to update, detached from the context.</param>
    /// <param name="existingChilds">The latest existing data, attached to the context.</param>
    /// <param name="match">A function to match models by their primary key(s).</param>
    public static void TrackChildChanges<T>(this DbContext context, IList<T> childs, IList<T> existingChilds, Func<T, T, bool> match)
        where T : class
    {
        context.CheckNotNull(nameof(context));
        childs.CheckNotNull(nameof(childs));
        existingChilds.CheckNotNull(nameof(existingChilds));
    
        // Delete childs.
        foreach (var existing in existingChilds.ToList())
        {
            if (!childs.Any(c => match(c, existing)))
            {
                existingChilds.Remove(existing);
            }
        }
    
        // Update and Insert childs.
        var existingChildsCopy = existingChilds.ToList();
        foreach (var item in childs.ToList())
        {
            var existing = existingChildsCopy
                .Where(c => match(c, item))
                .SingleOrDefault();
    
            if (existing != null)
            {
                // Update child.
                context.Entry(existing).CurrentValues.SetValues(item);
            }
            else
            {
                // Insert child.
                existingChilds.Add(item);
                // context.Entry(item).State = EntityState.Added;
            }
        }
    }
    
    /// <summary>
    /// Saves changes to a detached model by comparing it with the latest data.
    /// </summary>
    /// <typeparam name="T">The type of model to save.</typeparam>
    /// <param name="context">The database context tracking changes.</param>
    /// <param name="model">The model object to save.</param>
    /// <param name="existing">The latest model data.</param>
    public static void SaveChanges<T>(this DbContext context, T model, T existing)
        where T : class
    {
        context.CheckNotNull(nameof(context));
        model.CheckNotNull(nameof(context));
    
        context.Entry(existing).CurrentValues.SetValues(model);
        context.SaveChanges();
    }
    
    /// <summary>
    /// Saves changes to a detached model by comparing it with the latest data.
    /// </summary>
    /// <typeparam name="T">The type of model to save.</typeparam>
    /// <param name="context">The database context tracking changes.</param>
    /// <param name="model">The model object to save.</param>
    /// <param name="existing">The latest model data.</param>
    /// <param name="cancellationToken">A cancellation token to cancel the operation.</param>
    /// <returns></returns>
    public static async Task SaveChangesAsync<T>(this DbContext context, T model, T existing, CancellationToken cancellationToken = default)
        where T : class
    {
        context.CheckNotNull(nameof(context));
        model.CheckNotNull(nameof(context));
    
        context.Entry(existing).CurrentValues.SetValues(model);
        await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
    }
    
    {
        "groupId": 1,
        "groupName": "Group 1",
        "sortOrder": 1,
        "filterNames": [
            {
                "filterId": 1,
                "filterName1": "Name11111",
                "sortOrder": 10,
                "groupId": 1           
            }  ,
            {
                "filterId": 1006,
                "filterName1": "Name Changed 1",
                "sortOrder": 10,
                "groupId": 1           
            }  ,
            {
                "filterId": 1007,
                "filterName1": "New Filter 1",
                "sortOrder": 10,
                "groupId": 1           
            } ,
            {
                "filterId": 2,
                "filterName1": "Name 2 Changed",
                "sortOrder": 10,
                "groupId": 1           
            }                 
        ]
    }
    
    
    public async Task<int> UpdateFilter(FilterGroup filterGroup)
            {                        
                var Ids = from f in filterGroup.FilterNames select f.FilterId;
                var toBeDeleted = dbContext.FilterNames.Where(x => x.GroupId == filterGroup.GroupId
                && !Ids.Contains(x.FilterId)).ToList();
                foreach(var item in toBeDeleted)
                {
                    dbContext.FilterNames.Remove(item);
                }
                await dbContext.SaveChangesAsync();
    
                dbContext.FilterGroups.Attach(filterGroup);
                dbContext.Entry(filterGroup).State = EntityState.Modified;
                for(int i=0;i<filterGroup.FilterNames.Count();i++)            
                {
                    if (filterGroup.FilterNames.ElementAt(i).FilterId != 0)
                    {
                        dbContext.Entry(filterGroup.FilterNames.ElementAt(i)).State = EntityState.Modified;
                    }
                }            
                return await dbContext.SaveChangesAsync();
            }
    
    private void Reconcile<T>(DbContext context,
        IReadOnlyCollection<T> oldItems,
        IReadOnlyCollection<T> newItems,
        Func<T, T, bool> compare)
    {
        var itemsToAdd = new List<T>();
        var itemsToRemove = new List<T>();
    
        foreach (T newItem in newItems)
        {
            T oldItem = oldItems.FirstOrDefault(arg1 => compare(arg1, newItem));
    
            if (oldItem == null)
            {
                itemsToAdd.Add(newItem);
            }
            else
            {
                context.Entry(oldItem).CurrentValues.SetValues(newItem);
            }
        }
    
        foreach (T oldItem in oldItems)
        {
            if (!newItems.Any(arg1 => compare(arg1, oldItem)))
            {
                itemsToRemove.Add(oldItem);
            }
        }
    
        foreach (T item in itemsToAdd)
            context.Add(item);
    
        foreach (T item in itemsToRemove)
            context.Remove(item);
    }