1

I have two entities which have Many-To-Many relationship as given below.

public class StandardContact {
    ...
    public virtual ICollection<RelayConfig> RelayConfigs { get; set; }
}

public class RelayConfig {
....
    public virtual ICollection<StandardContact> StandardContacts { get; set; }
}

I am trying to update RelayConfig and its relationship with StandardContact. Here is the code that I have put up in an Update() method. It just adds some new StandardContact and/or deletes existing StandardContacts (as requested by user). The parameter exposedContacts represent the all-inclusive List of StandardContacts that should be updated in database

public void Update(RelayConfig relayConfig, List<StandardContact> exposedContacts) {

    RelayConfig dbRelayConfig = context.RelayConfigs.Include(r => r.StandardContacts)
                                       .Where(r => r.Id == relayConfig.Id).SingleOrDefault();
    context.Entry<RelayConfig> (dbRelayConfig).CurrentValues.SetValues(relayConfig);

    List<StandardContact> addedExposedContacts = 
        exposedContacts.Where(c1 => !dbRelayConfig.StandardContacts.Any(c2 => c1.Id == c2.Id)).ToList();
    List<StandardContact> deletedExposedContacts = 
        dbRelayConfig.StandardContacts.Where(c1 => !exposedContacts.Any(c2 => c2.Id == c1.Id)).ToList();

    StandardContact dbExposedContact = null;
    addedExposedContacts.ForEach(exposedContact => {
        dbExposedContact = context.StandardContacts.SingleOrDefault(sc => sc.Id == exposedContact.Id);
        dbRelayConfig.StandardContacts.Add(dbExposedContact);
    });
    deletedExposedContacts.ForEach(exposedContact => { dbRelayConfig.StandardContacts.Remove(exposedContact); });
}

The addition of the new StandardContact entities related to RelayConfig is taking place properly. However, the deletion (last line in the code above) has no effect and the Many-To-Many relation in the link table remains as it is.

In short I am not able to remove StandardContact entities from an RelayConfig object. I don't get any exceptions and the code runs through though.

Update:

Here is the code related to Database:

The RelayContext class

public class RelayContext : DbContext {

    public DbSet<Station> Stations { get; set; }
    public DbSet<StandardContact> StandardContacts { get; set; }
    public DbSet<RelayConfig> RelayConfigs { get; set; }
    public DbSet<StandardRelay> StandardRelays { get; set; }
    public DbSet<Rack> Racks { get; set; }
    public DbSet<Group> Groups { get; set; }
    public DbSet<Relay> Relays { get; set; }
    public DbSet<Contact> Contacts { get; set; }

    public RelayContext() {
        Database.SetInitializer(new RelayContextInitializer());
    }


    protected override void OnModelCreating(DbModelBuilder modelBuilder) {
        modelBuilder.Entity<Rack>().HasOptional<RelayConfig>(r => r.DefaultFirstRelayConfig).WithMany(rc => rc.RacksWithDefaultFirstRelay).WillCascadeOnDelete(false);
        modelBuilder.Entity<Rack>().HasOptional<RelayConfig>(r => r.DefaultSecondRelayConfig).WithMany(rc => rc.RacksWithDefaultSecondRelay).WillCascadeOnDelete(false);
    }
}

The RelayConfigRepository

public class RelayConfigRepository {

    internal RelayContext context;

    public RelayConfigRepository(RelayContext context) {
        this.context = context;
    }
    ....
    public void Update(RelayConfig relayConfig, List<StandardContact> exposedContacts) {
        RelayConfig dbRelayConfig = context.RelayConfigs.Include(r => r.StandardContacts)
                                           .Where(r => r.Id == relayConfig.Id).SingleOrDefault();
        context.Entry<RelayConfig> (dbRelayConfig).CurrentValues.SetValues(relayConfig);

        List<StandardContact> addedExposedContacts = 
            exposedContacts.Where(c1 => !dbRelayConfig.StandardContacts.Any(c2 => c1.Id == c2.Id)).ToList();
        List<StandardContact> deletedExposedContacts = 
            dbRelayConfig.StandardContacts.Where(c1 => !exposedContacts.Any(c2 => c2.Id == c1.Id)).ToList();

        StandardContact dbExposedContact = null;
        addedExposedContacts.ForEach(exposedContact => {
            dbExposedContact = context.StandardContacts.SingleOrDefault(sc => sc.Id == exposedContact.Id);
            dbRelayConfig.StandardContacts.Add(dbExposedContact);
        });
        deletedExposedContacts.ForEach(exposedContact => { dbRelayConfig.StandardContacts.Remove(exposedContact);});

    }
    .... 
}

The UnitOfWork class

public class UnitOfWork : IDisposable {

    private RelayContext context = new RelayContext();

    private bool disposed = false;

    private RelayConfigRepository _relayConfigRepository;
    public RelayConfigRepository RelayConfigRepository {
        get {
            if (_relayConfigRepository == null) {
                _relayConfigRepository = new RelayConfigRepository(context);
            }
            return _relayConfigRepository;
        }
    }

