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

Having navigations for base type and sub type in the same entity result in "Cannot create a relationship ... because a relationship already exists..." exception #32507

Closed
vlardn opened this issue Dec 4, 2023 · 8 comments

Comments

@vlardn
Copy link

vlardn commented Dec 4, 2023

Given this model:

public abstract class Animal
{
    public int Id { get; set; }
    public int ZooId { get; set; }
    public Zoo Zoo { get; set; }
}

public class Tiger : Animal
{
}

This works:

public class Zoo
{
    public int Id { get; set; }
    public IEnumerable<Animal> Animals { get; set; }
}

This works:

public class Zoo
{
    public int Id { get; set; }
    public IEnumerable<Tiger> Tigers { get; set; }
}

This does NOT work:

public class Zoo
{
    public int Id { get; set; }
    public IEnumerable<Animal> Animals { get; set; }
    public IEnumerable<Tiger> Tigers { get; set; }
}

The simple console test app:

using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;

using var sqliteConnection = new SqliteConnection("Data Source=:memory:");
sqliteConnection.Open();

var dbContextOptions = new DbContextOptionsBuilder<TestContext>()
	.UseSqlite(sqliteConnection)
	.EnableDetailedErrors()
	.EnableSensitiveDataLogging()
	.LogTo(Console.WriteLine)
	.Options;

using var context = new TestContext(dbContextOptions);

try
{
	context.Database.EnsureCreated();
}
catch (Exception ex)
{
	Console.WriteLine(ex);
}

Console.ReadKey();

public class TestContext : DbContext
{
	public TestContext(DbContextOptions options) : base(options) { }

	protected override void OnModelCreating(ModelBuilder modelBuilder)
	{
		base.OnModelCreating(modelBuilder);

		// Explicit FK configuration recommended in 
		// https://github.com/dotnet/efcore/issues/26961#issuecomment-992343923
		modelBuilder.Entity<Zoo>(b =>
		{
			b.HasMany(e => e.Animals).WithOne(e => e.Zoo).HasForeignKey(e => e.ZooId);
			b.HasMany(e => e.Tigers).WithOne(e => e.Zoo).HasForeignKey(e => e.ZooId);
		});

		modelBuilder.Entity<Animal>();
		modelBuilder.Entity<Tiger>();
	}
}

public class Zoo
{
	public int Id { get; set; }
	public IEnumerable<Animal> Animals { get; set; }
	public IEnumerable<Tiger> Tigers { get; set; }
}

public abstract class Animal
{
	public int Id { get; set; }
	public int ZooId { get; set; }
	public Zoo Zoo { get; set; }
}

public class Tiger : Animal
{
}

gives an exception:

