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

Multiple TPH entities with same dependant #20719

Closed
untaught opened this issue Apr 22, 2020 · 3 comments · Fixed by #21834
Closed

Multiple TPH entities with same dependant #20719

untaught opened this issue Apr 22, 2020 · 3 comments · Fixed by #21834
Labels
area-save-changes closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. customer-reported type-bug
Milestone

Comments

@untaught
Copy link

untaught commented Apr 22, 2020

In the following example EF throws InvalidCastException:

public abstract class Person
{
    [Key]
    public int Id { get; set; }
    public int TypeId { get; set; }
    ....
}

public class Student : Person
{
    public ICollection<Address> Addresses { get; set; } 
    ...
}

public class Teacher : Person
{
    public ICollection<Address> Addresses { get; set; }
    public ICollection<Room> Rooms { get; set; }
    ....
}

public class Address
{
    [Key]
    public int Id { get; set; }
    public int PersonId { get; set; }
    public string Street { get; set; }

    public Student Student { get; set; }
    public Teacher Teacher { get; set; }
    ....
}

Both Student and Teacher types have discriminator TypeId and both are mapped to Person table.
Address is a table with relation that one person can have multiple addresses. The address entity can have only Student or Teacher property set depending on its type in Persons table.
I have tried to create the relationship with fluent api and attributes but EF throws the following exception on save in Teacher or Student DbSet:

Microsoft.EntityFrameworkCore.DbUpdateException: 'An error occurred while updating the entries. See the inner exception for details.'

Inner exception:
InvalidCastException: Unable to cast object of type Teacher to type Student.

I have person types which does not have addresses and the Teacher and some other types have Room collection so it's not a good idea to set collection of adresses or rooms to Person entity.

EF core version is 3.1.3

@ajcvickers
Copy link
Member

@untaught I have not been able to reproduce this--see my code below. Please attach a small, runnable project or post a small, runnable code listing like below that reproduces what you are seeing so that we can investigate.

public abstract class Person
{
    [Key]
    public int Id { get; set; }
    public int TypeId { get; set; }
}

public class Student : Person
{
    public ICollection<Address> Addresses { get; set; } 
}

public class Teacher : Person
{
    public ICollection<Address> Addresses { get; set; }
    public ICollection<Room> Rooms { get; set; }
}

public class Address
{
    [Key]
    public int Id { get; set; }
    public int PersonId { get; set; }
    public string Street { get; set; }

    public Student Student { get; set; }
    public Teacher Teacher { get; set; }
}

public class Room
{
    public int Id { get; set; }
}

public static class Program
{
    public static void Main()
    {
        using (var context = new SomeDbContext())
        {
            context.Database.EnsureDeleted();
            context.Database.EnsureCreated();

            context.AddRange(
                new Student
            {
                Addresses = new List<Address>
                {
                    new Address(),
                    new Address()
                }
            },
                new Teacher
                {
                    Addresses = new List<Address>
                    {
                        new Address(),
                        new Address()
                    }
                });
            
            context.SaveChanges();
        }

        using (var context = new SomeDbContext())
        {
            var teachers = context.Set<Teacher>().Include(e => e.Addresses).ToList();
            var students = context.Set<Student>().Include(e => e.Addresses).ToList();
        }
    }
}

public class SomeDbContext : DbContext
{
    private static readonly ILoggerFactory
        Logger = LoggerFactory.Create(x => x.AddConsole()); //.SetMinimumLevel(LogLevel.Debug));

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Person>()
            .HasDiscriminator(e => e.TypeId)
            .HasValue<Person>(1)
            .HasValue<Teacher>(2)
            .HasValue<Student>(3);
        
        modelBuilder.Entity<Student>().HasMany(e => e.Addresses).WithOne(e => e.Student);
        modelBuilder.Entity<Teacher>().HasMany(e => e.Addresses).WithOne(e => e.Teacher);
        modelBuilder.Entity<Teacher>().HasMany(e => e.Rooms).WithOne();
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder
            .UseLoggerFactory(Logger)
            .EnableSensitiveDataLogging()
            .UseSqlServer(Your.SqlServerConnectionString);
}

@untaught
Copy link
Author

Here is an example of the exception. There we have TPH, so PersonId in the Address table should be the FK to the Person table. In your example EF creates two FKs in the Address table - one for Student and one for Teacher. This must not be the case because TPH is referencing same table for multiple types. It should be possible to define only one FK to that table. This way it reflects the structure in the DB.

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using Microsoft.EntityFrameworkCore;

public abstract class Person
{
    [Key]
    public int Id { get; set; }
    public int TypeId { get; set; }
}

public class Student : Person
{
    public ICollection<Address> Addresses { get; set; }
}

