Efcore: Relational model builder: Consistently map shadow FK properties

Created on 13 Dec 2016  路  3Comments  路  Source: dotnet/efcore

The documentation says that I'm not required to have FK properties in my model, and instead I can rely on the model builder to generate them as shadow properties. This works in most cases, but I've discovered an inconsistency, demonstrated by the below _Scenario 4_, regarding how those shadow properties are mapped to the database.

I kept the below pseudocode concise for readability, so first note the following:

  • Parent and Child entities each have an Id property.
  • Every property has an auto-implemented getter and setter.
  • Entities and their properties are public.
  • No navigations exist from dependant back to principle.
  • Mappings are by convention except where the fluent API is mentioned.
  • _Disjoint child subclasses_ 1 and 2 directly derive from a common base class that's also mapped.

Scenario 1

class Parent { IList<Child> A; IList<Child> B; }

Generates ParentId and ParentId1 shadow FK properties on Child. These then map to respective FKs in the database.

Scenario 2

class Parent { IList<DisjointChildSubclass1> A; IList<DisjointChildSubclass2> B; }

Generates a ParentId shadow FK property for each subclass. These then map to a single FK in the database.

Scenario 3

class Parent { Child A; IList<Child> B; }

Requires fluent API to specify correct dependant. Specifying a ParentId shadow FK property on Child to correspond to A renames the shadow property corresponding to B to ParentId1. These then map to respective FKs in the database.

Scenario 4

class Parent { DisjointChildSubclass1 A; IList<DisjointChildSubclass2> B; }

Requires fluent API to specify correct dependant. Specifying a ParentId shadow FK property on DisjointChildSubclass1 to correspond to A throws an exception (see below). This is inconsistent and unnecessary; it's better to resolve the problem like in the other scenarios. Resolve it by mapping the two ParentId shadow properties to separate ParentId and ParentId1 FKs in the database.

public class BloggingContext : DbContext
{
    public BloggingContext( DbContextOptions<BloggingContext> options )
        : base( options ) { }

    public DbSet<Parent> Parents { get; set; }
    public DbSet<Child> Children { get; set; }

    protected override void OnModelCreating( ModelBuilder modelBuilder )
    {
        modelBuilder.Entity<Parent>()
            .HasOne( p => p.A )
            .WithOne()
            .HasForeignKey<DisjointChildSubclass1>( "ParentId" );
    }
}

public class Parent
{
    public int Id { get; set; }
    public DisjointChildSubclass1 A { get; set; }
    public IList<DisjointChildSubclass2> B { get; set; }
}

public abstract class Child
{
    public int Id { get; set; }
}

public class DisjointChildSubclass1 : Child { }

public class DisjointChildSubclass2 : Child { }

I just restored my EF-Core reference from the dev feed to 1.2.0-preview1-22878 and retested all scenarios. The exception for _Scenario 4_ says:

System.InvalidOperationException was unhandled by user code
HResult=-2146233079
Message= The foreign keys {ParentId} on DisjointChildSubclass2 and {ParentId} on DisjointChildSubclass1 are both mapped to Children.FK_Children_Parents_ParentId but with different uniqueness.
Source=Microsoft.EntityFrameworkCore
StackTrace:
at Microsoft.EntityFrameworkCore.Infrastructure.ModelValidator.ShowError(String message)
at Microsoft.EntityFrameworkCore.Infrastructure.RelationalModelValidator.EnsureSharedForeignKeysCompatibility(IModel model)
at Microsoft.EntityFrameworkCore.Infrastructure.RelationalModelValidator.Validate(IModel model)
at Microsoft.EntityFrameworkCore.Infrastructure.SqlServerModelValidator.Validate(IModel model)
at Microsoft.EntityFrameworkCore.Infrastructure.ModelSource.CreateModel(DbContext context, IConventionSetBuilder conventionSetBuilder, IModelValidator validator)
at Microsoft.EntityFrameworkCore.Infrastructure.ModelSource.<>c__DisplayClass19_0.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.LazyRef'1.get_Value()
at Microsoft.EntityFrameworkCore.Internal.DbContextServices.get_Model()
at Microsoft.EntityFrameworkCore.Infrastructure.EntityFrameworkServiceCollectionExtensions.<>c.b__0_6(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.Resolve(IServiceCallSite callSite, ServiceProvider provider)
at Microsoft.Extensions.DependencyInjection.ServiceProvider.<>c__DisplayClass16_0.b__0(ServiceProvider provider)
at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(Type serviceType)
at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType)
at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredServiceT
at Microsoft.EntityFrameworkCore.Storage.DatabaseProviderServices.GetServiceTService
at Microsoft.EntityFrameworkCore.Storage.Internal.SqlServerDatabaseProviderServices.get_RelationalDatabaseCreator()
at Microsoft.EntityFrameworkCore.Storage.RelationalDatabaseProviderServices.get_Creator()
at Microsoft.EntityFrameworkCore.Infrastructure.EntityFrameworkServiceCollectionExtensions.<>c.b__0_13(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.Resolve(IServiceCallSite callSite, ServiceProvider provider)
at Microsoft.Extensions.DependencyInjection.ServiceProvider.<>c__DisplayClass16_0.b__0(ServiceProvider provider)
at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(Type serviceType)
at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetServiceT
at Microsoft.EntityFrameworkCore.Infrastructure.AccessorExtensions.GetServiceTService
at Microsoft.EntityFrameworkCore.Infrastructure.DatabaseFacade.get_DatabaseCreator()
at Microsoft.EntityFrameworkCore.Infrastructure.DatabaseFacade.EnsureDeleted()
at EFGetStarted.AspNetCore.NewDb.Startup.Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, BloggingContext dataContext) in C:\Users\Adrian\Development\Examples\ASP.NET Core\EFGetStarted.AspNetCore.NewDb\src\EFGetStarted.AspNetCore.NewDb\Startup.cs:line 58
InnerException:

closed-fixed type-bug

All 3 comments

The error is arising because, the user specified shadow FK property matched with conventional shadow FK in the other subclass, making it to share column in the database though the both FK have different uniqueness.
We should not throw exception for such case, instead change the FK property for the other relationship if the uniqueness is difference. Exception should be thrown only if user explicitly map FK to the same property for relationships with different cardinality.

@HappyNomad - As a work-around you can specify the FK property (different from ParentId) for the other relationship and it would work.

can I poach this?

Poaching this back

Was this page helpful?
0 / 5 - 0 ratings