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.75k stars 3.18k forks source link

Can't add migration for nested owned entities #32746

Closed vyruz1986 closed 9 months ago

vyruz1986 commented 9 months ago

I'm failing to figure out how to configure data seeding on nested owned entities. I've read through #31373 and #14359 and am aware of the current issues about EFCore not being able to seed data on the top-level entity (tracked in #10000). What those issues have not yet discussed, is seeding data across nested owned types. I have made a minimal example (also attached below) which configures a Product > Price > MonetaryAmount relation.

Creating a migration without the data seeding works, and the generated SQL looks good in that it wants to create a single table with a column for each of the properties of the owned types, but when I add the HasData() statements and try to create a migration, I'm given the following error:

Unable to create a 'DbContext' of type ''. The exception 'The seed entity for entity type 'Product' cannot be added because it has the navigation 'Price' set. To seed relationships,  add the entity seed to 'Price' and specify the foreign key values {'ProductId'}.

Include your code

public class Product
{
    private Product() { }
    public Product(int id, Price price)
    {
        Id = id;
        Price = price ?? throw new ArgumentNullException(nameof(price));
    }

    public int Id { get; private init; }
    public Price Price { get; private init; }
}

public record Price
{
    private Price() { }

    public Price(MonetaryAmount amount)
    {
        Amount = amount ?? throw new ArgumentNullException(nameof(amount));
    }

    public MonetaryAmount Amount { get; }
}

public record MonetaryAmount
{
    public MonetaryAmount(decimal value, Currency currency)
    {
        if (!Enum.IsDefined(currency))
        {
            throw new ArgumentOutOfRangeException(nameof(currency), currency, "The currency is unsupported");
        }
        Value = value;
        Currency = currency;
    }

    public decimal Value { get; private set; }

    public Currency Currency { get; }
}

public enum Currency
{
    EUR = 978
}

public class ProductContext : DbContext
{
    public ProductContext()
    {
    }

    public ProductContext(DbContextOptions options) : base(options)
    {
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder.UseSqlServer();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Product>(builder =>
        {
            builder.OwnsOne(p => p.Price, price =>
            {
                price.OwnsOne(p => p.Amount, priceAmount =>
                {
                    priceAmount.Property(p => p.Value)
                        .HasPrecision(2, 20)
                        .IsRequired();

                    priceAmount.Property(p => p.Currency)
                        .HasDefaultValue(Currency.EUR)
                        .IsUnicode(false)
                        .HasMaxLength(3)
                        .IsRequired();

                    priceAmount.HasData(new { PriceProductId = 1, Value = 1m, Currency = Currency.EUR });
                });

                price.HasData(new { ProductId = 1 });
            });

            builder.HasData(new Product(1, new Price(new MonetaryAmount(1, Currency.EUR))));
        });
    }
}

Full example available here: EFCoreNestedOwnedTypesDataSeeding.zip

Stack traces

Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.
   at SixFour.Sub() in C:\Stuff\AllTogetherNow\SixFour\SixFour.cs:line 49
   at SixFour.Main() in C:\Stuff\AllTogetherNow\SixFour\SixFour.cs:line 54

Verbose output

