efcore: Metadata: InverseProperty fails to resolve ambiguity while use KeyAttribute on PK

I am opening a issue regarding the [Foreignkey] attribute This was using the Pomelo.MySQL driver, but it might also affect other drivers.

(origional issue) https://github.com/PomeloFoundation/Pomelo.EntityFrameworkCore.MySql/issues/324

Note (as described below) that it worked perfectly fine in EFCore 1.0 and 1.1 and broke in 2.0

Steps to reproduce

  1. Create a test application with the model below 1 targeting NetCoreApp 1.1 1 targeting NetCoreApp 2.0
  2. Run “dotnet restore”
  3. Run “dotnet build”
  4. Run “dotnet ef migrations add initial”

Model used

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

namespace EFModelTest
{
    class Program
    {
        static void Main(string[] args) { }
    }

    public class User
    {
        [Key]
        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public int UserId { get; set; }

        [MaxLength(64)]
        public string Name { get; set; }

        [InverseProperty(nameof(Relation.AccountManager))]
        public virtual ICollection<Relation> AccountManagerRelations { get; set; }

        [InverseProperty(nameof(Relation.SalesManager))]
        public virtual ICollection<Relation> SalesManagerRelations { get; set; }
    }

    public class Relation
    {
        [Key]
        [DatabaseGenerated(DatabaseGeneratedOption.None)]
        public string Id { get; set; }

        [MaxLength(64)]
        public string Name { get; set; }

        public int? AccountManagerId { get; set; }

        [ForeignKey(nameof(AccountManagerId))]
        public virtual User AccountManager { get; set; }

        public int? SalesManagerId { get; set; }

        [ForeignKey(nameof(SalesManagerId))]
        public virtual User SalesManager { get; set; }
    }

    public class MyContext : DbContext
    {
        public DbSet<User> Users { get; set; }
		
        public DbSet<Relation> Relations { get; set; }
		
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
            => optionsBuilder.UseMySql(@"User Id=root;Password=root;Host=localhost;Database=eftest;Persist Security Info=True;Convert Zero Datetime=True;");
    }
}

The issue

Using data attributes to add a foreign key fails in EFCore 2.0 when the classname is not equal to a property name.

For instance: Mapping a UserId as foreign key to a class called User using a property User => works fine Mapping a ManagerId as foreign key to a class called User using a property called Manager => Broken

Stacktrace EF MySQL 1.1

PS D:\Sourcecodes\EF Core MySQL\NET1.1> dotnet ef migrations add initial

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed 00:00:01.39
Done. To undo this action, use 'ef migrations remove'

Migration model

.....
   migrationBuilder.CreateTable(
                name: "Relations",
                columns: table => new
                {
                    Id = table.Column<string>(nullable: false),
                    AccountManagerId = table.Column<int>(nullable: true),
                    Name = table.Column<string>(maxLength: 64, nullable: true),
                    SalesManagerId = table.Column<int>(nullable: true)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_Relations", x => x.Id);
                    table.ForeignKey(
                        name: "FK_Relations_Users_AccountManagerId",
                        column: x => x.AccountManagerId,
                        principalTable: "Users",
                        principalColumn: "UserId",
                        onDelete: ReferentialAction.Restrict);
                    table.ForeignKey(
                        name: "FK_Relations_Users_SalesManagerId",
                        column: x => x.SalesManagerId,
                        principalTable: "Users",
                        principalColumn: "UserId",
                        onDelete: ReferentialAction.Restrict);
                });

            migrationBuilder.CreateIndex(
                name: "IX_Relations_AccountManagerId",
                table: "Relations",
                column: "AccountManagerId");

            migrationBuilder.CreateIndex(
                name: "IX_Relations_SalesManagerId",
                table: "Relations",
                column: "SalesManagerId");

Stacktrace EF MySQL 2.0

PS D:\Sourcecodes\Model Scaffolding Error\NET2.0> dotnet ef migrations add initial
System.InvalidOperationException: Unable to determine the relationship represented by navigation property 'Relation.AccountManager' of type 'User'. Either manually configure the relationship, or ignore this property using the '[NotMapped]' attribute or by using 'EntityTypeBuilder.Ignore' in 'OnModelCreating'.
   at Microsoft.EntityFrameworkCore.Metadata.Conventions.Internal.PropertyMappingValidationConvention.Apply(InternalModelBuilder modelBuilder)
   at Microsoft.EntityFrameworkCore.Metadata.Conventions.Internal.ConventionDispatcher.ImmediateConventionScope.OnModelBuilt(InternalModelBuilder modelBuilder)
   at Microsoft.EntityFrameworkCore.Metadata.Conventions.Internal.ConventionDispatcher.OnModelBuilt(InternalModelBuilder modelBuilder)
   at Microsoft.EntityFrameworkCore.Metadata.Internal.Model.Validate()
   at Microsoft.EntityFrameworkCore.Metadata.Internal.InternalModelBuilder.Validate()
   at Microsoft.EntityFrameworkCore.Infrastructure.ModelSource.CreateModel(DbContext context, IConventionSetBuilder conventionSetBuilder, IModelValidator validator)
   at Microsoft.EntityFrameworkCore.Infrastructure.ModelSource.<>c__DisplayClass5_0.<GetModel>b__0(Object k)
   at System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd(TKey key, Func`2 valueFactory)
   at Microsoft.EntityFrameworkCore.Infrastructure.ModelSource.GetModel(DbContext context, IConventionSetBuilder conventionSetBuilder, IModelValidator validator)
   at Microsoft.EntityFrameworkCore.Internal.DbContextServices.CreateModel()
   at Microsoft.EntityFrameworkCore.Internal.DbContextServices.get_Model()
   at Microsoft.EntityFrameworkCore.Infrastructure.EntityFrameworkServicesBuilder.<>c.<TryAddCoreServices>b__7_1(IServiceProvider p)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitFactoryService(FactoryService factoryService, ServiceProvider provider)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(IServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScoped(ScopedCallSite scopedCallSite, ServiceProvider provider)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(IServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, ServiceProvider provider)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(IServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScoped(ScopedCallSite scopedCallSite, ServiceProvider provider)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(IServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, ServiceProvider provider)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(IServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScoped(ScopedCallSite scopedCallSite, ServiceProvider provider)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(IServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, ServiceProvider provider)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(IServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScoped(ScopedCallSite scopedCallSite, ServiceProvider provider)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(IServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, ServiceProvider provider)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(IServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScoped(ScopedCallSite scopedCallSite, ServiceProvider provider)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(IServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, ServiceProvider provider)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(IServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScoped(ScopedCallSite scopedCallSite, ServiceProvider provider)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(IServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceProvider.<>c__DisplayClass17_0.<RealizeService>b__0(ServiceProvider provider)
   at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(Type serviceType)
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetService[T](IServiceProvider provider)
   at Microsoft.EntityFrameworkCore.Design.Internal.DesignTimeServicesBuilder.<>c__DisplayClass6_0.<ConfigureContextServices>b__7(IServiceProvider _)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitFactoryService(FactoryService factoryService, ServiceProvider provider)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(IServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitTransient(TransientCallSite transientCallSite, ServiceProvider provider)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(IServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceProvider.<>c__DisplayClass17_0.<RealizeService>b__0(ServiceProvider provider)
   at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(Type serviceType)
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetService[T](IServiceProvider provider)
   at Microsoft.EntityFrameworkCore.Design.Internal.MigrationsOperations.EnsureServices(IServiceProvider services)
   at Microsoft.EntityFrameworkCore.Design.Internal.MigrationsOperations.AddMigration(String name, String outputDir, String contextType)
   at Microsoft.EntityFrameworkCore.Design.OperationExecutor.AddMigrationImpl(String name, String outputDir, String contextType)
   at Microsoft.EntityFrameworkCore.Design.OperationExecutor.AddMigration.<>c__DisplayClass0_1.<.ctor>b__0()
   at Microsoft.EntityFrameworkCore.Design.OperationExecutor.OperationBase.<>c__DisplayClass3_0`1.<Execute>b__0()
   at Microsoft.EntityFrameworkCore.Design.OperationExecutor.OperationBase.Execute(Action action)
Unable to determine the relationship represented by navigation property 'Relation.AccountManager' of type 'User'. Either manually configure the relationship, or ignore this property using the '[NotMapped]' attribute or by using 'EntityTypeBuilder.Ignore' in 'OnModelCreating'.

Workaround

Using the fluent api to add the constraint fixes the issue, its purely data attribute constraints that can’t be resolved.

Adding the below modelbuilder to the MyContext fixes the issue in EF Core 2.0 This was not required in 1.1

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
	base.OnModelCreating(modelBuilder);
	modelBuilder.Entity<Relation>(e =>
	{
	   e.HasOne(r => r.AccountManager).WithMany(u => u.AccountManagerRelations).HasForeignKey(r => r.AccountManagerId);
	   e.HasOne(r => r.SalesManager).WithMany(u => u.SalesManagerRelations).HasForeignKey(r => r.SalesManagerId);
	});
}

Output with workaround

PS D:\Sourcecodes\Model Scaffolding Error\NET2.0> dotnet ef migrations add initial
Done. To undo this action, use 'ef migrations remove'

Scaffolds just fine

Further technical details

MySQL version: 5.7.14 Operating system: Windows Server 2012 Pomelo.EntityFrameworkCore.MySql version: 1.1.2 & 2.0.0-preview2-10046

Other details about my project setup: Visual Studio 2017 Preview 4

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Reactions: 3
  • Comments: 20 (9 by maintainers)

Commits related to this issue

Most upvoted comments

In case anyone needs it, here is a quick fix that works for me (not thoroughly tested for all scenarios, and by no means meant to be efficient for long term):

/// <summary>
/// Temporarily fixes a bug with InverseProperty attribute being ignored: https://github.com/aspnet/EntityFrameworkCore/issues/9180
/// </summary>
public static void InversePropertyPatchEF20<TContext>(ModelBuilder modelBuilder) where TContext : DbContext
{
   // (need a patch to fix the InverseProperty issue in EF 2.0)

   // ... get all 'DbSet<>' properties from the given context and extract the entity types ...

   var modelTypes = (from t in typeof(TContext).GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy)
                       where t.PropertyType.IsGenericType && t.PropertyType.GetGenericTypeDefinition() == typeof(DbSet<>)
                       select t.PropertyType.GenericTypeArguments[0]);

   foreach (var modelType in modelTypes)
   {
       // ... find all properties with InversePropertyAttribute and handle it ...

       var ipProperties = (from p in modelType.GetProperties()
                           where p.GetCustomAttribute<InversePropertyAttribute>() != null && p.PropertyType.IsGenericType && p.PropertyType.GetGenericTypeDefinition() == typeof(ICollection<>)
                           select p);

       foreach (var prop in ipProperties)
       {
           var otherModelType = prop.PropertyType.GenericTypeArguments[0]; // (gets the ICollection<T> generic parameter type (T), which points to the entity model on the other end)
           var ipAttr = prop.GetCustomAttribute<InversePropertyAttribute>();
           modelBuilder.Entity(modelType).HasMany(otherModelType, prop.Name).WithOne(ipAttr.Property);
       }
   }
}

Requirements: Make sure you have the [InverseProperty(...)] attrbiute on your ICollection<T> navigational properties where needed (do not have this attribute both ends). Why on the collection? Because it’s easier to filter by looking for the generic collection ICollection<T> and hard code Many-To-One instead of trying to detect the reverse as well. When this is no longer needed, removing this method should be all you need to do and it will still work as it should.

Usage example:

public partial class MyDBContextType // : DbContext
{
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        ... many-to-many fluent mappings for "map" types ...
        InversePropertyPatchEF20<MyDBContextType>(modelBuilder);
        base.OnModelCreating(modelBuilder);
    }
...

I have 75 tables so far (many more coming) in a complex weave of many-to-many mappings, and it works like a charm; now I can continue. 8)

Did anyone also have a problem with EF Core not being able to distinguish between the two foreign keys when persisting the data in the database?

I have a similar scenario with two one-to-many relations between the same entities in my model but when I save my changes to the DB, the same foreing key is used in both columns. I am using SQL server.

As shown here: relations

I have created this simple project to show how to reproduce the issue.

public class User {

	[Key]
	public int Id { get; set; }

	[InverseProperty(nameof(UserRelation.Relation1))]
	public Collection<UserRelation> Relations1 { get; set; }

	[InverseProperty(nameof(UserRelation.Relation2))]
	public Collection<UserRelation> Relations2 { get; set; }


}

public class UserRelation {

	[Key]
	public int Id { get; set; }

	public int Relation1Id { get; set; }

	[ForeignKey(nameof(Relation1Id))]
	public User Relation1 { get; set; }

	public int Relation2Id{ get; set; }

	[ForeignKey(nameof(Relation2Id))]
	public User Relation2 { get; set; }

}

On my DbContext I also have:

protected override void OnModelCreating(ModelBuilder modelBuilder) {
	base.OnModelCreating(modelBuilder);
	modelBuilder.Entity<UserRelation>(e => {
		e.HasOne(r => r.Relation1).WithMany(u => u.Relations1).HasForeignKey(r => r.Relation1Id).OnDelete(DeleteBehavior.ClientSetNull);
		e.HasOne(r => r.Relation2).WithMany(u => u.Relations2).HasForeignKey(r => r.Relation2Id).OnDelete(DeleteBehavior.ClientSetNull);
	});
}

You can find the whole demo project attached. I’m using Visual Studio 2017 Preview 2 TestCoreApp.zip

Are there any workarounds available is this an known issue? It seems to be connected to this one, but not exactly the same problem.

@yanchenw These log messages do not indicate errors; they are messages out of the model building conventions mechanisms describing what was discovered about the model as it is being processed by the conventions. The log level for these messages will be bumped down to “debug” as part of #10966.

@brunopessanha - I downloaded the zip file and examined the code. Following code

user1.Relations1 = new System.Collections.ObjectModel.Collection<UserRelation>();
user1.Relations1.Add(new UserRelation() { Relation1Id = user2.Id, Relation2Id = user1.Id });
user1.Relations1.Add(new UserRelation() { Relation1Id = user3.Id, Relation2Id = user1.Id });
user1.Relations1.Add(new UserRelation() { Relation1Id = user4.Id, Relation2Id = user1.Id });

Even though you are setting Relation1Id to different values you are still adding all those UserRelations to user1.Relations1 collection. Once you add UserRelation to user1.Relation1 collection, navigation fixup will kick in and it will set Relation1Id (FK associated with navigation) to PK of user1. Hence for all the UserRelations the Relation1Id has value of user1.Id

Sorry, .ToTable(Type) was my own custom extension method, forgot about that. I had a custom convention to deal with table names. Also, yes, all my entities have the table attribute, it was a quick and dirty fix for me.

The code is now updated to work properly, and no longer requires the Table attribute. It checks the DbSet<> properties on the context instead in order to get the entity model types. You probably don’t need ToTable() either since the EF should figure this out implicitly, so I removed it.

@ajcvickers No, the fix would have a non-trivial risk of causing other regressions.