Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Child inheritance generating InvalidCastException #10446

Closed
seantarogers opened this issue Nov 30, 2017 · 7 comments
Closed

Child inheritance generating InvalidCastException #10446

seantarogers opened this issue Nov 30, 2017 · 7 comments

Comments

@seantarogers
Copy link

seantarogers commented Nov 30, 2017

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 ajcvickers self-assigned this Dec 1, 2017
@ajcvickers ajcvickers added this to the 2.1.0 milestone Dec 1, 2017
@ajcvickers
Copy link
Contributor

Related to #9696

@ajcvickers ajcvickers modified the milestones: 2.1.0-preview1, 2.1.0 Jan 17, 2018
@divega divega modified the milestones: 2.1.0-preview2, 2.1.0 Apr 2, 2018
@todd-skelton
Copy link

Any work around until its fixed?

@todd-skelton
Copy link

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

@ajcvickers
Copy link
Contributor

@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 ajcvickers removed this from the 2.1.0 milestone Apr 9, 2018
@ajcvickers
Copy link
Contributor

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");
        }

@ajcvickers ajcvickers added this to the Backlog milestone Apr 9, 2018
@ajcvickers ajcvickers removed their assignment Apr 9, 2018
@mc0re
Copy link

mc0re commented Mar 26, 2019

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

@ajcvickers
Copy link
Contributor

Closing in favor of #12963

@ajcvickers ajcvickers removed this from the Backlog milestone Nov 20, 2019
@ajcvickers ajcvickers reopened this Oct 16, 2022
@ajcvickers ajcvickers closed this as not planned Won't fix, can't repro, duplicate, stale Oct 16, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants