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

Entity that owns multiple entities of the same type with navigations back to the owning type makes model construction fail #35346

Open
alexanderchr opened this issue Dec 18, 2024 · 4 comments

Comments

@alexanderchr
Copy link

alexanderchr commented Dec 18, 2024

I have lots of entities for which I need to track when and by whom they were created and last updated. I have solved this by putting an owned type UserTimestamp on tracked entities as Created and Updated. Something like this:

public class SomeEntity
{
    // ...other properties
    public UserTimesamp Created { get; set; }
    public UserTimesamp Updated { get; set; }
}

[Owned]
public class UserTimestamp
{
    public int UserId { get; set; }
    public User User { get; set; }
    public DateTime Timestamp { get; set; }
}

This worked well until I needed to add tracking to the User model, probably because of the self referencing that this introduces. If I put Created and Updated as above on the User model, EF throws during model construction.

Below is a minimal reproduction:

using Microsoft.EntityFrameworkCore;

using var context = new MyContext();
Console.WriteLine(context.Model.ToDebugString());

public class User
{
    public int Id { get; set; }
    public UserTimestamp Created { get; set; }
    public UserTimestamp Updated { get; set; }
}

[Owned]
public class UserTimestamp
{
    public int UserId { get; set; }
    public User User { get; set; }
    public DateTime Timestamp { get; set; }
}

public class MyContext : DbContext
{
    public DbSet<User> Users  { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder options)
    {
        options.UseSqlServer();
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<User>().OwnsOne(u => u.Created, o => o.WithOwner().HasForeignKey("OwnerUserId"));
        modelBuilder.Entity<User>().OwnsOne(u => u.Updated, o => o.WithOwner().HasForeignKey("OwnerUserId"));
    }
}

which throws

Unhandled exception. System.InvalidOperationException: The object 'UserTimestamp' has been removed from the model.
   at Microsoft.EntityFrameworkCore.Metadata.Internal.EntityType.get_Builder()
   at Microsoft.EntityFrameworkCore.Metadata.Internal.EntityType.Microsoft.EntityFrameworkCore.Metadata.IConventionEntityType.get_Builder()
   at Microsoft.EntityFrameworkCore.Metadata.Conventions.RelationshipDiscoveryConvention.ProcessNavigationAdded(IConventionNavigationBuilder navigationBuilder, IConventionContext`1 context)
   at Microsoft.EntityFrameworkCore.Metadata.Conventions.Internal.ConventionDispatcher.ImmediateConventionScope.OnNavigationAdded(IConventionNavigationBuilder navigationBuilder)
   at Microsoft.EntityFrameworkCore.Metadata.Conventions.Internal.ConventionDispatcher.OnNavigationAddedNode.Run(ConventionDispatcher dispatcher)
   at Microsoft.EntityFrameworkCore.Metadata.Conventions.Internal.ConventionDispatcher.DelayedConventionScope.Run(ConventionDispatcher dispatcher)
   at Microsoft.EntityFrameworkCore.Metadata.Conventions.Internal.ConventionDispatcher.ConventionBatch.Run()
   at Microsoft.EntityFrameworkCore.Metadata.Conventions.Internal.ConventionDispatcher.ConventionBatch.Run(IConventionForeignKey foreignKey)
   at Microsoft.EntityFrameworkCore.Metadata.Builders.EntityTypeBuilder`1.OwnsOneBuilder[TRelatedEntity](TypeIdentity ownedType, MemberIdentity navigation)
   at Microsoft.EntityFrameworkCore.Metadata.Builders.EntityTypeBuilder`1.OwnsOne[TRelatedEntity](Expression`1 navigationExpression, Action`1 buildAction)
   at MyContext.OnModelCreating(ModelBuilder modelBuilder) in .../Program.cs:line 33
   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_Model()
   at Program.<Main>$(String[] args) in .../Program.cs:line 4

However, if I remove the Updated property from the type and the configuration, the following model is created, which looks as expected:

Model: 
  EntityType: User
    Properties: 
      Id (int) Required PK AfterSave:Throw ValueGenerated.OnAdd
    Navigations: 
      Created (UserTimestamp) ToDependent UserTimestamp
    Keys: 
      Id PK
  EntityType: UserTimestamp Owned
    Properties: 
      OwnerUserId (no field, int) Shadow Required PK FK AfterSave:Throw
      Timestamp (DateTime) Required
      UserId (int) Required FK Index
    Navigations: 
      User (User) ToPrincipal User
    Keys: 
      OwnerUserId PK
    Foreign keys: 
      UserTimestamp {'OwnerUserId'} -> User {'Id'} Unique Required Ownership Cascade ToDependent: Created
      UserTimestamp {'UserId'} -> User {'Id'} Required Cascade ToPrincipal: User
    Indexes: 
      UserId

Everything also works if I make a copy of the class UserTimestamp and use that copy for the Updated property (so that Created and Updated have nominally different, but otherwise equivalent, types).

It seems to me that since it works to have navigations back to owning entity type for the case when the owning type owns a type once, and also when it owns multiple (nominally different) types, the expected behaviour is that is should also work for the case when it owns the same type multiple times.

Include provider and version information

EF Core version: 9.0.0
Database provider: occurs with both SQLServer and Postgres providers
Target framework: .NET 9.0
Operating system: N/A
IDE: N/A

@kirides
Copy link

kirides commented Dec 18, 2024

      UserTimestamp {'OwnerUserId'} -> User {'Id'} Unique Required Ownership Cascade ToDependent: Created
      UserTimestamp {'UserId'} -> User {'Id'} Required Cascade ToPrincipal: User

you have two UserId columns in your Owned entity.

As your User "Owns" a "UserTimestamp" there is an implicit "UserId"-Column in that entity.
You now added a second UserId and a Navigation-Property to that Second userid

i imagine just removing the UserId from UserTimestamp would be enough to fix it

@alexanderchr
Copy link
Author

alexanderchr commented Dec 18, 2024

@kirides Yes, that is by design. The UserTimestamp is owned by a User, but it also points to a User (the user that made the change to the owning User). This works well when UserTimestamp is placed on any other entity than User, or when User owns only one UserTimestamp, but not when it owns multiple.

@kirides
Copy link

kirides commented Dec 18, 2024

@alexanderchr what happens if modify it to this?
I could imagine the model builder trying to use "User" as the navigation property for the "OwnedUserId"-Shadow property and not correctly using it for "UserId" in that case

[ForeignKey(nameof(User))]
public int UserId { get; set; }

public User User { get; set; }

@alexanderchr
Copy link
Author

alexanderchr commented Dec 18, 2024

I get the same error with that change. But as you can see from the model with Updated commented out, the "has one" relation to User is picked up correctly and is using the right foreign key. I don't understand what changes when I add the second UserTImestamp, but I think it must be EF confusing the two relations internally.

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

2 participants