dimgl dimgl -4 years ago 96
C# Question

Updating many-to-many relationships with a generic repository

I have a database context with lazy loading disabled. I am using eager loading to load all of my entities. I cannot update many to many relationships.

Here's the repository.

public class GenericRepository<TEntity> : IGenericRepository<TEntity>
where TEntity : class
{
... other code here...

public virtual void Update(TEntity t)
{
Set.Attach(t);
Context.Entry(t).State = EntityState.Modified;
}

...other code here...
}


Here's the User model.

public partial class User
{
public User()
{
this.Locks = new HashSet<Lock>();
this.BusinessModels = new HashSet<BusinessModel>();
}

public int UserId { get; set; }
public string Username { get; set; }
public string Name { get; set; }
public string Phone { get; set; }
public string JobTitle { get; set; }
public string RecoveryEmail { get; set; }
public Nullable<double> Zoom { get; set; }

public virtual ICollection<Lock> Locks { get; set; }
public virtual ICollection<BusinessModel> BusinessModels { get; set; }
}


If I modify the business models collection, it does not save the business models collection although I have attached the entire entity.

Worker.UserRepository.Update(user);


I'm not sure what is going on. I don't want to break my generic repository/unit of work pattern just to update many-to-many relationships.

Edit 2: I've got this working...but it is extremely different from the pattern that I'm going for. Having hard implementations means I will need to create a method for each type that has a many to many relationship. I am investigating now to see if I can make this a generic method.

Edit 3: So the previous implementation I had did not work like I thought it would. But now, I have a slightly working implementation. If someone would please help me so I can move on from this, I will love you forever.

public virtual void Update(TEntity updated,
IEnumerable<object> set,
string navigationProperty,
Expression<Func<TEntity, bool>> filter,
Type propertyType)
{
// Find the existing item
var existing = Context.Set<TEntity>().Include(navigationProperty).FirstOrDefault(filter);

// Iterate through every item in the many-to-many relationship
foreach (var o in set)
{
// Attach it if its unattached
if (Context.Entry(o).State == EntityState.Detached)
// Exception "an object with the same key already exists"
// This is due to the include statement up above. That statement
// is necessary in order to edit the entity's navigation
// property.
Context.Set(propertyType).Attach(o);
}

// Set the new value on the navigation property.
Context.Entry(existing).Collection(navigationProperty).CurrentValue = set;

// Set new primitive property values.
Context.Entry(existing).CurrentValues.SetValues(updated);
Context.Entry(existing).State = EntityState.Modified;
}


I then call it like this:

Worker.UserRepository.Update(user, user.BusinessModels, "BusinessModels", i => i.UserId == user.UserId, typeof (BusinessModel));


Extremely messy, but it lets me update many-to-many relationships with generics. My big problem is the exception when I go to attach new values that already exist. They're already loaded because of the include statement.

This works:

Update from Update to

This doesn't:

Update from Update to

Answer Source

After many painful hours, I have finally found a way to update many-to-many relationships with a completely generic repository. This will allow me to create (and save) many different types of entities without creating boilerplate code for each one.

This method assumes that:

  • Your entity already exists
  • Your many to many relationship is stored in a table with a composite key
  • You are using eager loading to load your relationships into context
  • You are using a unit-of-work/generic repository pattern to save your entities.

Here's the Update generic method.

public virtual void Update(Expression<Func<TEntity, bool>> filter,
    IEnumerable<object> updatedSet, // Updated many-to-many relationships
    IEnumerable<object> availableSet, // Lookup collection
    string propertyName) // The name of the navigation property
{
    // Get the generic type of the set
    var type = updatedSet.GetType().GetGenericArguments()[0];

    // Get the previous entity from the database based on repository type
    var previous = Context
        .Set<TEntity>()
        .Include(propertyName)
        .FirstOrDefault(filter);

    /* Create a container that will hold the values of
        * the generic many-to-many relationships we are updating.
        */
    var values = CreateList(type);

   /* For each object in the updated set find the existing
        * entity in the database. This is to avoid Entity Framework
        * from creating new objects or throwing an
        * error because the object is already attached.
        */
    foreach (var entry in updatedSet
        .Select(obj => (int)obj
            .GetType()
            .GetProperty("Id")
            .GetValue(obj, null))
        .Select(value => Context.Set(type).Find(value)))
    {
        values.Add(entry);
    }

    /* Get the collection where the previous many to many relationships
        * are stored and assign the new ones.
        */
    Context.Entry(previous).Collection(propertyName).CurrentValue = values;
}

Here's a helper method I found online which allows me to create generic lists based on whatever type I give it.

public IList CreateList(Type type)
{
    var genericList = typeof(List<>).MakeGenericType(type);
    return (IList)Activator.CreateInstance(genericList);
}

And from now on, this is what calls to update many-to-many relationships look like:

Worker.UserRepository.Update(u => u.UserId == user.UserId,
    user.BusinessModels, // Many-to-many relationship to update
    Worker.BusinessModelRepository.Get(), // Full set
    "BusinessModels"); // Property name

Of course, in the end you will need to somewhere call:

Context.SaveChanges();

I hope this helps anyone who never truly found how to use many-to-many relationships with generic repositories and unit-of-work classes in Entity Framework.

Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download