    private StandardContactRepository _standardContactRepository;
    public StandardContactRepository StandardContactRepository {
        get {
            if (_standardContactRepository == null) {
                _standardContactRepository = new StandardContactRepository(context);
            }
            return _standardContactRepository;
        }
    }
    public void Save() {
        try {
            context.SaveChanges();
        }
        catch (DbUpdateConcurrencyException ex) {
            throw new RepositoryException(ErrorMessages.UpdateConcurrencyError);
        }
        catch (Exception ex) {
            throw new RepositoryException(ErrorMessages.GeneralError);
        }
    }

    protected virtual void Dispose(bool disposing) {
        if (!this.disposed) {
            if (disposing) {
                context.Dispose();
            }
        }
        this.disposed = true;
    }

    public void Dispose() {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
}

The WPF Client(ViewModel) Which Calls the Update method on RelayConfigRepository.

[Export(typeof(RelayConfigEditViewModel))]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class RelayConfigEditViewModel :Screen {
    private RelayConfig _relayConfig;
    public RelayConfig RelayConfig {
        get { return _relayConfig; }
        set { _relayConfig = value; NotifyOfPropertyChange(() => RelayConfig); }
    }

    private ObservableCollection<StandardContact> _standardContacts = new ObservableCollection<StandardContact>();
    public ObservableCollection<StandardContact> StandardContacts {
        get { return _standardContacts;}
        set { _standardContacts = value; NotifyOfPropertyChange(() => StandardContacts); }
    }
    ....
    public void Save() {
        List<StandardContact> exposedContacts = StandardContacts.Where(sc => sc.IsMarked).ToList();
        try {
            using (UnitOfWork unitOfWork = new UnitOfWork()) {
                if (editMode == EditMode.Add) {
                    unitOfWork.RelayConfigRepository.Insert(RelayConfig, exposedContacts);
                }
                else {
                    unitOfWork.RelayConfigRepository.Update(RelayConfig, exposedContacts);
                }
                unitOfWork.Save();
            }
        }
        catch (Exception ex) {
            HandleException(ex);
        }
        events.Publish(RelayConfig);
    }
}

Update 2

Tracing was a bit clumsy, but I could see the special procedure statements relating to the Update Method. Here are the relevant SQL staments generated by EF.

update [dbo].[RelayConfigs]
set [Name] = @0, [DisplayName] = @1
where (([Id] = @2) and ([Version] = @3))


**exec sp_executesql N'insert [dbo].[RelayConfigStandardContacts]([RelayConfig_Id], [StandardContact_Id])
values (@0, @1)
',N'@0 int,@1 int',@0=1,@1=4**

As you can see there is SQL for updating RelayConfig record and for adding another entry in the link table RelayConfigStandardContact (a new record that I am adding in many-to-many record). But there is no SQL statement generated for the link table record that I am removing from the many-to-many relationship.

Jatin
  • 4,023
  • 10
  • 60
  • 107
  • Do you call `SaveChanges` after this method somewhere? Your method is perfect and works as expected (if `SaveChanges` is called of course), I've tested it. – Slauma Nov 18 '12 at 12:56
  • Yes, following the UnitOfWork pattern. I can be sure that SaveChanges is called, because addedExposedContacts nicely show up in the database. The problem is only with the deleted Standard Contacts. They don't get deleted from the link table. Can there be some issue with Entity Relationship configuration ? – Jatin Nov 18 '12 at 13:52
  • I don't know what could be wrong. I've posted my test program below. I did no explicit configuration at all, just mapping by convention. Maybe you see some important difference between the test model and your model+configuration? BTW: Are you using SQL Server or another database (and EF provider)? – Slauma Nov 18 '12 at 14:20
  • @Slauma, I created a sample project and tested the code posted by you. It runs fine. So EF is fine. But I still can't figure out why it doesn't work on my actual project. I pass the correct parameters, checked exposedContacts that they are correct, but the delete doesn't work. – Jatin Nov 19 '12 at 15:51
  • Did you check some very simple test cases, like having only one entry in join table and only one element in exposedContacts, that the `Include` populates the expected contacts in the loaded relayconfig, that `deletedExposedContacts` is filled as expected, etc.? Maybe you should post more code and context, like mapping in Fluent API, if you have set any configuration options on the dbcontext, if `relayConfig` and `exposedContacts` are detached or loaded from another context, etc. – Slauma Nov 19 '12 at 16:52
  • I have tested the code on One-To-Many relationship as well and the same problem exists there too. I just cannot remove the many side of the one-to-many relationship. I have updated my question to reflect some more code. – Jatin Nov 20 '12 at 05:45
  • @Slauma, I had commented before that I tested a sample console application with only the 2 entities in question and it worked fine. But when I add my database project into the console application solution and run the code, it depicts the same behavior as in my question, that is, the records do not get deleted from the link table. I have looked into my Database (EF) project for a long time and I couldn't find any issues with it. If you will you be willing to just give look at my EF Database project, then I can upload/email that project to you. Please feel free to refuse. – Jatin Nov 20 '12 at 14:29
  • Did you debug your program? Is the dbRelayConfig object before save what you expect, did you look at the SQL send to SQL server (With SQL profiler)? – Wim Nov 21 '12 at 08:42
  • I did debug the program and looked at all the objects in play. The dbRelayConfig was indeed having the proper StandardContacts just before the SaveChanges is executed. About SQL Profiler, I think I will give a look at it. Will let you know the SQL that is getting executed. – Jatin Nov 21 '12 at 09:12
  • @Wim I traced the generated SQL statements that get executed on the SQLServer. Please see **Update 2** for the same. – Jatin Nov 21 '12 at 12:56
  • Finally, I could stop scratching my head and go ahead with the development. I had overridden hashcode and Equals method in my RelayConfig Entity and the logic in it was responsible for this odd behavior. But it is not obvious that such overriding could result in a bug like this. I will be careful when overriding these methods in future. For the effort taken in affirming me about the correctness of the Update() method, I would award the bounty to @Slauma. – Jatin Nov 21 '12 at 14:32
  • @Nirvan: Entity Framework uses dictionaries internally (with dict key probably as combination of entity key + entity class name or similar). If you implement a custom hashcode that does not reflect this logic (for example a hashcode that can be *different* for the same key and class, maybe depending on other properties) EF doesn't recognize the equality of entities with respect to their entity key correctly anymore. This might cause unforeseeable wrong behaviour. See for example here: http://stackoverflow.com/q/4185537/270591 and here: http://stackoverflow.com/a/371348/270591 – Slauma Nov 21 '12 at 17:06

1 Answers1

0

Test program to show that it should work as expected:

using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;

namespace EFDelMany
{
    public class StandardContact 
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public virtual ICollection<RelayConfig> RelayConfigs { get; set; }
    }

