dotnet / efcore

EF Core is a modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations.
https://docs.microsoft.com/ef/
MIT License
13.78k stars 3.19k forks source link

Navigation properties using foreign key relationships to composite unique key constraints cause an error #20881

Closed mitselplik closed 2 years ago

mitselplik commented 4 years ago

An error occurs if trying to model against an existing database where the parent table has both an int Id primary key plus a composite unique key consisting of other columns, and the child table has a foreign key constraint to the unique key constraint rather than the primary key constraint.

Many legacy databases are likely to have similar issues, where having an Id identity column makes data easy to track for CRUD operations, but a composite list of column values defines a unique constraint used elsewhere in the database.

For example, we have a class that mirrors an existing parent database table:

public class PropertyExemption
{
    // PK Identity column for table.
    public int PropertyExemptionId { get; set; }  

    // These fields are a unique constraint for the table
    public short Year { get; set; }
    public int PropertyId { get; set; }
    public string ExemptionCode { get; set; }

    // ... other properties ...
}

We have a related class that mirrors an existing child database table that has a foreign key constraint to the parent table's unique constraint rather than to the primary key:

public class PropertyExemptionTaxingAuthorityValue
{
    public short Year { get; set; }
    public int PropertyId { get; set; }
    public string ExemptionCode { get; set; }
    public string TaxingAuthorityCode { get; set; }
    public decimal GrantedExemptionAmount { get; set; }
}

The relevant portions of the model builder code are setup like so:

builder.Entity<PropertyExemption>
    .HasKey(e => new { e.PropertyExemptionId });

builder.Entity<PropertyExemption>
    .HasAlternateKey(c => new { c.Year, c.PropertyId, c.ExemptionCode })
    .HasName("CUQ_property_exemption");

builder.Entity<PropertyExemptionTaxingAuthorityValue>
    .HasKey(e => new { e.Year, e.PropertyId, e.ExemptionCode, e.TaxingAuthorityCode);

builder.Entity<PropertyExemptionTaxingAuthorityValue>
    .HasOne(d => d.PropertyExemption)
    .WithMany(p => p.PropertyExemptionTaxingAuthorityValue)
    .HasForeignKey(d => new { d.Year, d.PropertyId, d.ExemptionCode })
    .HasPrincipalKey(d => new { d.PropertyExemptionId })
    .HasConstraintName("CFK_property_exemption_taxing_authority_value_property_exemption");

The above setup causes the following error to occur:

InvalidOperationException: The number of properties specified for the foreign key {'Year', 'PropertyId', 'ExemptionCode'} on entity type 'PropertyExemptionTaxingAuthorityValue' does not match the number of properties in the principal key {'PropertyExemptionId'} on entity type 'PropertyExemption'.

Further technical details

EF Core version: 3.1.3 Database provider: Microsoft.EntityFrameworkCore.SqlServer Target framework: .NET Core 3.1 Operating system: Windows 10 IDE: Visual Studio 2019 16.5.4 Community Edition Stack Trace:

   at Microsoft.EntityFrameworkCore.Metadata.Internal.ForeignKey.AreCompatible(IReadOnlyList`1 principalProperties, IReadOnlyList`1 dependentProperties, IEntityType principalEntityType, IEntityType dependentEntityType, Boolean shouldThrow)
   at Microsoft.EntityFrameworkCore.Metadata.Internal.ForeignKey.AreCompatible(EntityType principalEntityType, EntityType dependentEntityType, MemberInfo navigationToPrincipal, MemberInfo navigationToDependent, IReadOnlyList`1 dependentProperties, IReadOnlyList`1 principalProperties, Nullable`1 unique, Boolean shouldThrow)
   at Microsoft.EntityFrameworkCore.Metadata.Builders.RelationshipBuilderBase..ctor(InternalRelationshipBuilder builder, RelationshipBuilderBase oldBuilder, Boolean foreignKeySet, Boolean principalKeySet, Boolean requiredSet)
   at Microsoft.EntityFrameworkCore.Metadata.Builders.ReferenceCollectionBuilder..ctor(InternalRelationshipBuilder builder, ReferenceCollectionBuilder oldBuilder, Boolean foreignKeySet, Boolean principalKeySet, Boolean requiredSet)
   at Microsoft.EntityFrameworkCore.Metadata.Builders.ReferenceCollectionBuilder`2..ctor(InternalRelationshipBuilder builder, ReferenceCollectionBuilder oldBuilder, Boolean foreignKeySet, Boolean principalKeySet, Boolean requiredSet)
   at Microsoft.EntityFrameworkCore.Metadata.Builders.ReferenceCollectionBuilder`2.HasPrincipalKey(Expression`1 keyExpression)
   at PropertyDbContext.OnModelCreating(EntityTypeBuilder`1 entity) in ***(removed IP info)***\PropertyDbContext.cs:line 4303
   at Microsoft.EntityFrameworkCore.ModelBuilder.Entity[TEntity](Action`1 buildAction)
   at PropertyDbContext.OnModelCreating(ModelBuilder modelBuilder) in ***(removed IP info)***\PropertyDbContext.cs:line 117
   at Microsoft.EntityFrameworkCore.Infrastructure.ModelCustomizer.Customize(ModelBuilder modelBuilder, DbContext context)
   at Microsoft.EntityFrameworkCore.Infrastructure.ModelSource.CreateModel(DbContext context, IConventionSetBuilder conventionSetBuilder)
   at Microsoft.EntityFrameworkCore.Infrastructure.ModelSource.GetModel(DbContext context, IConventionSetBuilder conventionSetBuilder)
   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.Collections.Generic.IEnumerable<TEntity>.GetEnumerator()
smitpatel commented 4 years ago
.HasForeignKey(d => new { d.Year, d.PropertyId, d.ExemptionCode })
    .HasPrincipalKey(d => new { d.PropertyExemptionId })

is incorrect. Referential integrity constraint in database requires that referencing properties and referenced properties should be matching in count to propagate values.

the child table has a foreign key constraint to the unique key constraint rather than the primary key constraint.

HasPrincipalKey should refer to unique key constraint rather than PK constraint. Change your code to following in relevant place.

builder.Entity<PropertyExemptionTaxingAuthorityValue>
    .HasOne(d => d.PropertyExemption)
    .WithMany(p => p.PropertyExemptionTaxingAuthorityValue)
    .HasForeignKey(d => new { d.Year, d.PropertyId, d.ExemptionCode })
    .HasPrincipalKey(c => new { c.Year, c.PropertyId, c.ExemptionCode })
    .HasConstraintName("CFK_property_exemption_taxing_authority_value_property_exemption");
mitselplik commented 4 years ago

Apologies - I misunderstood the documentation. It is working now.