这两个实体是一对多关系(由代码第一个fluent api构建)。

public class Parent
{
    public Parent()
    {
        this.Children = new List<Child>();
    }

    public int Id { get; set; }

    public virtual ICollection<Child> Children { get; set; }
}

public class Child
{
    public int Id { get; set; }

    public int ParentId { get; set; }

    public string Data { get; set; }
}

在我的WebApi控制器中,我有创建父实体(工作正常)和更新父实体(有一些问题)的操作。更新操作如下所示:

public void Update(UpdateParentModel model)
{
    //what should be done here?
}

目前我有两个想法:

获取一个被跟踪的父实体,命名为按模型存在的。Id,并将模型中的值逐个分配给实体。这听起来很愚蠢。在模型中。我不知道哪个子是新的,哪个子是修改的(甚至是删除的)。 通过模型创建一个新的父实体,并将其附加到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));
    }

你可以这样调用:

UpdateChildCollection(dbCopy, detached, p => p.MyCollectionProp, collectionItem => collectionItem.Id)

不幸的是,如果子类型上有集合属性也需要更新,这种方法就会失败。考虑通过传递一个IRepository(带有基本的CRUD方法)来解决这个问题,这个IRepository将负责自己调用UpdateChildCollection。调用repo,而不是直接调用DbContext.Entry。

不知道这将如何大规模地执行,但不确定还能做什么来解决这个问题。


只是概念验证控制器。UpdateModel不能正常工作。

完整的课堂:

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

}

如果你正在使用EntityFrameworkCore,你可以在你的控制器post动作中做以下事情(Attach方法递归地附加导航属性,包括集合):

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

假设每个被更新的实体都设置了所有属性,并在来自客户端的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();

你可以用一个新的列表替换整个列表!SQL代码将根据需要删除和添加实体。你不必为那件事操心。确保包含子集合,否则就没有骰子。好运!


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

这就是我解决这个问题的方法。通过这种方式,EF知道要添加哪些来更新哪些。


VB。NET开发人员使用这个通用子标记子状态,易于使用

注: PromatCon:实体对象 amList:要添加或修改的子列表 rList:要删除的子列表

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

这是我的代码,工作得很好。

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

因为我讨厌重复复杂的逻辑,这里有一个Slauma解决方案的通用版本。

这是我的更新方法。请注意,在分离场景中,有时您的代码将读取数据,然后更新它,因此它并不总是分离的。

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

这里定义了CheckNotNull。

创建这些扩展方法。

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

下面的代码片段来自我的一个项目,我在其中实现了同样的事情。它将保存数据,如果有新的条目,更新,如果记录是不可用的张贴json。 Json数据来帮助你理解模式:

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

这不是最优雅的方法,但很有效。干杯!

var entity = await context.Entities.FindAsync(id);

var newEntity = new AmazingEntity() {
  p1 = child1
  p2 = child2
  p3 = child3.child4 //... nested collections
};

if (entity != null) 
{
  db.Entities.Remove(entity);
}

db.Entities.Add(newEntity);

await db.SaveChangesAsync();

记住去掉PK。

var child4 = Tools.CloneJson(deepNestedElement);
child4.id = 0;
child3.Add(child4);


public static class Tools
{
  public static JsonSerializerSettings jsonSettings = new JsonSerializerSettings {
    ObjectCreationHandling = ObjectCreationHandling.Replace,
    ReferenceLoopHandling = ReferenceLoopHandling.Ignore
  }; 

  public static string JSerialize<T>(T source) {       
    return JsonConvert.SerializeObject(source, Formatting.Indented, jsonSettings);
  }

  public static T JDeserialize<T>(string source) {       
    return JsonConvert.DeserializeObject<T>(source, jsonSettings);
  }

  public static T CloneJson<T>(this T source)
  { 
    return CloneJson<T, T>(source);
  }

