Home > Mobile >  How to delete an entry belonging to a many-to-many relationship in .NET EF 6 without using join clas
How to delete an entry belonging to a many-to-many relationship in .NET EF 6 without using join clas

Time:01-08

Hey everyone I am doing a project where I have plenty of many-to-many relationships and I need to figure out how to delete them. I don't want to create join classes for each of them if possible as there are a lot of many-to-many relationships. I created the database with a code first approach using Fluent API. The relationships are in the database just fine and adding an entry to a many-to-many relationship is done by using DbContext.Update() function. I send the object that has the object I want to see in my many-to-many table inside a list to this function. But when I get the object with many-to-many relationship entries inside the list and remove them from that list DbContext.Update() function does not delete those entries in the database. I can not use DbContext.Remove() as I don't have join classes. Is there another way of achieving adding and deleting functions in many-to-many relationships? Here are the parts of the code:

public class FullStackBootcampSetTubeDBContext : DbContext
{
    //creating db connection
    //assigning entities as DbSet<> properties
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
    
        //creating tables for other entities
        modelBuilder.Entity<Channel>().HasMany(i => i.Subscribers).WithMany(j => j.SubscribedTo).UsingEntity<Dictionary<string, object>>(
        "ChannelSubscriber",
        k => k
            .HasOne<User>()
            .WithMany()
            .HasForeignKey("UserId")
            .HasConstraintName("FK_ChannelSubscriber_Users_UserId")
            .OnDelete(DeleteBehavior.Cascade),
        k => k
            .HasOne<Channel>()
            .WithMany()
            .HasForeignKey("ChannelId")
            .HasConstraintName("FK_ChannelSubscriber_Channels_ChannelId")
            .OnDelete(DeleteBehavior.ClientCascade));
        //creating tables for other entities
    }
}

This part is for the creation of the many-to-many table.

public class EfEntityRepositoryBase<TEntity, TContext> : IEntityRepository<TEntity> 
        where TEntity : class, IEntity, new() 
        where TContext : DbContext, new()
{
    public void Add(TEntity entity)
    {
        using (TContext DbContext = new TContext()) 
        {
            DbContext.Add<TEntity>(entity);
            DbContext.SaveChanges();
        }
    }

    public void Delete(TEntity entity)
    {
        using (TContext DbContext = new TContext())
        {
            DbContext.Remove<TEntity>(entity);
            DbContext.SaveChanges();
        }
    }

    public virtual TEntity Get(Expression<Func<TEntity, bool>> filter)
    {
        using (TContext DbContext = new TContext())
        {
            return DbContext.Set<TEntity>().SingleOrDefault(filter);
        }
    }

    public List<TEntity> GetAll(Expression<Func<TEntity, bool>> filter = null)
    {
        using (TContext DbContext = new TContext())
        {
            return filter == null ? DbContext.Set<TEntity>().ToList() : DbContext.Set<TEntity>().Where(filter).ToList();
        }
    }

    public void Update(TEntity entity)
    {
        using (TContext DbContext = new TContext())
        {
            DbContext.Update<TEntity>(entity);
            DbContext.SaveChanges();
        }
    }
}

This part is for the CRUD operations on the database. I am not so sure what type of approach I need to follow in these functions as I am fairly new to .NET EF. IEntityRepository is the interface of this class.

public IResult DeleteSubscribedChannel(int id, int channelId)
{
    try
    {
        var user = _userDao.Get(p => p.Id == id);
        var channel = _channelDao.Get(p => p.Id == channelId);
        if (user != null)
        {
            if (channel != null)
            {
                if (user.SubscribedTo.Where((channel) => channel.Id == channelId) != null)
                {
                    user.SubscribedTo = user.SubscribedTo.Where((channel) => channel.Id != channelId).ToList();
                    channel.SubscriberCount--;
                    _channelDao.Update(channel);
                    _userDao.Update(user);
                    return new SuccessResult(Messages.UserUnsubscribedTr);
                }
                else
                {
                    return new ErrorResult(Messages.UserIsNotSubscribed);
                }
            }
            return new ErrorResult(Messages.NoChannel);
        }
        else
        {
            return new ErrorResult(Messages.NoUser);
        }
    }
    catch (Exception Ex)
    {
        return new ErrorResult(Ex.Message);
    }
}

