Search code examples
c#entity-frameworkmany-to-many

Creating copy of entities with many to many relationship without duplicating one of the type


I have problem with copying entities with many to many relationship. I have three entities Company, Role and User defined like this:

Company:

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

    public string Name { get; set; }

    public virtual IList<User> Users { get; set; }
}

User:

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

    public string Name { get; set; }

    public virtual IList<Role> Roles { get; set; }
}

Role:

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

    public string Name { get; set; }

    public virtual IList<User> Users { get; set; }
}

Also, I defined many to many relationship between users and roles:

public class UserConfiguration : EntityTypeConfiguration<User>
{
    public UserConfiguration()
    {
        ToTable("TUser");
        HasKey(x => x.Id);

        HasMany(x => x.Roles).WithMany(x => x.Users).Map(m =>
        {
            m.MapLeftKey("UserId");
            m.MapRightKey("RoleId");
            m.ToTable("TUserRole");
        });
    }
}

I used migrations to create tables in db and obviously EF created table TUserRole (so far everything good).

And now, I would like to create copy of company and users but without copying roles (so I want to create new records at the tables TCompany, TUser and TUserRole, but no new records at the TRole).

I thought that something like this would work but I'm getting exception:

Context context = new Context();

var company = context.Companies.Include(x => x.Users.Select(u => u.Roles)).AsNoTracking().SingleOrDefault();

context.Companies.Add(company);

foreach (var user in company.Users)
{
    foreach (var role in user.Roles)
    {
        context.Entry(role).State = EntityState.Unchanged;
    }
}

context.SaveChanges();

And the exception is Saving or accepting changes failed because more than one entity of type 'Mackas.EF.Model.Role' have the same primary key value.

I understand why I'm getting this (because there is more than one role with the same ID), but I don't know what should be my approach.

Any suggestions?

I'm using EF 6.1.3.


Solution

  • Using AsNoTracking generally is a good idea to obtain a graph of entities that aren't attached to a context. As you know, adding the root entity (company) to a new context will mark all entities in the graph as Added and copying entities is a piece of cake.

    But there's one bummer. AsNoTracking causes EF to materialize a new object for each entity in the result set, because it has no way to track that an entity has already been materialized. So you're OK as long as the object graph only diverges off the root entity. I.e. as long as all associations are 1 - 0..n. It that is true, all entities in the graph will represent exactly one "real" entity.

    However, in your case, there's a m - n association between User and Roles. The graph converges. If some users have the same roles, EF will create duplicate Role objects when using AsNoTracking.

    [By the way, contrary to EF6, EF-core manages to create unique entities even with AsNoTracking]

    The way to go here is to query the object graph by one context, as POCOs, not proxies, and then add/attach it to a second context:

    Company company;
    using (Context context = new Context())
    {
        context.Configuration.ProxyCreationEnabled = false;
        company = context.Companies.Include(x => x.Users.Select(u => u.Roles))
                         .SingleOrDefault();
    }
    
    using (Context context = new Context())
    {
        context.Companies.Add(company);
    
        foreach (var user in company.Users.SelectMany(u => u.Roles)
                                    .Distinct())
        {
            context.Entry(role).State = EntityState.Unchanged;
        }
    
        context.SaveChanges();
    }
    

    Proxies have a reference to the context they were created by, so you can't attach them to a second context.