这两个实体是一对多关系(由代码第一个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如何知道子节点的状态(新增/删除/修改)呢?

实现这个功能的正确方法是什么?


当前回答

下面的代码片段来自我的一个项目,我在其中实现了同样的事情。它将保存数据,如果有新的条目,更新,如果记录是不可用的张贴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();
        }

其他回答

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

考虑使用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

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

因为发布到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可以接受任何对象,并根据属性名将属性值映射到附加的实体。如果模型中的属性名称与实体中的名称不同,则不能使用此方法,必须逐个分配值。

下面的代码片段来自我的一个项目,我在其中实现了同样的事情。它将保存数据,如果有新的条目,更新,如果记录是不可用的张贴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();
        }

因为我讨厌重复复杂的逻辑,这里有一个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);
}