218

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?

1

15 Answers 15

302

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.

Sign up to request clarification or add additional context in comments.

15 Comments

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.
@DannyChen: It's indeed a long request that updating disconnected entities should be supported by EF in a more comfortable way (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.
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.
@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))
@RalphWillgoss What's the fix in 2.2 you were talking about?
|
35

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!

13 Comments

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.
@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.
@CharlesMcIntosh _dbContext.Children.Where(c => <Query for New List Here>); what is the incomplete code here?
@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.
It sets foreign keys to NULL instead of deleting them. How could I fix this. I want to remove them from DB.
|
15

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.

2 Comments

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.
toAdd.ForEach(i => (selector(dbItem) as ICollection<Tchild>).Add(i.Value)); should solve n -> n problem.
12

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.

1 Comment

It doesn't delete removed entities from a parent collection
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.

2 Comments

where is the delete? Also, will this work with client generated GUIDS ?
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.
6

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

2 Comments

what about items to update?
How does this work with child items?
2

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

Comments

2

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

Comments

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

}

Comments

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

2 Comments

Please keep it to C# answers for C# questions :)
if you are used to search for VB answer, usually you will end up finding the answers in C#, --!--
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;
        }

Comments

0

Consider to use https://github.com/WahidBitar/EF-Core-Simple-Graph-Update. It works well for me.

The library is simple, practically has only one extension method

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

Compare to most of the answers to this question, it is generic (doesn’t use hardcoded table names, can be used for different models), and includes unit tests for different model changes.
The author promptly responses to reported issues.

2 Comments

Should be a comment as it does not answer the question.
@EdwardOlamisan, it gives the solution to the problem. Have you tried, and it didn’t work for you?
0

Similar to @Slauma answer here https://stackoverflow.com/a/27177623/3850405.

This is Microsofts example for handling disconnected entities with Insert, Update or Delete Graph.

public static async Task InsertUpdateOrDeleteGraph(BloggingContext context, Blog blog)
{
    var existingBlog = await context.Blogs
        .Include(b => b.Posts)
        .FirstOrDefaultAsync(b => b.BlogId == blog.BlogId);

    if (existingBlog == null)
    {
        context.Add(blog);
    }
    else
    {
        context.Entry(existingBlog).CurrentValues.SetValues(blog);
        foreach (var post in blog.Posts)
        {
            var existingPost = existingBlog.Posts
                .FirstOrDefault(p => p.PostId == post.PostId);

            if (existingPost == null)
            {
                existingBlog.Posts.Add(post);
            }
            else
            {
                context.Entry(existingPost).CurrentValues.SetValues(post);
            }
        }

        foreach (var post in existingBlog.Posts)
        {
            if (!blog.Posts.Any(p => p.PostId == post.PostId))
            {
                context.Remove(post);
            }
        }
    }

    await context.SaveChangesAsync();
}

Source: https://learn.microsoft.com/en-us/ef/core/saving/disconnected-entities#handling-deletes

Comments

0

My searches landed me here, so I post this solution here for others. Generally people talk about collections, but I just wanted to utilize Owned classes, and i found that doing CurrentValues.SetValues wouldn't touch my owned columns as they are a navigation away in a sense...

Owned Class Example

public class SomeClass
{
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int SomeClassID { get; set; }

// ...
    public WorkDays WorkDays { get; set; } = new WorkDays();
}

public class SomeOtherClass
{
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int SomeOtherClassID { get; set; }

// ...
    public WorkDays WorkDays { get; set; } = new WorkDays();
}

[Owned]
public class WorkDays
{
    public bool Sunday { get; set; } = false;
    public bool Monday { get; set; } = false;
    public bool Tuesday { get; set; } = false;
    public bool Wednesday { get; set; } = false;
    public bool Thursday { get; set; } = false;
    public bool Friday { get; set; } = false;
    public bool Saturday { get; set; } = false;
}

Defining SetValuesIncludingOwned

using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore;

namespace SomeProject.Helpers
{
    public static class DbContextExtensions
    {
        public static IEnumerable<INavigation> GetOwnedNavigations<TEntity>(this DbContext context) where TEntity : class
        {
            var entityType = context.Model.FindEntityType(typeof(TEntity));
            if (entityType == null)
            {
                throw new InvalidOperationException($"Entity type '{typeof(TEntity).Name}' not found in the model.");
            }

            // Retrieve all owned navigations
            var ownedNavigations = entityType.GetNavigations()
                .Where(n => n.TargetEntityType.IsOwned());

            return ownedNavigations;
        }

        public static IEnumerable<IProperty> GetOwnedProperties<TEntity>(this DbContext context) where TEntity : class
        {
            var ownedNavigations = context.GetOwnedNavigations<TEntity>();
            var ownedProperties = ownedNavigations.SelectMany(n => n.TargetEntityType.GetProperties());

            return ownedProperties;
        }

        public static void SetValuesIncludingOwned<TEntity>(this DbContext context, TEntity existingEntity, TEntity NewValues) where TEntity : class
        {
            context.Entry(existingEntity).CurrentValues.SetValues(NewValues);

            var ownedNavigations = context.GetOwnedNavigations<TEntity>();

            foreach (var ownedNavigation in ownedNavigations)
            {
                var property = typeof(TEntity).GetProperty(ownedNavigation.Name);

                property.SetValue(existingEntity, property.GetValue(NewValues));
            }
        }
    }
}

Usage

private SomeClass SomeClass; // Values Modified by User

var context = await DbFactory.CreateDbContextAsync();

if (SomeClass.SomeClassID == 0) // New
{
    context.Add(SomeClass);
}
else // Update
{
    var existingSomeClass = await context.SomeClass.FirstOrDefaultAsync(s => s.SomeClassID == SomeClass.SomeClassID );

    context.SetValuesIncludingOwned<SomeClass>(existingSomeClass, SomeClass);
}

await context.SaveChangesAsync();

It's still a work in progress, but now the owned properties aren't being left behind, where before I was just calling context.Entry(existingSomeClass).CurrentValues.SetValues(SomeClass);

I only leave this here because google really wanted me here despite my search terms, so I hope this helps someone. My goal was to be able to add owned values and not have to overly maintain them differently.

I imagine some of the child graph solutions would probably work for my situation, but I wanted to do far less with this, and just make sure the object itself with all owned items got updated.

Comments

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

3 Comments

How does this improve all the other answers that are given?
It is shorter for a start
@GertArnold: I think, it's complete one.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.