dotnet / efcore

EF Core is a modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations.
https://docs.microsoft.com/ef/
MIT License
13.79k stars 3.19k forks source link

Child inheritance generating InvalidCastException #10446

Closed seantarogers closed 2 years ago

seantarogers commented 6 years ago

Issue

I am trying to use Table Per Hierarchy inheritance in conjunction with a one to many relationship. Both the Parent and child entities use inheritance. I have a very simple entity model. I have a one base parent entity: Session which has two entities which extend from it: QuotingSession and BackOfficeSession. Both of these two parent entities contain a collection of child entities (a one to many relationship). The child entities are also built using inheritance. There is a base child entity: Policy. This base child entity is extended by two entities: QuotingPolicy and BackOfficePolicy.

When I construct either of the Parent entities and attempt to save I receive this exception:

System.InvalidCastException: Unable to cast object of type 'NetCore21.QuotingSession' to type 'NetCore21.BackOfficeSession'

Full stack trace:

{Microsoft.EntityFrameworkCore.DbUpdateException: An error occurred while updating the entries. See the inner exception for details. ---> System.InvalidCastException: Unable to cast object of type 'NetCore21.QuotingSession' to type 'NetCore21.BackOfficeSession'.
   at Microsoft.EntityFrameworkCore.Metadata.Internal.ClrICollectionAccessor`3.GetCollection(Object instance)
   at Microsoft.EntityFrameworkCore.Metadata.Internal.ClrICollectionAccessor`3.GetOrCreateCollection(Object instance)
   at Microsoft.EntityFrameworkCore.Metadata.Internal.ClrICollectionAccessor`3.Add(Object instance, Object value)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.NavigationFixer.AddToCollection(InternalEntityEntry entry, INavigation navigation, IClrCollectionAccessor collectionAccessor, Object value)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.NavigationFixer.KeyPropertyChanged(InternalEntityEntry entry, IProperty property, IReadOnlyList`1 containingPrincipalKeys, IReadOnlyList`1 containingForeignKeys, Object oldValue, Object newValue)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntryNotifier.KeyPropertyChanged(InternalEntityEntry entry, IProperty property, IReadOnlyList`1 keys, IReadOnlyList`1 foreignKeys, Object oldValue, Object newValue)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.ChangeDetector.DetectKeyChange(InternalEntityEntry entry, IProperty property)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntryNotifier.PropertyChanged(InternalEntityEntry entry, IPropertyBase property, Boolean setModified)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.SetProperty(IPropertyBase propertyBase, Object value, Boolean setModified)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.NavigationFixer.SetForeignKeyProperties(InternalEntityEntry dependentEntry, InternalEntityEntry principalEntry, IForeignKey foreignKey, Boolean setModified)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.NavigationFixer.KeyPropertyChanged(InternalEntityEntry entry, IProperty property, IReadOnlyList`1 containingPrincipalKeys, IReadOnlyList`1 containingForeignKeys, Object oldValue, Object newValue)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntryNotifier.KeyPropertyChanged(InternalEntityEntry entry, IProperty property, IReadOnlyList`1 keys, IReadOnlyList`1 foreignKeys, Object oldValue, Object newValue)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.ChangeDetector.DetectKeyChange(InternalEntityEntry entry, IProperty property)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntryNotifier.PropertyChanged(InternalEntityEntry entry, IPropertyBase property, Boolean setModified)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.SetProperty(IPropertyBase propertyBase, Object value, Boolean setModified)
   at Microsoft.EntityFrameworkCore.Update.ModificationCommand.PropagateResults(ValueBuffer valueBuffer)
   at Microsoft.EntityFrameworkCore.Update.AffectedCountModificationCommandBatch.ConsumeResultSetWithPropagation(Int32 commandIndex, RelationalDataReader reader)
   at Microsoft.EntityFrameworkCore.Update.AffectedCountModificationCommandBatch.Consume(RelationalDataReader reader)
   --- End of inner exception stack trace ---
   at Microsoft.EntityFrameworkCore.Update.AffectedCountModificationCommandBatch.Consume(RelationalDataReader reader)
   at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.Execute(IRelationalConnection connection)
   at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.Execute(Tuple`2 parameters)
   at Microsoft.EntityFrameworkCore.Storage.Internal.SqlServerExecutionStrategy.Execute[TState,TResult](TState state, Func`3 operation, Func`3 verifySucceeded)
   at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.Execute(IEnumerable`1 commandBatches, IRelationalConnection connection)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChanges(IReadOnlyList`1 entriesToSave)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.SaveChanges(Boolean acceptAllChangesOnSuccess)
   at Microsoft.EntityFrameworkCore.DbContext.SaveChanges(Boolean acceptAllChangesOnSuccess)
   at NetCore21.Program.Main(String[] args) in C:\EFTest-master\NetCore21\Program.cs:line 22}

