Unable to determine the relationship represented by navigation * of type *. Either manually configure the relationship, or ignore this property using

Viewed 1798

I have .NET 5.0.0 console app that uses Microsoft.EntityFrameworkCore.Sqlite 5.0.0. I've installed Microsoft.EntityFrameworkCore.Sqlite 5.0.0, Microsoft.AspNetCore.Identity.EntityFrameworkCore 5.0.0 (for IdentityUser<>) and Microsoft.EntityFrameworkCore.Design 5.0.0 (for dotnet-ef migrations command) nuget packages.

My model represents a set of tests where users select other users, in some other test users do the same and leave some comment (model is greatly simplified for test purpose). So my TestAnswer classes uses three-level inheritance:

public class TestAnswerBase
{
   public Int64 Id { get; set; }
   [Required]
   public Int64 UserId { get; set; }
   public virtual User User { get; set; }
}


public partial class TestAnswerChild : TestAnswerBase
{
   [Required]
   public Int64 ChosenId { get; set; }
   public virtual User Chosen { get; set; }
}

//If I use this TestAnswerGrandChild variant - it shows an error while creating a migration:
//"Unable to determine the relationship represented by navigation 'TestAnswerChild.Chosen' 
//of type 'User'. Either manually configure the relationship, or ignore this property using 
//the '[NotMapped]' attribute or by using 'EntityTypeBuilder.Ignore' in 'OnModelCreating'."
public partial class TestAnswerGrandChild : TestAnswerChild
{
   public string Comment { get; set; }
}    

My User class (it has collection of tests where user selected somebody AND collection where user is chosen by somebody):

public partial class User : IdentityUser<Int64>
{
    public User()
    {
        TestAnswerChildChosen = new HashSet<TestAnswerChild>();
        TestAnswerChildUser = new HashSet<TestAnswerChild>();
        TestAnswerGrandChildChosen = new HashSet<TestAnswerGrandChild>();
        TestAnswerGrandChildUser = new HashSet<TestAnswerGrandChild>();
        
    }

    [Required]
    public string Name { get; set; }

    [InverseProperty(nameof(TestAnswerChild.Chosen))]
    public virtual ICollection<TestAnswerChild> TestAnswerChildChosen { get; set; }

    [InverseProperty(nameof(TestAnswerChild.User))]
    public virtual ICollection<TestAnswerChild> TestAnswerChildUser { get; set; }

    [InverseProperty(nameof(TestAnswerGrandChild.Chosen))]
    public virtual ICollection<TestAnswerGrandChild> TestAnswerGrandChildChosen { get; set; }

    [InverseProperty(nameof(TestAnswerGrandChild.User))]
    public virtual ICollection<TestAnswerGrandChild> TestAnswerGrandChildUser { get; set; }
}

My DbContext is rather simple but I'm including it's code here:

public partial class MyDbContext : IdentityDbContext<User, IdentityRole<Int64>, Int64>
{
    public MyDbContext()
    {
    }

    public MyDbContext(DbContextOptions<MyDbContext> options)
        : base(options)
    {
    }
   
    public virtual DbSet<TestAnswerChild> TestAnswersChild { get; set; }
    public virtual DbSet<TestAnswerGrandChild> TestAnswersGrandChild { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        if (!optionsBuilder.IsConfigured)
        {
            optionsBuilder.UseSqlite("data source=test.db");
        }
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        //is necessary because -> "The entity type 'IdentityUserLogin<long>' requires a primary key to be defined"
        base.OnModelCreating(modelBuilder);
    }
}

So when I call dotnet-ef migrations add InitialCreate command fo example I get an error:

Unable to determine the relationship represented by navigation 'TestAnswerChild.Chosen' of type 'User'. Either manually configure the relationship, or ignore this property using the '[NotMapped]' attribute or by using 'EntityTypeBuilder.Ignore' in 'OnModelCreating'.

If I change TestAnswerGrandChild code like that (only two-level inheritance) - it's creates a migration with no errors:

//If I use that TestAnswerGrandChild variant - migration is created with no errors
    
public partial class TestAnswerGrandChild : TestAnswerBase
{
   [Required]
   public Int64 ChosenId { get; set; }
   public virtual User Chosen { get; set; }
   public string Comment { get; set; }
}

So what is my problem and how to solve it (I need to have multiple level inheritance - that test project is greatly simplified)?

I've uploaded my test project here (run update migrations.cmd to get an error)

Thank you in advance.

1 Answers

So what is my problem and how to solve it

The problem is caused by a combination of several factors and some current EF Core shortcomings and implementation specifics.

The main problem is that by default when some base class is used as entity, EF Core assumes it should be mapped to the database using one of the available database inheritance strategies (currently TPH and TPT). However you don't want that as I understand - you are using object hierarchy just for code reusability, and eventually some polymorphic/generic type processing, but you want them to be completely mapped to a separate tables.

Part of the problem is that there is no data annotation for that. You have to specify that fluently:

modelBuilder.Entity<TestAnswerGrandChild>()
    .HasBaseType((Type)null);

However, the conventions and data annotations are applied before OnModelCreating. And since the defined relationships via navigation properties and annotations in this case are not valid for any of the existing database inheritance, EF Core simply ignores them and generates exception asking you to do that manually.

And that's what you should do unfortunately - additionally to the above, and after it, manually specifying and pairing the navigation properties, thus fully redefining the relationships:

modelBuilder.Entity<TestAnswerGrandChild>()
    .HasBaseType((Type)null);

modelBuilder.Entity<TestAnswerChild>()
    .HasOne(e => e.Chosen)
    .WithMany(e => e.TestAnswerChildChosen);

modelBuilder.Entity<TestAnswerChild>()
    .HasOne(e => e.User)
    .WithMany(e => e.TestAnswerChildUser);

modelBuilder.Entity<TestAnswerGrandChild>()
    .HasOne(e => e.Chosen)
    .WithMany(e => e.TestAnswerGrandChildChosen);

modelBuilder.Entity<TestAnswerGrandChild>()
    .HasOne(e => e.User)
    .WithMany(e => e.TestAnswerGrandChildUser);
Related