System.InvalidOperationException: Cannot create a relationship between 'Zoo.Tigers' and 'Tiger.Zoo' because a relationship already exists between 'Zoo.Animals' and 'Animal.Zoo'. Navigations can only participate in a single relationship. If you want to override an existing relationship call 'Ignore' on the navigation 'Tiger.Zoo' first in 'OnModelCreating'.
   at Microsoft.EntityFrameworkCore.Metadata.Internal.InternalForeignKeyBuilder.ThrowForConflictingNavigation(IReadOnlyForeignKey foreignKey, IReadOnlyEntityType principalEntityType, IReadOnlyEntityType dependentEntityType, String navigationToDependent, String navigationToPrincipal)
   at Microsoft.EntityFrameworkCore.Metadata.Internal.InternalForeignKeyBuilder.GetOrCreateRelationshipBuilder(EntityType principalEntityType, EntityType dependentEntityType, Nullable`1 navigationToPrincipal, Nullable`1 navigationToDependent, IReadOnlyList`1 dependentProperties, IReadOnlyList`1 oldNameDependentProperties, IReadOnlyList`1 principalProperties, Nullable`1 isRequired, Boolean removeCurrent, Nullable`1 principalEndConfigurationSource, Nullable`1 configurationSource, Nullable`1& existingRelationshipInverted)
   at Microsoft.EntityFrameworkCore.Metadata.Internal.InternalForeignKeyBuilder.ReplaceForeignKey(InternalEntityTypeBuilder principalEntityTypeBuilder, InternalEntityTypeBuilder dependentEntityTypeBuilder, Nullable`1 navigationToPrincipal, Nullable`1 navigationToDependent, IReadOnlyList`1 dependentProperties, IReadOnlyList`1 oldNameDependentProperties, IReadOnlyList`1 principalProperties, Nullable`1 isUnique, Nullable`1 isRequired, Nullable`1 isRequiredDependent, Nullable`1 isOwnership, Nullable`1 deleteBehavior, Boolean removeCurrent, Boolean oldRelationshipInverted, Nullable`1 principalEndConfigurationSource, ConfigurationSource configurationSource)
   at Microsoft.EntityFrameworkCore.Metadata.Internal.InternalForeignKeyBuilder.ReplaceForeignKey(ConfigurationSource configurationSource, InternalEntityTypeBuilder principalEntityTypeBuilder, InternalEntityTypeBuilder dependentEntityTypeBuilder, Nullable`1 navigationToPrincipal, Nullable`1 navigationToDependent, IReadOnlyList`1 dependentProperties, IReadOnlyList`1 oldNameDependentProperties, IReadOnlyList`1 principalProperties, Nullable`1 isUnique, Nullable`1 isRequired, Nullable`1 isRequiredDependent, Nullable`1 isOwnership, Nullable`1 deleteBehavior, Boolean removeCurrent, Nullable`1 principalEndConfigurationSource, Boolean oldRelationshipInverted)
   at Microsoft.EntityFrameworkCore.Metadata.Internal.InternalForeignKeyBuilder.HasNavigations(Nullable`1 navigationToPrincipal, Nullable`1 navigationToDependent, EntityType principalEntityType, EntityType dependentEntityType, ConfigurationSource configurationSource)
   at Microsoft.EntityFrameworkCore.Metadata.Internal.InternalForeignKeyBuilder.HasNavigations(MemberInfo navigationToPrincipal, MemberInfo navigationToDependent, EntityType principalEntityType, EntityType dependentEntityType, ConfigurationSource configurationSource)
   at Microsoft.EntityFrameworkCore.Metadata.Builders.CollectionNavigationBuilder.WithOneBuilder(MemberIdentity reference)
   at Microsoft.EntityFrameworkCore.Metadata.Builders.CollectionNavigationBuilder.WithOneBuilder(MemberInfo navigationMemberInfo)
   at Microsoft.EntityFrameworkCore.Metadata.Builders.CollectionNavigationBuilder`2.WithOne(Expression`1 navigationExpression)
   at TestContext.<>c.<OnModelCreating>b__1_0(EntityTypeBuilder`1 b) in C:\wrk\Hopted\test1\ConsoleApp1\Program.cs:line 38
   at Microsoft.EntityFrameworkCore.ModelBuilder.Entity[TEntity](Action`1 buildAction)
   at TestContext.OnModelCreating(ModelBuilder modelBuilder) in C:\wrk\Hopted\test1\ConsoleApp1\Program.cs:line 35
   at Microsoft.EntityFrameworkCore.Infrastructure.ModelCustomizer.Customize(ModelBuilder modelBuilder, DbContext context)
   at Microsoft.EntityFrameworkCore.Infrastructure.ModelSource.CreateModel(DbContext context, IConventionSetBuilder conventionSetBuilder, ModelDependencies modelDependencies)
   at Microsoft.EntityFrameworkCore.Infrastructure.ModelSource.GetModel(DbContext context, ModelCreationDependencies modelCreationDependencies, Boolean designTime)
   at Microsoft.EntityFrameworkCore.Internal.DbContextServices.CreateModel(Boolean designTime)
   at Microsoft.EntityFrameworkCore.Internal.DbContextServices.get_Model()
   at Microsoft.EntityFrameworkCore.Infrastructure.EntityFrameworkServicesBuilder.<>c.<TryAddCoreServices>b__8_4(IServiceProvider p)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitFactory(FactoryCallSite factoryCallSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine, RuntimeResolverLock lockType)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.Resolve(ServiceCallSite callSite, ServiceProviderEngineScope scope)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.DynamicServiceProviderEngine.<>c__DisplayClass2_0.<RealizeService>b__0(ServiceProviderEngineScope scope)
   at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(ServiceIdentifier serviceIdentifier, ServiceProviderEngineScope serviceProviderEngineScope)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope.GetService(Type serviceType)
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType)
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider)
   at Microsoft.EntityFrameworkCore.DbContext.get_DbContextDependencies()
   at Microsoft.EntityFrameworkCore.DbContext.get_ContextServices()
   at Microsoft.EntityFrameworkCore.DbContext.get_InternalServiceProvider()
   at Microsoft.EntityFrameworkCore.DbContext.Microsoft.EntityFrameworkCore.Infrastructure.IInfrastructure<System.IServiceProvider>.get_Instance()
   at Microsoft.EntityFrameworkCore.Infrastructure.Internal.InfrastructureExtensions.GetService(IInfrastructure`1 accessor, Type serviceType)
   at Microsoft.EntityFrameworkCore.Infrastructure.Internal.InfrastructureExtensions.GetService[TService](IInfrastructure`1 accessor)
   at Microsoft.EntityFrameworkCore.Infrastructure.AccessorExtensions.GetService[TService](IInfrastructure`1 accessor)
   at Microsoft.EntityFrameworkCore.Infrastructure.DatabaseFacade.get_Dependencies()
   at Microsoft.EntityFrameworkCore.Infrastructure.DatabaseFacade.EnsureCreated()
   at Program.<Main>$(String[] args) in C:\wrk\Hopted\test1\ConsoleApp1\Program.cs:line 18

Tested on EF Core 7.0.13 and 8.0.0

@vlardn
Copy link
Author

vlardn commented Dec 4, 2023

I found one working workaround (which doesn't use explicit FK configuration) but it looks more like a limited hack:

public class Zoo
{
    public int Id { get; set; }
    public IEnumerable<Animal> Animals { get; set; }
    // public IEnumerable<Tiger> Tigers { get; set; } <= THIS DOESN'T WORK!
    [NotMapped] public IEnumerable<Tiger> Tigers => Animals.OfType<Tiger>();
}

But the loading of Zoo entity with Tigers property only becomes more complex with this workaround:

var zoo = context.Zoos.Include(x => x.Animals.Where(y => y is Tiger)).First();

and the loading of nested properties like Zoo.Tigers.Sponsors with Include or nested querying like Zoos.Where(z => z.Tigers.Any(t => t.Name == "Ava")) is not possible at all here.

So I still think that navigation for sub-types (Zoo.Tigers in this case) could and should be implemented natively by EF (for all inheritance types, i.e.: TPH, TPT, and TPC).

@vlardn
Copy link
Author

vlardn commented Dec 4, 2023

Initially, this problem was reported in #26961 but that issue was closed without working solution and nobody wants to reopen it for a month.

@vlardn vlardn changed the title Navigations for base type and sub type in the same entity results in "Cannot create a relationship ... because a relationship already exists..." exception Having navigations for base type and sub type in the same entity result in "Cannot create a relationship ... because a relationship already exists..." exception Dec 4, 2023
@ajcvickers
Copy link
Member

/cc @AndriySvyryd

@AndriySvyryd
Copy link
Member

This is by design and you already found the best workaround.

@AndriySvyryd AndriySvyryd closed this as not planned Won't fix, can't repro, duplicate, stale Dec 15, 2023
@vlardn
Copy link
Author

vlardn commented Dec 15, 2023

@AndriySvyryd What do you mean "by design"? Does this mean EF Core can construct SQL query for either base-type or sub-type navigation but it's impossible to do the same for both? The database schema is completely identical in all 3 cases and it's just a matter of building the same SQL queries for navigations in 3rd case:

CASE 1 - works:

public class Zoo
{
    public int Id { get; set; }
    public IEnumerable<Animal> Animals { get; set; }
}

var zooWithAnimals = context.Zoos.Include(x => x.Animals).Single()
SELECT "t"."Id", "a"."Id", "a"."ZooId"
      FROM (
          SELECT "z"."Id"
          FROM "Zoos" AS "z"
          LIMIT 2
      ) AS "t"
      LEFT JOIN "Animals" AS "a" ON "t"."Id" = "a"."ZooId"
      ORDER BY "t"."Id"

CASE 2 - works:

public class Zoo
{
    public int Id { get; set; }
    public IEnumerable<Tiger> Tigers { get; set; }
}

var zooWithTigers = context.Zoos.Include(x => x.Tigers).Single()
SELECT "t"."Id", "t0"."Id", "t0"."Discriminator", "t0"."ZooId"
      FROM (
          SELECT "z"."Id"
          FROM "Zoos" AS "z"
          LIMIT 2
      ) AS "t"
      LEFT JOIN (
          SELECT "a"."Id", "a"."Discriminator", "a"."ZooId"
          FROM "Animals" AS "a"
          WHERE "a"."Discriminator" = 'Tiger'
      ) AS "t0" ON "t"."Id" = "t0"."ZooId"
      ORDER BY "t"."Id"

CASE 3 - impossible??

public class Zoo
{
    public int Id { get; set; }
    public IEnumerable<Animal> Animals { get; set; }
    public IEnumerable<Tiger> Tigers { get; set; }
}

var zooWithAnimals = context.Zoos.Include(x => x.Animals).Single()
var zooWithTigers = context.Zoos.Include(x => x.Tigers).Single()

@AndriySvyryd
Copy link
Member

Try configuring the Tiger relationship as one-directional:
b.HasMany(e => e.Tigers).WithOne().HasForeignKey(e => e.ZooId)

@vlardn
Copy link
Author

vlardn commented Dec 15, 2023

Try configuring the Tiger relationship as one-directional: b.HasMany(e => e.Tigers).WithOne().HasForeignKey(e => e.ZooId)

This changes the database schema and adds unnecessary shadow property ZooId1 into the Animals table:

public class Zoo
{
	public int Id { get; set; }
	public IEnumerable<Animal> Animals { get; set; }
	public IEnumerable<Tiger> Tigers { get; set; }
}

public abstract class Animal
{
	public int Id { get; set; }
	public int ZooId { get; set; }
	//public Zoo Zoo { get; set; }
}

public class Tiger : Animal
{
}

modelBuilder.Entity<Zoo>(b =>
{
        b.HasMany(e => e.Animals).WithOne(/* e => e.Zoo */).HasForeignKey(e => e.ZooId);
	b.HasMany(e => e.Tigers).WithOne(/* e => e.Zoo */).HasForeignKey(e => e.ZooId);
});
CREATE TABLE "Animal" (
          "Id" INTEGER NOT NULL CONSTRAINT "PK_Animal" PRIMARY KEY AUTOINCREMENT,
          "ZooId" INTEGER NOT NULL,
          "Discriminator" TEXT NOT NULL,
          "ZooId1" INTEGER NULL,
          CONSTRAINT "FK_Animal_Zoo_ZooId" FOREIGN KEY ("ZooId") REFERENCES "Zoo" ("Id") ON DELETE CASCADE,
          CONSTRAINT "FK_Animal_Zoo_ZooId1" FOREIGN KEY ("ZooId1") REFERENCES "Zoo" ("Id")
);

PS. You can check these SQL statements by running the simple console test app mentioned in the description of this issue.

@AndriySvyryd
Copy link
Member

AndriySvyryd commented Dec 22, 2023

This changes the database schema and adds unnecessary shadow property ZooId1 into the Animals table:

I see, then, indeed there doesn't appear to be a good workaround.

#7623 will enable this scenario too.

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

3 participants