public class Teacher : Person
{
    public ICollection<Address> Addresses { get; set; }
}

public class Address
{
    [Key]
    public int Id { get; set; }
    public int PersonId { get; set; }
    public string Street { get; set; }

    [ForeignKey(nameof(PersonId))]
    public Student Student { get; set; }

    [ForeignKey(nameof(PersonId))]
    public Teacher Teacher { get; set; }
}

public static class Program
{
    public static void Main()
    {
        using (var context = new SomeDbContext())
        {
            context.AddRange(
                new Student
                {
                    Addresses = new List<Address>
                {
                    new Address(),
                    new Address()
                }
                },
                new Teacher
                {
                    Addresses = new List<Address>
                    {
                        new Address(),
                        new Address()
                    }
                });

            context.SaveChanges();
        }

        using (var context = new SomeDbContext())
        {
            var teachers = context.Set<Teacher>().Include(e => e.Addresses).ToList();
            var students = context.Set<Student>().Include(e => e.Addresses).ToList();
        }
    }
}

public class SomeDbContext : DbContext
{
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Person>()
            .HasDiscriminator(e => e.TypeId)
            .HasValue<Teacher>(1)
            .HasValue<Student>(2);

        modelBuilder.Entity<Student>().HasMany(e => e.Addresses).WithOne(e => e.Student).IsRequired(false);
        modelBuilder.Entity<Teacher>().HasMany(e => e.Addresses).WithOne(e => e.Teacher).IsRequired(false);
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder
            .EnableSensitiveDataLogging()
            .UseSqlServer(ConnectionString);
}

This is the DB:
image

@ajcvickers
Copy link
Member

@untaught Thanks--I am able to reproduce the exception now.

Team/@AndriySvyryd: I was able to get the model built correctly this:

modelBuilder
    .Entity<Student>()
    .HasMany(e => e.Addresses)
    .WithOne(e => e.Student)
    .HasForeignKey(e => e.PersonId)
    .HasConstraintName("[FK_Address_Person_PersonId]");
modelBuilder
    .Entity<Teacher>()
    .HasMany(e => e.Addresses)
    .WithOne(e => e.Teacher)
    .HasForeignKey(e => e.PersonId)
    .HasConstraintName("[FK_Address_Person_PersonId]");

(Using HasConstraintName due to #12963)

Which gives me:

Model: 
  EntityType: Address
    Properties: 
      Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
      PersonId (int) Required FK Index
      Street (string)
    Navigations: 
      Student (Student) ToPrincipal Student Inverse: Addresses
      Teacher (Teacher) ToPrincipal Teacher Inverse: Addresses
    Keys: 
      Id PK
    Foreign keys: 
      Address {'PersonId'} -> Student {'Id'} ToDependent: Addresses ToPrincipal: Student Cascade
      Address {'PersonId'} -> Teacher {'Id'} ToDependent: Addresses ToPrincipal: Teacher Cascade
    Indexes: 
      PersonId
  EntityType: Person Abstract
    Properties: 
      Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
      TypeId (int) Required
    Keys: 
      Id PK
  EntityType: Room
    Properties: 
      Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
      TeacherId (no field, Nullable<int>) Shadow FK Index
    Keys: 
      Id PK
    Foreign keys: 
      Room {'TeacherId'} -> Teacher {'Id'} ToDependent: Rooms ClientSetNull
    Indexes: 
      TeacherId
  EntityType: Student Base: Person
    Navigations: 
      Addresses (ICollection<Address>) Collection ToDependent Address Inverse: Student
  EntityType: Teacher Base: Person
    Navigations: 
      Addresses (ICollection<Address>) Collection ToDependent Address Inverse: Teacher
      Rooms (ICollection<Room>) Collection ToDependent Room

SaveChanges then executes this successfully:

info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (14ms) [Parameters=[@p0='3'], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      INSERT INTO [Person] ([TypeId])
      VALUES (@p0);
      SELECT [Id]
      FROM [Person]
      WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();

and throws the exception reported above.

@ajcvickers ajcvickers added this to the 5.0.0 milestone May 1, 2020
@AndriySvyryd AndriySvyryd added the closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. label Jul 29, 2020
@AndriySvyryd AndriySvyryd removed their assignment Jul 29, 2020
@ghost ghost closed this as completed in #21834 Jul 29, 2020
@ajcvickers ajcvickers modified the milestones: 5.0.0, 5.0.0-rc1 Aug 14, 2020
@ajcvickers ajcvickers modified the milestones: 5.0.0-rc1, 5.0.0 Nov 7, 2020
This issue was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-save-changes closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. customer-reported type-bug
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants