Search code examples
c#entity-frameworkmany-to-manyentity-framework-6table-per-hierarchy

Issue with many-to-many relationship + TPH inhertitance in Entity Framework 6


I am running into an issue with EF6, though I'm fairly sure that this applies to previous versions that support this type of mapping. I fear I know the answer to the question at hand, but I hope that I am doing something wrong, or there is a better workaround than what I present here. All classes are gutted for clarity.

So I have

public abstract class SoftwareFirmware
{
    public long Id { get; private set; }
    public ICollection<DeviceType> DeviceTypes { get; private set; } 

    public SoftwareFirmware()
    {
        DeviceTypes=new HashSet<DeviceType>();
    }
}

and

public class DeviceType
{
    public long Id { get; set; }
    public virtual ICollection<Firmware> AvailableFirmwareVerions { get; private set; }
    public virtual ICollection<Software> AvailableSoftwareVerions { get; private set; }

    public DeviceType()
    {
        AvailableFirmwareVerions = new HashSet<Firmware>();
        AvailableSoftwareVerions = new HashSet<Software>();
    }
}

which as you can see have a many to many relationship defined. I've defined two classes which derive from SoftwareFirmware, the aptly named

public class Firmware : SoftwareFirmware {}

and

public class Software : SoftwareFirmware {}

I'm using Table Per Hierarchy inheritance, so Software and Firmware are stored in the same table with a discriminator column. Finally, I've mapped the relationships in the derived DbContext's OnModelCreating method with

modelBuilder.Entity<DeviceType>().HasMany(d => d.AvailableFirmwareVerions).WithMany(firmware=>firmware.DeviceTypes);
modelBuilder.Entity<DeviceType>().HasMany(d => d.AvailableSoftwareVerions).WithMany(sofware=>sofware.DeviceTypes);

The problem at hand is that Entity Framework does not seem to support inheritance with this mapping, as I receive the following when EF tries to generate the database:

DeviceTypes: FromRole: NavigationProperty 'DeviceTypes' is not valid. Type 'Software' of FromRole 'DeviceType_AvailableSoftwareVerions_Target' in AssociationType 'DeviceType_AvailableSoftwareVerions' must exactly match with the type 'SoftwareFirmware' on which this NavigationProperty is declared on.

From this I gather that a type that inherits from SoftwareFirmware is not good enough for the NavigationProperty, it must be a SoftwareFirmware type. If I tear the DeviceType collection out of the SoftwareFirmware base class and duplicate it in each of the derived classes, things work, but that's certainly less than ideal.

So finally, my question is- is there another way to configure this so that I can keep my navigation property in my base class? If not, is there a cleaner workaround than what I've described?


UPDATE: so it would seem that SQL Server Management Studio did me wrong, as I had diagrammed out the database previously without the overloaded version of WithMany that takes an expression and it did not include the junction tables. It seems like SSMS doesn't play nice with schema changes in terms adding new diagramming even when the database has been dropped and recreated- it must be restarted. Major pain, but I digress...

As a last ditch effort, I reverted to the parameterless version of WithMany for the mappings, deleted and recreated the database by restarting the application, restarted SSMS, and lo! The junction tables were created. All I needed to do is add an Ignore for the base SoftwareFirmware class's DeviceTypes property and everything generated cleanly. So my FluentAPI mapping code looks like this:

modelBuilder.Entity<DeviceType>().HasMany(d => d.AvailableFirmwareVerions).WithMany();
modelBuilder.Entity<DeviceType>().HasMany(d => d.AvailableSoftwareVerions).WithMany();
modelBuilder.Entity<SoftwareFirmware>().Ignore(s => s.DeviceTypes);

which generates this schema- pretty much exactly the schema I wanted (ignore the extra properties):

enter image description here

However, since the parameterless call to WithMany only hooks up a navigation property on one side, updates to Software.DeviceTypes and Firmware.DeviceTypes aren't tracked by EF so I'm back where I started.


Solution

  • The issue is that you have a single SoftwareFirmware.DeviceTypes property but you are then trying to use it as part of two separate relationships. SoftwareFirmware.DeviceTypes can't be the inverse of both DeviceType.AvailableFirmwareVerions and DeviceType.AvailableSoftwareVerions.

    What you're trying to model is a bit strange because you're kind of treating them as distinct relationships, but also not. There are two options here...

    Option 1: It's two separate relationships

    Remove SoftwareFirmware.DeviceTypes and add a DeviceTypes property on Firmware and Software.

    This is actually what you are doing when you put Ignore on the SoftwareFirmware.DeviceTypes property and use the empty overload of WithMany - which is why it works. You're telling EF that there are two relationships (one Software -> DeviceType and the other Firmware -> DeviceType) and that there is no navigation property that points back the other way. Since you ignored SoftwareFirmware.DeviceTypes it's just not part of your model.

    Option 2: It's one relationship

    Remove the two navigation properties on DeviceType and replace them with a single navigation to the SoftwareFirmware base class. You can always add some façade properties that filter the contents to Software and Firmware (as shown below)

    public class DeviceType
    {
        public long Id { get; set; }
    
        public virtual ICollection<SoftwareFirmware> AvailableVerions { get; private set; }
    
        public virtual IEnumerable<Firmware> AvailableFirmwareVerions
        {
            get
            {
                return this.AvailableVerions.OfType<Firmware>();
            }
        }
    
        public virtual IEnumerable<Software> AvailableSoftwareVerions
        {
            get
            {
                return this.AvailableVerions.OfType<Software>();
            }
        }
    
        public DeviceType()
        {
            AvailableVerions = new HashSet<SoftwareFirmware>();
        }
    }