Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Entity Framework Core - update related collection

I'm trying to update collection of ProjectEmployees inside ProjectModel. I want to remove all old values and set new.

My models:

public class Project
{
    ... 
    public ICollection<ProjectEmployee> ProjectEmployees { get; set; }
}

public class ProjectEmployee
{
    public int ProjectId { get; set; }
    public virtual Project Project { get; set; }
    public int UserId { get; set; }
    public virtual Employee Employee { get; set; }
}

public class Employee
{
    public int UserId { get; set; }
    public User User { get; set; }
    ...
}
public class ProjectGroupModel //ViewModel
{
    public int ProjectId { get; set; }
    public ICollection<Employee> ProjectEmployees { get; set; }
}

It's typical many-to-many relationship.

My controller action:

    [HttpPost("group")]
    public async Task<IActionResult> CreateGroup([FromBody] ProjectGroupModel pro)
    {
            var dbProject = await _context.Project
                .Include(p=>p.ProjectEmployees)
                .FirstAsync(p => p.ProjectId == pro.ProjectId);
            dbProject.ProjectEmployees.Clear();

            foreach (var emp in pro.ProjectEmployees)
            {
                dbProject.ProjectEmployees.Add(new ProjectEmployee()
                {
                    UserId = emp.UserId
                });
            }

            await _context.SaveChangesAsync();

            return Ok();
    }

When pro.ProjectEmployees is empty all records from dbProject.ProjectEmployees were removed correctly, also if dbProject.ProjectEmployees is empty new records from model were added, but when dbProject.ProjectEmployees is not empty I can't set new records:

ERROR:

"The instance of entity type 'ProjectEmployee' cannot be tracked because another instance of this type with the same key is already being tracked. When adding new entities, for most key types a unique temporary key value will be created if no key is set (i.e. if the key property is assigned the default value for its type). If you are explicitly setting key values for new entities, ensure they do not collide with existing entities or temporary values generated for other new entities. When attaching existing entities, ensure that only one entity instance with a given key value is attached to the context."

I tried to repair this action in a hundreds way but always sth is wrong.

like image 560
Kuba Avatar asked Oct 03 '16 16:10

Kuba


2 Answers

It's far from perfect, but the only way I was able to make it work is to remove the items from the corresponding DbSet and call SaveChanges before adding the new ones:

    var dbProject = await _context.Project
        .Include(p=>p.ProjectEmployees)
        .FirstAsync(p => p.ProjectId == pro.ProjectId);

    if (dbProject.ProjectEmployees.Any())
    {
        _context.ProjectEmployee.RemoveRange(dbProject.ProjectEmployees);
        await _context.SaveChangesAsync();
    }

    foreach (var emp in pro.ProjectEmployees)
    {
        dbProject.ProjectEmployees.Add(new ProjectEmployee()
        {
            UserId = emp.UserId
        });
    }
    
    await _context.SaveChangesAsync();
like image 97
Ivan Stoev Avatar answered Nov 20 '22 07:11

Ivan Stoev


linked to another SO question

I can answer it here with your classes.

Then use this extension i made to remove the unselected and add the newly selected to the list

    public static void TryUpdateManyToMany<T, TKey>(this DbContext db, IEnumerable<T> currentItems, IEnumerable<T> newItems, Func<T, TKey> getKey) where T : class
    {
        db.Set<T>().RemoveRange(currentItems.Except(newItems, getKey));
        db.Set<T>().AddRange(newItems.Except(currentItems, getKey));
    }

    public static IEnumerable<T> Except<T, TKey>(this IEnumerable<T> items, IEnumerable<T> other, Func<T, TKey> getKeyFunc)
    {
        return items
            .GroupJoin(other, getKeyFunc, getKeyFunc, (item, tempItems) => new { item, tempItems })
            .SelectMany(t => t.tempItems.DefaultIfEmpty(), (t, temp) => new { t, temp })
            .Where(t => ReferenceEquals(null, t.temp) || t.temp.Equals(default(T)))
            .Select(t => t.t.item);
    }

using it looks like this

 var model = db.Employees
             .Include(x => x.ProjectEmployees)
             .FirstOrDefault(x => x.EmployeeId == employee.EmployeeId);

 db.TryUpdateManyToMany(model.ProjectEmployees, listOfNewProjectIds
 .Select(x => new ProjectEmployee
 {
     ProjectId = x,
     EmployeeId = employee.EmployeeId
 }), x => x.ProjectId );
like image 4
Paw Ormstrup Madsen Avatar answered Nov 20 '22 07:11

Paw Ormstrup Madsen