    public class RelayConfig
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public virtual ICollection<StandardContact> StandardContacts { get; set; }
    }

    public class MyContext : DbContext
    {
        public DbSet<StandardContact> StandardContacts { get; set; }
        public DbSet<RelayConfig> RelayConfigs { get; set; }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Database.SetInitializer(new DropCreateDatabaseAlways<MyContext>());
            using (var context = new MyContext())
            {
                context.Database.Initialize(true);

                // Create one RelayConfig and 5 StandardContacts
                var relayConfig1 = new RelayConfig { Name = "R1" };
                var standardContact1 = new StandardContact { Name = "S1" };
                var standardContact2 = new StandardContact { Name = "S2" };
                var standardContact3 = new StandardContact { Name = "S3" };
                var standardContact4 = new StandardContact { Name = "S4" };
                var standardContact5 = new StandardContact { Name = "S5" };

                // Create relationship for StandardContacts 1,2,3
                relayConfig1.StandardContacts = new List<StandardContact>
                {
                    standardContact1,
                    standardContact2,
                    standardContact3
                };

                context.RelayConfigs.Add(relayConfig1);
                context.StandardContacts.Add(standardContact4);
                context.StandardContacts.Add(standardContact5);

                context.SaveChanges();
            }
            // see screenshot "Before"

            RelayConfig relayConfig = new RelayConfig { Id = 1, Name = "R1a" };
            List<StandardContact> exposedContacts = new List<StandardContact>
            {
                // delete relationship to StandardContacts 1 and 3
                new StandardContact { Id = 2 }, // keep relationship to StandardContact 2
                new StandardContact { Id = 4 }, // add relationship to StandardContact 4
                new StandardContact { Id = 5 }  // add relationship to StandardContact 5
            };

            using (var context = new MyContext())
            {
                RelayConfig dbRelayConfig = context.RelayConfigs.Include(r => r.StandardContacts)
                                                   .Where(r => r.Id == relayConfig.Id).SingleOrDefault();
                context.Entry<RelayConfig> (dbRelayConfig).CurrentValues.SetValues(relayConfig);

                List<StandardContact> addedExposedContacts = 
                    exposedContacts.Where(c1 => !dbRelayConfig.StandardContacts.Any(c2 => c1.Id == c2.Id)).ToList();
                List<StandardContact> deletedExposedContacts = 
                    dbRelayConfig.StandardContacts.Where(c1 => !exposedContacts.Any(c2 => c2.Id == c1.Id)).ToList();

                StandardContact dbExposedContact = null;
                addedExposedContacts.ForEach(exposedContact => {
                    dbExposedContact = context.StandardContacts.SingleOrDefault(sc => sc.Id == exposedContact.Id);
                    dbRelayConfig.StandardContacts.Add(dbExposedContact);
                });
                deletedExposedContacts.ForEach(exposedContact => { dbRelayConfig.StandardContacts.Remove(exposedContact); });

                context.SaveChanges();
            }
            // see screenshot "After"
        }
    }
}

Before Update:

Before

After Update:

After

Slauma
  • 175,098
  • 59
  • 401
  • 420