Steps to reproduce

Reproduction projects

Here is a reproduction of the problem in both EF Core 1.1.1 and EF Core 2.0.1, plus a project that successfully saves the same structure in EF 6:

https://github.com/seantarogers/EFTest

Database structure I am trying to write to

databaseschema

Class structure:

1. Session

namespace EFTest
{
    public abstract class Session
    {
        public int Id { get; private set; }
    }
}

2. QuotingSession

using System.Collections.Generic;

namespace EFTest
{
    public class QuotingSession : Session
    {
        public string QuotingName { get; private set; }
        public List<QuotingPolicy> Policies { get; private set; }

        private QuotingSession()
        {
        }

        public QuotingSession(string name, List<QuotingPolicy> quotingPolicies)
        {
            QuotingName = name;
            Policies = quotingPolicies;

        }
   }
}

3. BackOfficeSession

using System.Collections.Generic;

namespace EFTest
{
    public class BackOfficeSession : Session
    {
        public List<BackOfficePolicy> Policies { get; private set; }
        public string BackOfficeName { get; private set; }

        private BackOfficeSession()
        {
        }

        public BackOfficeSession(string name, List<BackOfficePolicy> policies)
        {
            BackOfficeName = name;
            Policies = policies;
        }
    }
}

4. Policy

namespace EFTest
{
    public abstract class Policy
    {
        public int Id { get; set; }
        public int SessionId { get; set; }
    }
}

5. QuotingPolicy

namespace EFTest
{
    public class QuotingPolicy : Policy
    {
        public string QuotingPolicyName { get; private set; }

        private QuotingPolicy()
        {

        }

        public QuotingPolicy(string name)
        {
            QuotingPolicyName = name;
        }
    }
}

6. BackOfficePolicy

namespace EFTest
 {
    public class BackOfficePolicy : Policy
    {
        public string BackOfficePolicyName { get; private set; }

        private BackOfficePolicy()
        {
        }

        public BackOfficePolicy(string name)
        {
           BackOfficePolicyName = name;
        }
    }
}

7. EF DB Context and Fluent Configuration

using Microsoft.EntityFrameworkCore;

namespace EFTest
{
    public class TestDbContext : DbContext
    {
        public TestDbContext(DbContextOptions options)
        : base(options)
        {
        }

        public DbSet<QuotingSession> QuotingSessions { get; set; }
        public DbSet<BackOfficeSession> BackOfficeSessions { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            ConfigureSession(modelBuilder);
            ConfigurePolicy(modelBuilder);
            ConfigureQuotingSession(modelBuilder);
            ConfigureBackOfficeSession(modelBuilder);
            ConfigureBackOfficePolicy(modelBuilder);
            ConfigureQuotingPolicy(modelBuilder);
        }

        public static void ConfigurePolicy(ModelBuilder modelBuilder)
        {
            var entity = modelBuilder.Entity<Policy>();
            entity.ToTable("Policy", "dbo");
            entity.HasKey(x => x.Id);

            entity.HasDiscriminator<int>("SessionType")
                .HasValue<QuotingPolicy>(1)
                .HasValue<BackOfficePolicy>(2);
        }

        public static void ConfigureBackOfficePolicy(ModelBuilder modelBuilder)
        {
            var entity = modelBuilder.Entity<BackOfficePolicy>();
            entity.Property(x => x.BackOfficePolicyName);
        }

        public static void ConfigureQuotingPolicy(ModelBuilder modelBuilder)
        {
            var entity = modelBuilder.Entity<QuotingPolicy>();
            entity.Property(x => x.QuotingPolicyName);
        }

        public static void ConfigureSession(ModelBuilder modelBuilder)
        {
            var entity = modelBuilder.Entity<Session>();
            entity.ToTable("Session", "dbo");
            entity.HasKey(x => x.Id);

            entity.HasDiscriminator<int>("SessionType")
                .HasValue<QuotingSession>(1)
                .HasValue<BackOfficeSession>(2);
        }

        public static void ConfigureBackOfficeSession(ModelBuilder modelBuilder)
        {
            var entity = modelBuilder.Entity<BackOfficeSession>();
            entity.Property(x => x.BackOfficeName);
            entity.HasMany(c => c.Policies).WithOne().HasForeignKey(c => c.SessionId);
       // entity.Ignore(c => c.Policies); uncomment this to see it working
        }

        public static void ConfigureQuotingSession(ModelBuilder modelBuilder)
        {
            var entity = modelBuilder.Entity<QuotingSession>();
            entity.Property(x => x.QuotingName);
            entity.HasMany(c => c.Policies).WithOne().HasForeignKey(c => c.SessionId);
        }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
        }
    }
}

8. To test it

using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;

namespace EFTest
{
    class Program
    {
        static void Main(string[] args)
        {
            var optionsBuilder = new DbContextOptionsBuilder<TestDbContext>();
            const string conn = "Server=.\\SqlServer2014;Database=EFTest;Trusted_Connection=True"    
            optionsBuilder.UseSqlServer(conn);
            using (var dbContext = new TestDbContext(optionsBuilder.Options))
            {
                var quotingPolicy = new QuotingPolicy("quotingPolicyname");
                var quotingSession = new QuotingSession("quotingSessionName", new List<QuotingPolicy> {quotingPolicy});

                dbContext.QuotingSessions.Add(quotingSession);
                dbContext.SaveChanges();  // BLOWS UP HERE!
           }
        }
    }
}

Further technical details

EF Core version: 1.1.1 and 2.0.1 Database Provider: (e.g. Microsoft.EntityFrameworkCore.SqlServer) Operating system: Windows 7 and Windows 10 IDE: Visual Studio 2017 Enterprise 15.3.0

Thanks

ajcvickers commented 6 years ago

Related to #9696

todd-skelton commented 6 years ago

Any work around until its fixed?

todd-skelton commented 6 years ago

I had to move my collection to the base type and expose it in the inherited type.

ajcvickers commented 6 years ago

@AndriySvyryd @bricelam This model now works with the current bits, except that two identical but differently named foreign key constraints are created:

CREATE TABLE [Session] (
    [Id] int NOT NULL IDENTITY,
    [Discriminator] nvarchar(max) NOT NULL,
    [BackOfficeName] nvarchar(max) NULL,
    [QuotingName] nvarchar(max) NULL,
    CONSTRAINT [PK_Session] PRIMARY KEY ([Id])
);

CREATE TABLE [Policy] (
    [Id] int NOT NULL IDENTITY,
    [SessionId] int NOT NULL,
    [Discriminator] nvarchar(max) NOT NULL,
    [BackOfficePolicyName] nvarchar(max) NULL,
    [QuotingPolicyName] nvarchar(max) NULL,
    CONSTRAINT [PK_Policy] PRIMARY KEY ([Id]),
    CONSTRAINT [FK_Policy_Session_SessionId] FOREIGN KEY ([SessionId]) REFERENCES [Session] ([Id]) ON DELETE NO ACTION,
    CONSTRAINT [FK_Policy_Session_SessionId1] FOREIGN KEY ([SessionId]) REFERENCES [Session] ([Id]) ON DELETE NO ACTION
);

Is there some reason that the constraint isn't collapsed into one in this case? (It creates issues having two constraints when cascade delete is setup on both.)

ajcvickers commented 6 years ago

Putting this on the backlog to consolidate the two constraints together, if it can be done safely. For now, the workaround is to explicitly give both constraints the same name. For example:

public static void ConfigureBackOfficeSession(ModelBuilder modelBuilder)
        {
            var entity = modelBuilder.Entity<BackOfficeSession>();
            entity.Property(x => x.BackOfficeName);
            entity.HasMany(c => c.Policies).WithOne().HasForeignKey(c => c.SessionId).HasConstraintName("MyConstraint");
        }

        public static void ConfigureQuotingSession(ModelBuilder modelBuilder)
        {
            var entity = modelBuilder.Entity<QuotingSession>();
            entity.Property(x => x.QuotingName);
            entity.HasMany(c => c.Policies).WithOne().HasForeignKey(c => c.SessionId).HasConstraintName("MyConstraint");
        }
mc0re commented 5 years ago

Is the issue still not fixed? I'm also trying to use TPH pointing to another TPH, fails with the same strange exception.

ajcvickers commented 4 years ago

Closing in favor of #12963