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.63k stars 3.15k forks source link

Mapping a hierarchy of classes with value objects and concurrency token is not working #22668

Closed marcwittke closed 1 year ago

marcwittke commented 3 years ago

Assuming the following domain model with a base Order class, that has two different inheritors: InternalOrder owning a OrderStoreInfo and ExternalOrder owning a OrderSupplierInfo:

public class Order
{
    public Order(int id)
    {
        Id = id;
    }

    public int Id { get; set; }
    public string Recipient { get; set; }
}

public class ExternalOrder : Order
{
    public ExternalOrder(int id) : base(id)  { }

    public OrderSupplierInfo Supplier { get; set; }
}

public class InternalOrder : Order
{
    public InternalOrder(int id) : base(id) { }

    public OrderStoreInfo Store { get; set; }
}

public class OrderSupplierInfo
{
    public string Name { get; set; }
    public string Url { get; set; }
}

public class OrderStoreInfo
{
    public string Name { get; set; }
    public string Site { get; set; }
}

gets mapped like this:

public class OrderDbContext : DbContext
{
    public DbSet<ExternalOrder> ExternalOrders { get; set; }
    public DbSet<InternalOrder> InternalOrders { get; set; }
    public DbSet<Order> Orders { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer("Server=localhost;Database=TestDb33;User=sa;Password=sa");
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.HasDefaultSchema("purchasing");

        modelBuilder.Entity<Order>(entityTypeBuilder =>
        {
            entityTypeBuilder.Property(nameof(Identified.Id)).ValueGeneratedNever();
            entityTypeBuilder.Property<byte[]>("Timestamp").IsRowVersion();
            entityTypeBuilder.Property(s => s.Recipient).IsRequired().HasMaxLength(100);
        });

        modelBuilder.Entity<InternalOrder>(entityTypeBuilder => entityTypeBuilder.OwnsOne(io => io.Store));

        modelBuilder.Entity<ExternalOrder>(entityTypeBuilder => entityTypeBuilder.OwnsOne(eo => eo.Supplier));
    }
}

Creating the migration with dotnet ef migrations add Initial --verbose fails with Entity type 'OrderStoreInfo' doesn't contain a property mapped to the store-generated concurrency token column 'Timestamp' that is used by another entity type sharing the table 'purchasing.Orders'. Add a store-generated property mapped to the same column to 'OrderStoreInfo'. It can be in shadow state. :