This part is where I use the functions to delete the entries. userDao and channelDao are my repositories where I use previous functions in EfEntityRepositoryBase to apply CRUD operations.

As I mentioned before I tried using DbContext.Update() and it didn't work. Before that I was using DbContext.Entry() and modified the state of the returned object but using this it didn't add the many-to-many relationship entries into the database even though I added them into the list of the object. If possible I would like to avoid using join classes and Include() function as then I would have to modify CRUD functions for all entities repositories.

Edit

I changed the Update function in EfEntityRepositoryBase to include other relationships but it still does not delete the many-to-many entry. Here is the updated version:

public void Update(TEntity entity, Func<IQueryable<TEntity>, IIncludableQueryable<TEntity, object>> include = null)
    {
        using (TContext DbContext = new TContext())
        {
            DbSet<TEntity> dbSet = DbContext.Set<TEntity>();
            IQueryable<TEntity> entities = dbSet;
            if (include != null)
            {
                entities = include(entities);
            }
            dbSet.Update(entity);
            DbContext.SaveChanges();
        }
    }

Also overriding this function and getting the user from the database then changing the lists of the user with the updated versions fixes this problem but violates generic repository pattern. Update function in the edit part can still Add new many-to-many entries but can not delete.

CodePudding user response:

I generally discourage developers from using a Generic Repository pattern with EF as it negates many of the benefits that EF brings to the table when dealing with related entities.

In a typical many-to-many relationship, say between User and Channel EF supports both with a joining entity and without:

With would look something like:

public class User
{
    // ...
    public virtual ICollection<UserChannel> UserChannels { get; set; } = new List<UserChannel>();
}

public class Channel
{
    // ...
    public virtual ICollection<UserChannel> UserChannels { get; set; } = new List<UserChannel>();
}

public class UserChannel
{
    public virtual User User { get; set; }
    public virtual Channel Channel { get; set; }
}

You would remove a channel either by having a DbSet of UserChannels, finding the appropriate one and removing it, or removing the desired record from User's (or Channel's) UserChannels collection.

Without the joining entity EF supports:

public class User
{
    // ...
    public virtual ICollection<Channel> Channels { get; set; } = new List<Channel>();
}

public class Channel
{
    // ...
    public virtual ICollection<User> Users { get; set; } = new List<User>();
}

... where any UserChannel table record is managed behind the scenes by EF. To remove a UserChannel association record using this method you would do so by letting EF manage the tracking references. This requires eager loading the associations then modifying the collection:

var user = _context.Users.Include(x => x.Channels).Single(x => x.UserId == userId);
var channelToRemove = user.Channels.SingleOrDefault(x => x.ChannelId == channelIdToRemove);
if (channelToRemove != null)
    user.Channels.Remove(channelToRemove);

_context.SaveChanges();

If you wanted to delete a channel and ensure all users are updated to remove it:

var channel = _context.Channels.Include(x => x.Users).Single(x => x.ChannelId == ChannelId);
_context.Channels.Remove(channel);
_context.SaveChanges();

If the relationships are set up to cascade then EF should drop all UserChannel records for that Channel. Any tracked Users in the DbContext should reflect this change as well.

I do not recommend Generic Repositories because they tend to put blinders on code to expect to work with individual entities rather than entity graphs. (An entity and its associations) When manipulating entity relationships you generally want to eager-load those associations to ensure that EF is working with a complete picture. It can then ensure that all FKs are managed properly, and it can manage linking tables behind many-to-many relationships which are just cleaner to work with as user.Channels rather than user.UserChannels.Select(uc => uc.Channel)

  • Related