Using assembly 'EFCoreNestedOwnedTypesDataSeeding'.
Using startup assembly 'EFCoreNestedOwnedTypesDataSeeding'.
Using application base 'C:\Users\ag\workspaces\poc\EFCoreNestedOwnedTypesDataSeeding\bin\Debug\net8.0'.
Using working directory 'C:\Users\ag\workspaces\poc\EFCoreNestedOwnedTypesDataSeeding'.
Using root namespace 'EFCoreNestedOwnedTypesDataSeeding'.
Using project directory 'C:\Users\ag\workspaces\poc\EFCoreNestedOwnedTypesDataSeeding\'.
Remaining arguments: .
Finding DbContext classes...
Finding IDesignTimeDbContextFactory implementations...
Finding application service provider in assembly 'EFCoreNestedOwnedTypesDataSeeding'...
Finding Microsoft.Extensions.Hosting service provider...
No static method 'CreateHostBuilder(string[])' was found on class 'Program'.
No application service provider was found.
Finding DbContext classes in the project...
Found DbContext 'ProductContext'.
Using context 'ProductContext'.
Microsoft.EntityFrameworkCore.Design.OperationException: Unable to create a 'DbContext' of type ''. The exception 'The seed entity for entity type 'Product' cannot be added because it has the navigation 'Price' set. To seed relationships,  add the entity seed to 'Price' and specify the foreign key values {'ProductId'}. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the involved property values.' was thrown while attempting to create an instance. For the different patterns supported at design time, see https://go.microsoft.com/fwlink/?linkid=851728
 ---> System.InvalidOperationException: The seed entity for entity type 'Product' cannot be added because it has the navigation 'Price' set. To seed relationships,  add the entity seed to 'Price' and specify the foreign key values {'ProductId'}. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the involved property values.
   at Microsoft.EntityFrameworkCore.Infrastructure.ModelValidator.ValidateData(IModel model, IDiagnosticsLogger`1 logger)
   at Microsoft.EntityFrameworkCore.Infrastructure.RelationalModelValidator.ValidateData(IModel model, IDiagnosticsLogger`1 logger)
   at Microsoft.EntityFrameworkCore.Infrastructure.ModelValidator.Validate(IModel model, IDiagnosticsLogger`1 logger)
   at Microsoft.EntityFrameworkCore.Infrastructure.RelationalModelValidator.Validate(IModel model, IDiagnosticsLogger`1 logger)
   at Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal.SqlServerModelValidator.Validate(IModel model, IDiagnosticsLogger`1 logger)
   at Microsoft.EntityFrameworkCore.Infrastructure.ModelRuntimeInitializer.Initialize(IModel model, Boolean designTime, IDiagnosticsLogger`1 validationLogger)
   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.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.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.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.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.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.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.EntityFrameworkCore.DbContext.get_DbContextDependencies()
   at Microsoft.EntityFrameworkCore.DbContext.get_ContextServices()
   at Microsoft.EntityFrameworkCore.DbContext.Microsoft.EntityFrameworkCore.Infrastructure.IInfrastructure<System.IServiceProvider>.get_Instance()
   at Microsoft.EntityFrameworkCore.Infrastructure.Internal.InfrastructureExtensions.GetService(IInfrastructure`1 accessor, Type serviceType)
   at Microsoft.EntityFrameworkCore.Design.Internal.DbContextOperations.CreateContext(String contextType)
   --- End of inner exception stack trace ---
   at Microsoft.EntityFrameworkCore.Design.Internal.DbContextOperations.CreateContext(String contextType)
   at Microsoft.EntityFrameworkCore.Design.Internal.MigrationsOperations.AddMigration(String name, String outputDir, String contextType, String namespace)
   at Microsoft.EntityFrameworkCore.Design.OperationExecutor.AddMigrationImpl(String name, String outputDir, String contextType, String namespace)
   at Microsoft.EntityFrameworkCore.Design.OperationExecutor.AddMigration.<>c__DisplayClass0_0.<.ctor>b__0()
   at Microsoft.EntityFrameworkCore.Design.OperationExecutor.OperationBase.<>c__DisplayClass3_0`1.<Execute>b__0()
   at Microsoft.EntityFrameworkCore.Design.OperationExecutor.OperationBase.Execute(Action action)
Unable to create a 'DbContext' of type ''. The exception 'The seed entity for entity type 'Product' cannot be added because it has the navigation 'Price' set. To seed relationships,  add the entity seed to 'Price' and specify the foreign key values {'ProductId'}. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the involved property values.' was thrown while attempting to create an instance. For the different patterns supported at design time, see https://go.microsoft.com/fwlink/?linkid=851728

Include provider and version information

EF Core version: Database provider: Microsoft.EntityFrameworkCore.SqlServer Target framework: .NET 8.0 Operating system: Windows 11 23H2 build 22631.2861 IDE: VSCode 1.85.1

ajcvickers commented 9 months ago

@vyruz1986 This requires using an anonymous type, since the Product type can't be created publicly without setting the navigation property. Something like this:

modelBuilder.Entity<Product>(builder =>
{
    builder.OwnsOne(p => p.Price, price =>
    {
        price.OwnsOne(p => p.Amount, priceAmount =>
        {
            priceAmount.Property(p => p.Value)
                .HasPrecision(2, 20)
                .IsRequired();
            priceAmount.Property(p => p.Currency)
                .HasDefaultValue(Currency.EUR)
                .IsUnicode(false)
                .HasMaxLength(3)
                .IsRequired();
            priceAmount.HasData(new { PriceProductId = 1, Value = 1m, Currency = Currency.EUR });
        });
        price.HasData(new { ProductId = 1 });
    });
    builder.HasData(new { Id = 1 });
});
vyruz1986 commented 9 months ago

Ah I see, I was still trying to seed the Product data with the actual type, instead of also using an anonymous there as well. This fixed it for me, thanks!