Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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 fromUpdate to

This doesn't:

Update fromUpdate to

like image 653
dimiguel Avatar asked Feb 23 '14 22:02

dimiguel


1 Answers

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.

like image 155
dimiguel Avatar answered Oct 27 '22 01:10

dimiguel