Using project '/home/marc/code/scratch/NpgsqlWithOwnedEntities/NpgsqlWithOwnedEntities/NpgsqlWithOwnedEntities.csproj'.
Using startup project '/home/marc/code/scratch/NpgsqlWithOwnedEntities/NpgsqlWithOwnedEntities/NpgsqlWithOwnedEntities.csproj'.
Writing '/home/marc/code/scratch/NpgsqlWithOwnedEntities/NpgsqlWithOwnedEntities/obj/NpgsqlWithOwnedEntities.csproj.EntityFrameworkCore.targets'...
dotnet msbuild /target:GetEFProjectMetadata /property:EFProjectMetadataFile=/tmp/tmp6cjTqf.tmp /verbosity:quiet /nologo /home/marc/code/scratch/NpgsqlWithOwnedEntities/NpgsqlWithOwnedEntities/NpgsqlWithOwnedEntities.csproj
Writing '/home/marc/code/scratch/NpgsqlWithOwnedEntities/NpgsqlWithOwnedEntities/obj/NpgsqlWithOwnedEntities.csproj.EntityFrameworkCore.targets'...
dotnet msbuild /target:GetEFProjectMetadata /property:EFProjectMetadataFile=/tmp/tmpuxr3if.tmp /verbosity:quiet /nologo /home/marc/code/scratch/NpgsqlWithOwnedEntities/NpgsqlWithOwnedEntities/NpgsqlWithOwnedEntities.csproj
Build started...
dotnet build /home/marc/code/scratch/NpgsqlWithOwnedEntities/NpgsqlWithOwnedEntities/NpgsqlWithOwnedEntities.csproj /verbosity:quiet /nologo

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed 00:00:01.42
Build succeeded.
dotnet exec --depsfile /home/marc/code/scratch/NpgsqlWithOwnedEntities/NpgsqlWithOwnedEntities/bin/Debug/netcoreapp3.1/NpgsqlWithOwnedEntities.deps.json --additionalprobingpath /home/marc/.nuget/packages --runtimeconfig /home/marc/code/scratch/NpgsqlWithOwnedEntities/NpgsqlWithOwnedEntities/bin/Debug/netcoreapp3.1/NpgsqlWithOwnedEntities.runtimeconfig.json /home/marc/.dotnet/tools/.store/dotnet-ef/3.1.8/dotnet-ef/3.1.8/tools/netcoreapp3.1/any/tools/netcoreapp2.0/any/ef.dll migrations add Initial --assembly /home/marc/code/scratch/NpgsqlWithOwnedEntities/NpgsqlWithOwnedEntities/bin/Debug/netcoreapp3.1/NpgsqlWithOwnedEntities.dll --startup-assembly /home/marc/code/scratch/NpgsqlWithOwnedEntities/NpgsqlWithOwnedEntities/bin/Debug/netcoreapp3.1/NpgsqlWithOwnedEntities.dll --project-dir /home/marc/code/scratch/NpgsqlWithOwnedEntities/NpgsqlWithOwnedEntities/ --language C# --working-dir /home/marc/code/scratch/NpgsqlWithOwnedEntities/NpgsqlWithOwnedEntities --verbose --root-namespace NpgsqlWithOwnedEntities
Using assembly 'NpgsqlWithOwnedEntities'.
Using startup assembly 'NpgsqlWithOwnedEntities'.
Using application base '/home/marc/code/scratch/NpgsqlWithOwnedEntities/NpgsqlWithOwnedEntities/bin/Debug/netcoreapp3.1'.
Using working directory '/home/marc/code/scratch/NpgsqlWithOwnedEntities/NpgsqlWithOwnedEntities'.
Using root namespace 'NpgsqlWithOwnedEntities'.
Using project directory '/home/marc/code/scratch/NpgsqlWithOwnedEntities/NpgsqlWithOwnedEntities/'.
Finding DbContext classes...
Finding IDesignTimeDbContextFactory implementations...
Finding application service provider...
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 'OrderDbContext'.
Using context 'OrderDbContext'.
System.InvalidOperationException: Entity type 'OrderStoreInfo' doesn't contain a property mapped to the store-generated concurrency token column 'Timestamp' that is used by another entity type sharing the table 'purchasing.Orders'. Add a store-generated property mapped to the same column to 'OrderStoreInfo'. It can be in shadow state.
   at Microsoft.EntityFrameworkCore.Infrastructure.RelationalModelValidator.ValidateSharedColumnsCompatibility(IReadOnlyList`1 mappedTypes, String tableName, IDiagnosticsLogger`1 logger)
   at Microsoft.EntityFrameworkCore.SqlServer.Internal.SqlServerModelValidator.ValidateSharedColumnsCompatibility(IReadOnlyList`1 mappedTypes, String tableName, IDiagnosticsLogger`1 logger)
   at Microsoft.EntityFrameworkCore.Infrastructure.RelationalModelValidator.ValidateSharedTableCompatibility(IModel model, IDiagnosticsLogger`1 logger)
   at Microsoft.EntityFrameworkCore.Infrastructure.RelationalModelValidator.Validate(IModel model, IDiagnosticsLogger`1 logger)
   at Microsoft.EntityFrameworkCore.SqlServer.Internal.SqlServerModelValidator.Validate(IModel model, IDiagnosticsLogger`1 logger)
   at Microsoft.EntityFrameworkCore.Metadata.Conventions.ValidatingConvention.ProcessModelFinalized(IConventionModelBuilder modelBuilder, IConventionContext`1 context)
   at Microsoft.EntityFrameworkCore.Metadata.Conventions.Internal.ConventionDispatcher.ImmediateConventionScope.OnModelFinalized(IConventionModelBuilder modelBuilder)
   at Microsoft.EntityFrameworkCore.Metadata.Conventions.Internal.ConventionDispatcher.OnModelFinalized(IConventionModelBuilder modelBuilder)
   at Microsoft.EntityFrameworkCore.Metadata.Internal.Model.FinalizeModel()
   at Microsoft.EntityFrameworkCore.ModelBuilder.FinalizeModel()
   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.Microsoft.EntityFrameworkCore.Infrastructure.IInfrastructure<System.IServiceProvider>.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.Design.Internal.DbContextOperations.CreateContext(Func`1 factory)
   at Microsoft.EntityFrameworkCore.Design.Internal.DbContextOperations.CreateContext(String contextType)
   at Microsoft.EntityFrameworkCore.Design.Internal.MigrationsOperations.AddMigration(String name, String outputDir, String contextType)
   at Microsoft.EntityFrameworkCore.Design.OperationExecutor.AddMigrationImpl(String name, String outputDir, String contextType)
   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)

Somewhere I read in a comment that you have to appy the timestamp mapping to all different types of the hierarchy or even on the owned types as well. So let's try... Both inheritors getting the Timestamp shadow property:

modelBuilder.Entity<InternalOrder>(entityTypeBuilder =>
{
    entityTypeBuilder.Property<byte[]>("Timestamp").IsRowVersion();
    entityTypeBuilder.OwnsOne(io => io.Store);
});

modelBuilder.Entity<ExternalOrder>(entityTypeBuilder =>
{
    entityTypeBuilder.Property<byte[]>("Timestamp").IsRowVersion();
    entityTypeBuilder.OwnsOne(eo => eo.Supplier);
});

running dotnet add migrations add Initial results in the exact same error:

Entity type 'OrderStoreInfo' doesn't contain a property mapped to the store-generated concurrency token column 'Timestamp' that is used by another entity type sharing the table 'purchasing.Orders'. Add a store-generated property mapped to the same column to 'OrderStoreInfo'. It can be in shadow state.

so let's add the timstamp shadow property to the owned types as well:

modelBuilder.Entity<InternalOrder>(entityTypeBuilder =>
{
    entityTypeBuilder.Property<byte[]>("Timestamp").IsRowVersion();
    entityTypeBuilder.OwnsOne(io => io.Store, 
        builder => builder.Property<byte[]>("Timestamp").IsRowVersion());
});

modelBuilder.Entity<ExternalOrder>(entityTypeBuilder =>
{
    entityTypeBuilder.Property<byte[]>("Timestamp").IsRowVersion();
    entityTypeBuilder.OwnsOne(eo => eo.Supplier, 
        builder => builder.Property<byte[]>("Timestamp").IsRowVersion());
});

running dotnet add migrations add Initial now results in a slightly different error:

Entity type 'ExternalOrder' doesn't contain a property mapped to the store-generated concurrency token column 'Store_Timestamp' that is used by another entity type sharing the table 'purchasing.Orders'. Add a store-generated property mapped to the same column to 'ExternalOrder'. It can be in shadow state.

but still no luck. Is this scenario supported? Did I find a bug?

Further technical details

EF Core version: 3.1.8 Database provider: Microsoft.EntityFrameworkCore.SqlServer Target framework: .NET Core 3.1 Operating system: GNU/Linux (Fedora 32) IDE: Rider 2020.2

Repro-repo: https://github.com/marcwittke/EfCoreInheritanceOwned

ajcvickers commented 3 years ago

@marcwittke We believe this is already fixed in EF Core 5.0. Can you try with the RC1 release and see if you still run into this?

marcwittke commented 3 years ago

@ajcvickers yes, rc1 fixes the issue, thanks for the feedback.

Also learned that I can target netcore 3.1 with EF Core 5.0 which is nice.