192

The two entities are one-to-many relationship (built by code first 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; }
}

In my WebApi controller I have actions to create a parent entity(which is working fine) and update a parent entity(which has some problem). The update action looks like:

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

Currently I have two ideas:

  1. Get a tracked parent entity named existing by model.Id, and assign values in model one by one to the entity. This sounds stupid. And in model.Children I don't know which child is new, which child is modified(or even deleted).

  2. Create a new parent entity via model, and attached it to the DbContext and save it. But how can the DbContext know the state of children (new add/delete/modified)?

What's the correct way of implement this feature?

Cheng Chen
  • 41,149
  • 16
  • 109
  • 168
  • See also example with GraphDiff in a duplicate question https://stackoverflow.com/questions/29351401/what-is-the-proper-way-to-update-child-entities-in-ef-6/29351950#29351950 – Michael Freidgeim Dec 04 '17 at 18:50

14 Answers14

272

Because the model that gets posted to the WebApi controller is detached from any entity-framework (EF) context, the only option is to load the object graph (parent including its children) from the database and compare which children have been added, deleted or updated. (Unless you would track the changes with your own tracking mechanism during the detached state (in the browser or wherever) which in my opinion is more complex than the following.) It could look like this:

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 can take any object and maps property values to the attached entity based on the property name. If the property names in your model are different from the names in the entity you can't use this method and must assign the values one by one.

Ogglas
  • 50,115
  • 30
  • 272
  • 333
Slauma
  • 171,778
  • 59
  • 393
  • 415
  • 53
    But why ef doesn't have a more "brilliant" way? I think ef can detect if the child is modified/deleted/added, IMO your code above can be part of the EF framework and become a more generic solution. – Cheng Chen Nov 28 '14 at 02:40
  • 8
    @DannyChen: It's indeed a long request that updating disconnected entities should be supported by EF in a more comfortable way (https://entityframework.codeplex.com/workitem/864) but it's still not part of the framework. Currently you can only try the third-party lib "GraphDiff" that is mentioned in that codeplex workitem or write manual code like in my answer above. – Slauma Nov 28 '14 at 12:55
  • I know I'm late to this, but when you mention deleting children, what's the reason for that? And by c.Id == existingChild.Id...are you trying to find "orphaned children"? – Safari137 Jan 11 '16 at 03:06
  • 9
    One thing to add: Within the foreach of update and insert children, you can't do `existingParent.Children.Add(newChild)` because then the existingChild linq search will return the recently added entity, and so that entity will be updated. You just need to insert into a temporary list and then add. – Erre Efe Apr 27 '16 at 21:38
  • Does this method work properly in many-to-many relations as well? – dotNET May 23 '16 at 19:20
  • 4
    @RandolfRincónFadul I just come across this issue. My fix which is a bit less effort is to change the where clause in `existingChild` LINQ query: `.Where(c => c.ID == childModel.ID && c.ID != default(int))` – Gavin Ward Jun 05 '17 at 12:44
  • I found another workaround for multiple child adding. I modified code as following `.Where(c => childModel.ID>0 && c.ID == childModel.ID)` it works if you got an ID auto generated by DB. – D. Pesc. Dec 20 '18 at 10:42
  • 1
    There's a fix coming in 2.2 but in EntityFramework Core 2.1 I had to do something similar to the above. – Ralph Willgoss May 09 '19 at 16:26
  • I get "Sequence contains more than one element" exception on SingleOrDefault as i get 2 values in the Children (one is the existing object from db and second is the updated object from model). Why? – A Coder Jun 20 '19 at 07:38
  • 3
    @RalphWillgoss What's the fix in 2.2 you were talking about? – Jan Paolo Go Aug 02 '19 at 03:09
  • 1
    Just a note for people assigning GUIDs as their Primary Key for the child entity. For this to work, the model you present must have Guids presented so that the Where clause can update the appropriate child. However, if you create child Ids (GUIDS) at the client this wont work. I suspect EF needs a null Primary key value to figure out if a record is new. In that case ensure your assign a default GUID to represent the new item ("00000000-0000-0000-0000-000000000000") or GUID.Empty. – Darren Street Jun 01 '20 at 12:56
  • To avoid the list getting edited mid-way, just add `.ToList()` to loop on a copy. – Etienne Charland Oct 06 '20 at 22:54
  • a question to the answer: why use the `var existingChild = existingParent.Children.Where(c => c.Id == childModel.Id).SingleOrDefault();` and not just `var existingChild = existingParent.Children.SingleOrDefault(c => c.Id == childModel.Id);` ?? – Erik Thysell Oct 09 '20 at 16:00
  • See here for a generic version of this code https://stackoverflow.com/a/64343713/3960200 – Etienne Charland Oct 13 '20 at 21:55
26

OK guys. I had this answer once but lost it along the way. absolute torture when you know there's a better way but can't remember it or find it! It's very simple. I just tested it multiple ways.

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

You can replace the whole list with a new one! The SQL code will remove and add entities as needed. No need to concern yourself with that. Be sure to include child collection or no dice. Good luck!

  • Just what I need, as the number of children in my model is generally quite small, so assuming Linq will delete all original children from the table initially and then add all the new ones the performance impact is not an issue. – William T. Mallard Apr 23 '19 at 21:36
  • @Charles McIntosh. I dont undestand why you set Children again while you Include it in the initial query? – pantonis Jul 23 '19 at 08:12
  • 1
    @pantonis I include the child collection so that it can be loaded for editing. If I rely on the lazy loading to figure it out it doesn't work. I set the children (once) because instead of manually deleting and adding items to the collection I can simply replace the list and entityframework will add and delete items for me. The key is setting the state of the entity to modified and allowing entityframework to do the heavy lifting. – Charles McIntosh Jul 23 '19 at 13:20
  • @CharlesMcIntosh I still dont understand what you are trying to achieve with the children there. You included it in the first request (Include(p=>p.Children). Why do you request it again? – pantonis Jul 25 '19 at 12:13
  • @pantonis, I had to pull the old list using .include() so it gets loaded and attached as a collection from the database. It's how lazy loading is invoked. without it, any changes to the list would not be tracked when I used entitystate.modified. to reiterate, what I'm doing is setting the current child collection to a different child collection. like if a manager got a bunch of new employees or lost a few. I would use a query to include or exclude those new employees and simply replace the old list with a new list then let EF add or delete as needed from the database side. – Charles McIntosh Jul 25 '19 at 16:30
  • 1
    @CharlesMcIntosh ```_dbContext.Children.Where(c => );``` what is the incomplete code here? – pantonis Jul 26 '19 at 16:24
  • @pantonis it could be anything. you could say _dbContext.Children.Where(c => c.WhosYourDaddy == parent.id). Normally these things are determined in the user interface you add and remove children and then collect a list of IDs which is the new list of children and pass it to the back end Update(List newChildList){ Parent.Children = _dbContext.Children.Where(child => newChildList.Contains(child.id)) } . The query is specific to each one's scenario. – Charles McIntosh Jul 26 '19 at 18:35
  • @CharlesMcIntosh sorry but what you did above is still not clear to me. you are requesting the children and then requesting them again. Don't understand why you request them twice. and still don't see the new children. Can you post full example? – pantonis Jul 29 '19 at 09:34
  • 1
    @pantonis @CharlesMcIntosh doesn't need to request the children again, it's just how he's decided to reassign to the `.Children`. The line `parent.Children = ` could be assigned to any new children created. – zola25 Nov 18 '19 at 01:55
  • It sets foreign keys to NULL instead of deleting them. How could I fix this. I want to remove them from DB. – Mahmood Jenami Dec 31 '21 at 14:29
13

I've been messing about with something like this...

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

which you can call with something like:

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

Unfortunately, this kinda falls over if there are collection properties on the child type which also need to be updated. Considering trying to solve this by passing an IRepository (with basic CRUD methods) which would be responsible for calling UpdateChildCollection on its own. Would call the repo instead of direct calls to DbContext.Entry.

Have no idea how this will all perform at scale, but not sure what else to do with this problem.

brettman
  • 491
  • 5
  • 14
  • 1
    Great solution! But fails if add more than one new item, updated dictionary cant have zero id twice. Need some work arround. And also fails if relationship is N -> N, in fact, the item is added to database, but N -> N table is not modified. – RenanStr Jan 19 '18 at 18:41
  • 1
    `toAdd.ForEach(i => (selector(dbItem) as ICollection).Add(i.Value));` should solve n -> n problem. – RenanStr Jan 19 '18 at 19:01
  • this lays out a by code VINN diagram. Thanks. – granadaCoder Aug 13 '21 at 12:14
10

If you are using EntityFrameworkCore you can do the following in your controller post action (The Attach method recursively attaches navigation properties including collections):

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

It is assumed that each entity that was updated has all properties set and provided in the post data from the client (eg. won't work for partial update of an entity).

You also need to make sure that you are using a new/dedicated entity framework database context for this operation.

hallz
  • 176
  • 3
  • 6
6
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));
        }

