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

TPH Navigation Properties For Inherited Types #25151

Closed
GravlLift opened this issue Jun 25, 2021 · 1 comment
Closed

TPH Navigation Properties For Inherited Types #25151

GravlLift opened this issue Jun 25, 2021 · 1 comment

Comments

@GravlLift
Copy link

I'm using TPH in my application and trying to get multiple navigations on various entities in the hierarchy. I've created a repo to demonstrate the issue I'm facing.

I have 4 entities in my context:

Entity

    public abstract class Entity
    {
        public int Id { get; set; }
        public string TaxIdentifier { get; set; }
        public int? ContactId { get; set; }
        public Contact Contact { get; set; }
    }

Person

    public class Person : Entity
    {
    }

Business

    public class Business : Entity
    {
    }

Contact

    public class Contact
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string PhoneNumber { get; set; }
        public Person Person { get; set; }
        public ICollection<Entity> Entities { get; set; }
    }

My db context describes the nature of the relationship, basically a Person to Contact is 1-1, and Contact to Entity is 1-Many. An Entity is required to have a contact.

AppliocationDbContext

    public class ApplicationDbContext : DbContext
    {
        public DbSet<Contact> Contacts { get; set; }
        public DbSet<Business> Businesses { get; set; }
        public DbSet<Person> People { get; set; }


        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
        { }

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


            modelBuilder.Entity<Contact>(
                eb =>
                {
                    eb.HasMany(c => c.Entities)
                        .WithOne(p => p.Contact)
                        .HasForeignKey(p => p.ContactId)
                        .IsRequired();
                    eb.HasOne(c => c.Person)
                        .WithOne(p => p.Contact)
                        .HasForeignKey<Person>(p => p.ContactId)
                        .IsRequired();
                }
            );


            modelBuilder.Entity<Entity>(
                eb =>
                {
                    eb.HasDiscriminator<string>("EntityType");
                }
            );


            modelBuilder.Entity<Business>(
                eb =>
                {
                    eb.HasBaseType<Entity>();
                }
            );


            modelBuilder.Entity<Person>(
                eb =>
                {
                    eb.HasBaseType<Entity>();
                }
            );
        }
    }

I would expect this to model a Contact table and an Entity table with a discriminator and a ContactId column, and populate Contact.Person and Contact.Entities if Included (with a shared object between those two properties if a Person was associated with the Contact).

However, running the reproduction code results in an InvalidOperationException instead:

System.InvalidOperationException: 'Cannot create a relationship between 'Contact.Person' and 'Person.Contact' because a relationship already exists between 'Contact.Entities' and 'Entity.Contact'. Navigation properties can only participate in a single relationship. If you want to override an existing relationship call 'Ignore' on the navigation 'Person.Contact' first in 'OnModelCreating'.'

I was able to hack around this restriction by mapping one of the relationships to a second ContactId column on Entity and then setting up a database trigger that makes the columns mirror each other, updating each other if one is updated.

CREATE TRIGGER [dbo].[TR_ContactId_Mirror] ON [dbo].[Entity] 
AFTER INSERT, UPDATE 
AS  
BEGIN 	
	SET NOCOUNT ON;
	IF (UPDATE([ContactId]))
	BEGIN
		UPDATE E
		SET [ContactId_Mirror] = inserted.[ContactId]
		FROM [dbo].[Entity] E
			INNER JOIN inserted ON E.[Id] = inserted.[Id]
		END
	ELSE IF (UPDATE([ContactId_Mirror]))
	BEGIN
		UPDATE E
		SET [ContactId] = inserted.[ContactId_Mirror]
		FROM [dbo].[Entity] E
			INNER JOIN inserted ON E.[Id] = inserted.[Id]
	END
END

I think it goes without saying that this is very much not an ideal workaround, but I think it demonstrates that what I'm trying to do here is at the very least a valid relational database configuration.

Did I configure something incorrectly, or is this just an unsupported use of EF Core I'm trying to accomplish here?

EF Core version:

Database provider: Microsoft.EntityFrameworkCore.SqlServer
Target framework: .NET 5.0
Operating system: Windows 10
IDE: Visual Studio 2019 16.10.2

Error Stack Trace

   at Microsoft.EntityFrameworkCore.Metadata.Internal.InternalForeignKeyBuilder.ThrowForConflictingNavigation(IForeignKey foreignKey, IEntityType principalEntityType, IEntityType 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.ReferenceNavigationBuilder.WithOneBuilder(MemberIdentity reference)
   at Microsoft.EntityFrameworkCore.Metadata.Builders.ReferenceNavigationBuilder.WithOneBuilder(MemberInfo navigationMemberInfo)
   at Microsoft.EntityFrameworkCore.Metadata.Builders.ReferenceNavigationBuilder`2.WithOne(Expression`1 navigationExpression)
   at TBHRepro.Data.ApplicationDbContext.<>c.<OnModelCreating>b__13_0(EntityTypeBuilder`1 eb) in C:\Users\Jason\source\repos\TPHRepro\TPHRepro\Data\ApplicationDbContext.cs:line 29
   at Microsoft.EntityFrameworkCore.ModelBuilder.Entity[TEntity](Action`1 buildAction)
   at TBHRepro.Data.ApplicationDbContext.OnModelCreating(ModelBuilder modelBuilder) in C:\Users\Jason\source\repos\TPHRepro\TPHRepro\Data\ApplicationDbContext.cs:line 22
   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, IConventionSetBuilder conventionSetBuilder, ModelDependencies modelDependencies)
   at Microsoft.EntityFrameworkCore.Internal.DbContextServices.CreateModel()
   at Microsoft.EntityFrameworkCore.Internal.DbContextServices.get_Model()
   at Microsoft.EntityFrameworkCore.Infrastructure.EntityFrameworkServicesBuilder.<>c.<TryAddCoreServices>b__7_3(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 singletonCallSite, 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 singletonCallSite, 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__DisplayClass1_0.<RealizeService>b__0(ServiceProviderEngineScope scope)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngine.GetService(Type serviceType, 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_InternalServiceProvider()
   at Microsoft.EntityFrameworkCore.DbContext.get_DbContextDependencies()
   at Microsoft.EntityFrameworkCore.DbContext.get_Model()
   at Microsoft.EntityFrameworkCore.Internal.InternalDbSet`1.get_EntityType()
   at Microsoft.EntityFrameworkCore.Internal.InternalDbSet`1.CheckState()
   at Microsoft.EntityFrameworkCore.Internal.InternalDbSet`1.get_EntityQueryable()
   at Microsoft.EntityFrameworkCore.Internal.InternalDbSet`1.System.Linq.IQueryable.get_Provider()
   at System.Linq.Queryable.FirstOrDefault[TSource](IQueryable`1 source)
   at TBHRepro.Pages.IndexModel.OnGet() in TPHRepro\TPHRepro\Pages\Index.cshtml.cs:line 25
   at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.ExecutorFactory.VoidHandlerMethod.Execute(Object receiver, Object[] arguments)
   at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.<InvokeHandlerMethodAsync>d__30.MoveNext()
@AndriySvyryd
Copy link
Member

This should be possible when #7623 is implemented.

@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

3 participants