  public static TOut CloneJson<TIn, TOut>(TIn source)
  { 
    if (Object.ReferenceEquals(source, null))      
      return default(TOut);      
    return JDeserialize<TOut>(JSerialize(source));
  }
}

So, I finally managed to get it working, although not fully automatically. Notice the AutoMapper <3. It handles all the mapping of properties so you don't have to do it manually. Also, if used in a way where it maps from one object to another, then it only updates the properties and that marks changed properties as Modified to EF, which is what we want. If you would use explicit context.Update(entity), the difference would be that entire object would be marked as Modified and EVERY prop would be updated. In that case you don't need tracking but the drawbacks are as mentioned. Maybe that's not a problem for you but it's more expensive and I want to log exact changes inside Save so I need correct info.

            // We always want tracking for auto-updates
            var entityToUpdate = unitOfWork.GetGenericRepository<Article, int>()
                .GetAllActive() // Uses EF tracking
                .Include(e => e.Barcodes.Where(e => e.Status == DatabaseEntityStatus.Active))
                .First(e => e.Id == request.Id);

            mapper.Map(request, entityToUpdate); // Maps it to entity with AutoMapper <3
            ModifyBarcodes(entityToUpdate, request);

            // Removed part of the code for space

            unitOfWork.Save();

ModifyBarcodes部分在这里。 我们希望以一种EF跟踪不会被打乱的方式修改集合。 不幸的是,AutoMapper映射会创建一个全新的集合实例,因此会搞砸跟踪,尽管,我很确定它应该工作。 无论如何,因为我从FE发送完整的列表,在这里我们实际上决定了应该添加/更新/删除什么,只是处理列表本身。 由于EF跟踪是打开的,EF处理它就像一个魅力。

            var toUpdate = article.Barcodes
                .Where(e => articleDto.Barcodes.Select(b => b.Id).Contains(e.Id))
                .ToList();

            toUpdate.ForEach(e =>
            {
                var newValue = articleDto.Barcodes.FirstOrDefault(f => f.Id == e.Id);
                mapper.Map(newValue, e);
            });

            var toAdd = articleDto.Barcodes
                .Where(e => !article.Barcodes.Select(b => b.Id).Contains(e.Id))
                .Select(e => mapper.Map<Barcode>(e))
                .ToList();

            article.Barcodes.AddRange(toAdd);

            article.Barcodes
                .Where(e => !articleDto.Barcodes.Select(b => b.Id).Contains(e.Id))
                .ToList()
                .ForEach(e => article.Barcodes.Remove(e));


CreateMap<ArticleDto, Article>()
            .ForMember(e => e.DateCreated, opt => opt.Ignore())
            .ForMember(e => e.DateModified, opt => opt.Ignore())
            .ForMember(e => e.CreatedById, opt => opt.Ignore())
            .ForMember(e => e.LastModifiedById, opt => opt.Ignore())
            .ForMember(e => e.Status, opt => opt.Ignore())
            // When mapping collections, the reference itself is destroyed
            // hence f* up EF tracking and makes it think all previous is deleted
            // Better to leave it on manual and handle collecion manually
            .ForMember(e => e.Barcodes, opt => opt.Ignore())
            .ReverseMap()
            .ForMember(e => e.Barcodes, opt => opt.MapFrom(src => src.Barcodes.Where(e => e.Status == DatabaseEntityStatus.Active)));

考虑使用https://github.com/WahidBitar/EF-Core-Simple-Graph-Update。 这对我来说很有效。

这个库很简单,实际上只有一个扩展方法

T InsertUpdateOrDeleteGraph<T>(this DbContext context,
 T newEntity, T existingEntity)

https://github.com/WahidBitar/EF-Core-Simple-Graph-Update/blob/master/src/Diwink.Extensions.EntityFrameworkCore/DbContextExtensions.cs#L34

与这个问题的大多数答案相比,它是通用的(不使用硬编码的表名,可以用于不同的模型), 并包括针对不同模型更改的单元测试。 作者及时回应报告的问题。