Support collections of foreign key values
- Dominant language
- C#
- Stars
- 14.8k
- Forks
- 3.4k
- PR merge metrics
- PR metrics pending
Description
### Issue
I have 2 entities were one has a property pointing to the primary key of another entity - a **classic foreign key**.
The issue is: I use **typed ids** ("owned entities", "value objects") for this and **EF Core throws during configuration** if I have the foreign key configured.
This is _**similar**_ to #30373, but as far I understand the point of "_you are using typed ids as property **_and_** as entity_" does not apply here.
Without the `HasOne` configuration it works fine, but **no foreign key is created** and **Entity Framework will sometimes reorders `INSERTs`**. This is a problem if in one `SaveChanges` call 2 referencing entities are created in the database. Depending on the ordering this, of course, fails with **foreign key constraint violation**.
As I currently cannot instruct EF Core that this foreign key constraint exists in database, this happens.
### Exception
`Microsoft.EntityFrameworkCore.Metadata.Internal.InternalEntityTypeBuilder.HasRelationship` seems to return `null` which throws when `Metadata` is accessed.

```
Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.
at Microsoft.EntityFrameworkCore.Metadata.Builders.EntityTypeBuilder.HasOneBuilder(MemberIdentity navigationId, EntityType relatedEntityType)
at Microsoft.EntityFrameworkCore.Metadata.Builders.EntityTypeBuilder`1.HasOne[TRelatedEntity](Expression`1 navigationExpression)
at OwnedForeignKeyIssue.OrmMapping.DataDbContext.Configure(EntityTypeBuilder`1 builder) in C:\\DataDbContext.cs:line 59
at OwnedForeignKeyIssue.OrmMapping.DataDbContext.OnModelCreating(ModelBuilder modelBuilder) in C:\\DataDbContext.cs:line 28
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.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.b__0(ServiceProviderEngineScope scope)
at Microsoft.Extensions.DependencyInjection.ServiceProvider.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_ContextServices()
at Microsoft.EntityFrameworkCore.DbContext.get_InternalServiceProvider()
at Microsoft.EntityFrameworkCore.DbContext.Microsoft.EntityFrameworkCore.Infrastructure.IInfrastructure.get_Instance()
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.EnsureDeleted()
at Program.$(String[] args) in C:\\Program.cs:line 31
at Program.(String[] args)
```
### Expectation
`Microsoft.EntityFrameworkCore.Metadata.Internal.InternalEntityTypeBuilder.HasRelationship` should be able to find the relationship, even if using typed ids, as without `HasOne` EntityFramework can handle typed ids just fine.
### Repo
The following code shows 3 cases
* **As-is**: `NullReferenceException` in EF Core
* **Repo 1**: Removing `HasOne` (revert to as-is and search for "Repo 1"): Most stuff works, but no foreign key is created and `INSERT` reordering gets a problem
* FK needs to be added _manually_ after a successful `EnsureDatabase()` call (or just running Repo 2 before) and by removing `EnsureDeleted()`
* **Repo 2**: Switching `SchemaId` with a `Guid` (revert to as-is and search for "Repo 2"): Everything works, also the foreign key is created, but the whole point of typed ids is out the window.
#### DbContext and Entity Configuration
```C#
public partial class DataDbContext : DbContext
{
public DataDbContext(DbContextOptions options)
: base(options)
{
}
public DbSet DataEntities => Set();
public DbSet Schemas => Set();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Owned();
modelBuilder.Owned();
Configure(modelBuilder.Entity());
Configure(modelBuilder.Entity());
}
public void Configure(EntityTypeBuilder builder)
{
builder.ToTable("Schemas");
builder.HasKey(x => x.Id);
builder.Property(x => x.Id)
// Repo 2: Comment this is you don't use SchemaId as Id for Schema
.HasConversion(x => x.Value, x => SchemaId.From(x))
;
}
public void Configure(EntityTypeBuilder builder)
{
builder.ToTable("DataEntities");
builder.HasKey(x => x.Id);
builder.Property(x => x.Id)
.HasConversion(x => x.Value, x => DataEntityId.From(x));
builder.Property(x => x.SchemaId)
.HasColumnName("SchemaId")
// Repo 2: Comment this is you don't use SchemaId as Id for Schema
.HasConversion(x => x.Value, x => SchemaId.From(x))
;
// Repo 1: Using SchemaId *works only* if the following foreign key configuration *is commented out*
builder.HasOne()
.WithMany()
.HasForeignKey(data => data.SchemaId)
.HasPrincipalKey(schema => schema.Id)
.IsRequired()
.HasConstraintName("FK_DataEntities_SchemaId_Schemas_Id");
}
}
```
#### Entities and Typed Ids
```C#
public class Schema
{
// Repo 2: Toggle the next 2 properties - if you change to SchemaId it breaks
public virtual SchemaId Id { get; set; }
//public Guid Id { get; set; }
}
public record SchemaId(Guid Value)
{
public static SchemaId From(Guid value)
{
return new SchemaId(value);
}
public static SchemaId Generate()
{
return new SchemaId(Guid.NewGuid());
}
public static SchemaId Parse(string value)
{
return new SchemaId(Guid.Parse(value));
}
public static bool TryParse(string value, [NotNullWhen(true)] out SchemaId? id)
{
if (Guid.TryParse(value, out var guid))
{
id = From(guid);
return true;
}
id = null;
return false;
}
public static implicit operator Guid(SchemaId value)
{
return value?.Value ?? default;
}
public static implicit operator SchemaId(Guid value)
{
return new SchemaId(value);
}
}
public class DataEntity
{
public virtual DataEntityId Id { get; set; }
// Repo 2: Toggle the next 2 properties - if you change to SchemaId it breaks
public virtual SchemaId SchemaId { get; set; }
//public Guid SchemaId { get; set; }
}
public record DataEntityId(Guid Value)
{
public static DataEntityId From(Guid value)
{
return new DataEntityId(value);
}
public static DataEntityId Generate()
{
return new DataEntityId(Guid.NewGuid());
}
public static DataEntityId Parse(string value)
{
return new DataEntityId(Guid.Parse(value));
}
public static implicit operator Guid(DataEntityId value)
{
return value.Value;
}
public static explicit operator DataEntityId(Guid value)
{
return new DataEntityId(value);
}
}
```
#### Test Console Application
```C#
Console.WriteLine("Repo: Typed foreign key bug with HasOne:");
Console.WriteLine("Either");
Console.WriteLine(" * remove HasOne configuration in DataEntity configuration ('Repo 1') which removes foreign key and can trigger a
wrong order or SQL insert statements, OR");
Console.WriteLine(" * don't use SchemaId in Schema and use a Guid ('Repo 2')");
Console.WriteLine();
Console.WriteLine("To switch between the 2: in source code, search for 'Repo 1' or 'Repo 2' to change the commented code to toggle between the different scenarios");
Console.WriteLine();
var services = new ServiceCollection();
services.AddDbContext(
options =>
{
var connectionString = "Server=(localdb)\\MSSQLLOCALDB;Initial Catalog=OwnedTypedForeignKey;Integrated Security=true;";
options.UseSqlServer(connectionString);
});
var sp = services.BuildServiceProvider();
Console.WriteLine("Configured, get DbContext");
var saveDbContext = sp.GetRequiredService();
Console.WriteLine("Create database");
saveDbContext.Database.EnsureDeleted();
saveDbContext.Database.EnsureCreated();
Console.WriteLine("Generate some data and insert it");
var schemaId = SchemaId.Generate();
var schema = new Schema()
{
Id = schemaId,
};
saveDbContext.Add(schema);
var dataEntityId = DataEntityId.Generate();
var dataEntity = new DataEntity
{
Id = dataEntityId,
SchemaId = schemaId
};
saveDbContext.Add(dataEntity);
await saveDbContext.SaveChangesAsync();
Console.WriteLine("Saved");
Console.WriteLine("Load data");
var loadDbContext = sp.GetRequiredService();
var loadedSchemas = loadDbContext.Schemas.ToList();
Console.WriteLine("Schemas: " + string.Join(", ", loadedSchemas.Select(x => x.Id)));
var loadedDataEntities = loadDbContext.DataEntities.ToList();
Console.WriteLine($"Data: {string.Join(", ", loadedDataEntities.Select(x => $"{x.Id} (Schema {x.SchemaId})"))}");
Console.WriteLine();
Console.WriteLine("If HasOne is configured, check database to see the foreign key is generated");
```
### Provider and Version Information
EF Core version: **7.0.10**
Database provider: Microsoft.EntityFrameworkCore.SqlServer **7.0.10**
Target framework: **.NET 7.0**
Operating system: Windows 10 22H2 (Build 19045.3393)
IDE: Visual Studio 2022 17.8.0 Preview 1.0
This also happens with latest **.NET 8 Preview 7** (SDK, EF Core,...).
Contributor guide
Assessment
This issue has not been assessed yet.