This is how I solved this problem. This way, EF knows which to add which to update.

Jokeur
  • 131
  • 1
  • 4
  • Worked like a charm! Thanks. – tbdrz Apr 15 '20 at 14:53
  • where is the delete? Also, will this work with client generated GUIDS ? – MattoMK Apr 24 '22 at 23:05
  • What if you're using client-generated guids and the entity is in a disconnected/detached state? So each child in the dto will have a guid that may or may not be in the DB (update, add respectively). But there may be child items in the DB that are not in the list and should be deleted. – MattoMK Apr 25 '22 at 00:25
2

There are a few projects out there that make the interaction between the client and the server easier as far as it concerns saving an entire object graph.

Here are two you'd want to look at:

Both the projects above take recognize the disconnected entities when it's returned to the server, detect and save the changes, and return to the client affected data.

Shimmy Weitzhandler
  • 97,705
  • 120
  • 409
  • 613
2

This ought to do it...

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

Just proof of concept Controler.UpdateModel won't work correctly.

Full class here:

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

}
Shimmy Weitzhandler
  • 97,705
  • 120
  • 409
  • 613
Mertuarez
  • 891
  • 7
  • 24
1

Because I hate repeating complex logic, here's a generic version of Slauma's solution.

Here's my update method. Note that in a detached scenario, sometimes your code will read data and then update it, so it's not always detached.

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 is defined here.

Create these extension methods.

/// <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);
}
Etienne Charland
  • 2,752
  • 2
  • 21
  • 49
0

For VB.NET developers Use this generic sub to mark the child state, easy to use

Notes:

  • PromatCon: the entity object
  • amList: is the child list that you want to add or modify
  • rList: is the child list that you want to remove
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()
Basil
  • 1,523
  • 12
  • 23
0

Here is my code that works just fine.

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;
        }
NoWar
  • 34,755
  • 78
  • 302
  • 475
0

It's not the most elegant procedure, but it works. Cheers!

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

Just remember to remove the 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));
  }
}
Pato
  • 397
  • 1
  • 2
  • 9
0

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 part here.
We want to modify our collection in a way that EF tracking won't end up messed up.
AutoMapper mapping would, unforunately, create a completely new instance of collection, there fore messing up the tracking, although, I was pretty sure it should work. Anyways, since I'm sending complete list from FE, here we actually determine what should be Added/Updated/Deleted and just handle the list itself.
Since EF tracking is ON, EF handles it like a charm.

            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)));
Cubelaster
  • 331
  • 4
  • 5
-1

Refer below code snippet from one of my projects where I implemented the same thing. It will make save data if new entry, updates if existing and delete if record is not available in the posting json. Json Data to help you understand the schema:

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