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

Migration error when use owned type for value object #13202

Closed
Bo-Yee-Woods opened this issue Sep 4, 2018 · 15 comments
Closed

Migration error when use owned type for value object #13202

Bo-Yee-Woods opened this issue Sep 4, 2018 · 15 comments

Comments

@Bo-Yee-Woods
Copy link

Bo-Yee-Woods commented Sep 4, 2018

https://github.com/Bo-Yee-Woods/TestOwnedType/tree/master/TestOwnedType
See my sample project, I attempt to use owned type to configure the relationship between my entity and value object (DDD concept). It works fine in my Case one, but I got an migration error (see below) for Case Two. I think the way of implementation of Case Two is the same as Case One, why case two cannot work?

Stack trace:
System.InvalidOperationException: No suitable constructor found for entity type 'ComputedStampDuty'. The following parameters could not be bound to properties of the entity: 'propertyType'.
at Microsoft.EntityFrameworkCore.Metadata.Conventions.Internal.ConstructorBindingConvention.Apply(InternalModelBuilder modelBuilder)
at Microsoft.EntityFrameworkCore.Metadata.Conventions.Internal.ConventionDispatcher.ImmediateConventionScope.OnModelBuilt(InternalModelBuilder modelBuilder)
at Microsoft.EntityFrameworkCore.Metadata.Conventions.Internal.ConventionDispatcher.OnModelBuilt(InternalModelBuilder modelBuilder)
at Microsoft.EntityFrameworkCore.Metadata.Internal.Model.Validate()
at Microsoft.EntityFrameworkCore.Infrastructure.ModelSource.CreateModel(DbContext context, IConventionSetBuilder conventionSetBuilder, IModelValidator validator)
at Microsoft.EntityFrameworkCore.Infrastructure.ModelSource.<>c__DisplayClass5_0.b__1()
at System.Lazy1.ViaFactory(LazyThreadSafetyMode mode) at System.Lazy1.ExecutionAndPublication(LazyHelper executionAndPublication, Boolean useDefaultConstructor)
at System.Lazy1.CreateValue() at Microsoft.EntityFrameworkCore.Infrastructure.ModelSource.GetModel(DbContext context, IConventionSetBuilder conventionSetBuilder, IModelValidator validator) at Microsoft.EntityFrameworkCore.Internal.DbContextServices.CreateModel() at Microsoft.EntityFrameworkCore.Internal.DbContextServices.get_Model() at Microsoft.EntityFrameworkCore.Infrastructure.EntityFrameworkServicesBuilder.<>c.<TryAddCoreServices>b__7_1(IServiceProvider p) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitFactory(FactoryCallSite factoryCallSite, ServiceProviderEngineScope scope) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor2.VisitCallSite(IServiceCallSite callSite, TArgument argument)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScoped(ScopedCallSite scopedCallSite, ServiceProviderEngineScope scope)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor2.VisitCallSite(IServiceCallSite callSite, TArgument argument) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, ServiceProviderEngineScope scope) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor2.VisitCallSite(IServiceCallSite callSite, TArgument argument)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScoped(ScopedCallSite scopedCallSite, ServiceProviderEngineScope scope)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor2.VisitCallSite(IServiceCallSite callSite, TArgument argument) 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.Internal.InternalAccessorExtensions.GetService[TService](IInfrastructure1 accessor)
at Microsoft.EntityFrameworkCore.Infrastructure.AccessorExtensions.GetService[TService](IInfrastructure1 accessor) at Microsoft.EntityFrameworkCore.Design.Internal.DbContextOperations.CreateContext(Func1 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_1.<.ctor>b__0()
at Microsoft.EntityFrameworkCore.Design.OperationExecutor.OperationBase.<>c__DisplayClass3_0`1.b__0()
at Microsoft.EntityFrameworkCore.Design.OperationExecutor.OperationBase.Execute(Action action)
No suitable constructor found for entity type 'ComputedStampDuty'. The following parameters could not be bound to properties of the entity: 'propertyType'.

Steps to reproduce

Include a complete code listing (or project/solution) that we can run to reproduce the issue.
Run "Add-Migration AddCaseTwo" at package manager console

Further technical details

EF Core version: 2.1.1.0
Database Provider: Microsoft.EntityFrameworkCore.SqlServer
Operating system: IDE: Visual Studio 2017 15.4

@Bo-Yee-Woods
Copy link
Author

Bo-Yee-Woods commented Sep 4, 2018

If I add a empty constructor to ComputedStampDuty like, "private ComputedStampDuty() { }"
Then I will get another error.

System.InvalidOperationException: Field 'k__BackingField' of entity type 'ComputedStampDuty' is readonly and so cannot be set.
at Microsoft.EntityFrameworkCore.Infrastructure.ModelValidator.ValidateFieldMapping(IModel model)
at Microsoft.EntityFrameworkCore.Infrastructure.ModelValidator.Validate(IModel model)
at Microsoft.EntityFrameworkCore.Infrastructure.RelationalModelValidator.Validate(IModel model)
at Microsoft.EntityFrameworkCore.Internal.SqlServerModelValidator.Validate(IModel model)
at Microsoft.EntityFrameworkCore.Infrastructure.ModelSource.CreateModel(DbContext context, IConventionSetBuilder conventionSetBuilder, IModelValidator validator)
at Microsoft.EntityFrameworkCore.Infrastructure.ModelSource.<>c__DisplayClass5_0.b__1()
at System.Lazy1.ViaFactory(LazyThreadSafetyMode mode) at System.Lazy1.ExecutionAndPublication(LazyHelper executionAndPublication, Boolean useDefaultConstructor)
at System.Lazy1.CreateValue() at Microsoft.EntityFrameworkCore.Infrastructure.ModelSource.GetModel(DbContext context, IConventionSetBuilder conventionSetBuilder, IModelValidator validator) at Microsoft.EntityFrameworkCore.Internal.DbContextServices.CreateModel() at Microsoft.EntityFrameworkCore.Internal.DbContextServices.get_Model() at Microsoft.EntityFrameworkCore.Infrastructure.EntityFrameworkServicesBuilder.<>c.<TryAddCoreServices>b__7_1(IServiceProvider p) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitFactory(FactoryCallSite factoryCallSite, ServiceProviderEngineScope scope) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor2.VisitCallSite(IServiceCallSite callSite, TArgument argument)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScoped(ScopedCallSite scopedCallSite, ServiceProviderEngineScope scope)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor2.VisitCallSite(IServiceCallSite callSite, TArgument argument) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, ServiceProviderEngineScope scope) at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor2.VisitCallSite(IServiceCallSite callSite, TArgument argument)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitScoped(ScopedCallSite scopedCallSite, ServiceProviderEngineScope scope)
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor2.VisitCallSite(IServiceCallSite callSite, TArgument argument) 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.Internal.InternalAccessorExtensions.GetService[TService](IInfrastructure1 accessor)
at Microsoft.EntityFrameworkCore.Infrastructure.AccessorExtensions.GetService[TService](IInfrastructure1 accessor) at Microsoft.EntityFrameworkCore.Design.Internal.DbContextOperations.CreateContext(Func1 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_1.<.ctor>b__0()
at Microsoft.EntityFrameworkCore.Design.OperationExecutor.OperationBase.<>c__DisplayClass3_0`1.b__0()
at Microsoft.EntityFrameworkCore.Design.OperationExecutor.OperationBase.Execute(Action action)
Field 'k__BackingField' of entity type 'ComputedStampDuty' is readonly and so cannot be set.

@ajcvickers
Copy link
Member

@Bo-Yee-Woods This is because the owned type is still an entity type, and using a constructor to set related entities is not yet supported--see #12078

The error about the backing field being read-only means what it says. EF can't write to a read-only field, and hence can't set the value for the related entity.

@Bo-Yee-Woods
Copy link
Author

@ajcvickers but how to explain my case one can work?

@ajcvickers
Copy link
Member

@Bo-Yee-Woods The way to make it work is:

  • Don't try to pass the entity instance through a constructor
  • Make sure that the property or it's backing field is read-write so that it can be set

@Bo-Yee-Woods
Copy link
Author

Bo-Yee-Woods commented Sep 4, 2018

@ajcvickers Thank you for explaining. As I try following DDD pattern, the value object (owned type) must be immutable (readonly).

@Bo-Yee-Woods
Copy link
Author

@ajcvickers
Copy link
Member

@Bo-Yee-Woods Which part of that documentation specifically?

@Bo-Yee-Woods
Copy link
Author

Bo-Yee-Woods commented Sep 4, 2018

@ajcvickers The fields in "Address" are readonly.

public class Address : ValueObject
{
    public String Street { get; }
    public String City { get; }
    public String State { get; }
    public String Country { get; }
    public String ZipCode { get; }

    private Address() { }

   public Address(string street, string city, string state, string country, string zipcode)
   {
        Street = street;
        City = city;
        State = state;
        Country = country;
        ZipCode = zipcode;
 }

 protected override IEnumerable<object> GetAtomicValues()
 {
      // Using a yield return statement to return each element one at a time
      yield return Street;
      yield return City;
      yield return State;
      yield return Country;
      yield return ZipCode;
  }  
 } 

@Bo-Yee-Woods
Copy link
Author

@ajcvickers As it is 2am now per Singapore time, I will read your update tomorrow.

@ajcvickers
Copy link
Member

@Bo-Yee-Woods That's fine because non of the property types are entity types--they are simple scalar properties. In your case, ComputedStampDuty is an instance of an owned entity type. (I know you are treating it as a value object type, but it's really still an entity type.)

@Bo-Yee-Woods
Copy link
Author

Bo-Yee-Woods commented Sep 4, 2018

@ajcvickers wait, the owner entity of address value object is order entity. The order entity has property Address and also parameter in constructor. Pls refer to here https://github.com/dotnet-architecture/eShopOnAzure/blob/master/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/Order.cs

@ajcvickers
Copy link
Member

@Bo-Yee-Woods I'm running out of different ways of explaining this, so let me show the difference using code:

public class EntityTypeOne
{
    public int Id { get; set; }

    // OwnedOne can't be read-only or set via constructor because OwnedEntityTypeOne is an entity type
    public OwnedEntityTypeOne OwnedOne { get; set; }
}

public class OwnedEntityTypeOne
{
    public OwnedEntityTypeOne(string a, string b)
    {
        A = a;
        B = b;
    }

    // A and B are not entity types and hence can be read-only and set in constructor
    public string A { get; }
    public string B { get; }

    // OwnedTwo can't be read-only or set via constructor because OwnedEntityTypeTwo is an entity type
    public OwnedEntityTypeTwo OwnedTwo { get; set; }
}

public class OwnedEntityTypeTwo
{
    public OwnedEntityTypeTwo(string c, string d)
    {
        C = c;
        D = d;
    }

    // C and D are not entity types and hence can be read-only and set in constructor
    public string C { get; }
    public string D { get; }
}

public class BloggingContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder
            .UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Test;ConnectRetryCount=0");

    protected override void OnModelCreating(ModelBuilder builder)
    {
        builder.Entity<EntityTypeOne>(b =>
        {
            b.OwnsOne(
                e => e.OwnedOne,
                b2 =>
                {
                    b2.Property(e => e.A);
                    b2.Property(e => e.B);
                    b2.OwnsOne(
                        e => e.OwnedTwo,
                        b3 =>
                        {
                            b3.Property(e => e.C);
                            b3.Property(e => e.D);
                        });
                });
        });
    }
}

@Bo-Yee-Woods
Copy link
Author

@ajcvickers Thank you very much!

@Bo-Yee-Woods
Copy link
Author

Bo-Yee-Woods commented Sep 5, 2018

@ajcvickers Hi, I make it works finally in my Case Three. Moreover, I have some other findings. I summarize the points we need to notice.

public class PaymentRecordEntityConfiguration
    : IEntityTypeConfiguration<PaymentRecordEntity>
{
    public void Configure(EntityTypeBuilder<PaymentRecordEntity> builder)
    {
        builder.Property(x => x.DocumentRefNo).IsRequired();

        builder.Property<double>("TotalAmount").IsRequired();

        builder.OwnsOne(
            paymentRecordEntity => paymentRecordEntity.ComputedStampDuty,
            computedStampDuty =>
            {
                // 1. must explicitly declare all property here
                computedStampDuty.Property(csd => csd.PropertyType);
                computedStampDuty.Property(csd => csd.BuyersStampDuty);
                computedStampDuty.Property(csd => csd.AdditionalBuyersStampDuty);
                computedStampDuty.Ignore(csd => csd.TotalAmountPayable);
            }
        );
    }
}

public class PaymentRecordEntity : BaseEntity
{
    private string _documentRefNo;
    private double _totalAmount;

    private PaymentRecordEntity() { }

    public PaymentRecordEntity(
        string documentRefNo,
        double totalAmount,
        ComputedStampDuty computedStampDuty)
    {
        _documentRefNo = documentRefNo;
        _totalAmount = totalAmount;
        ComputedStampDuty = computedStampDuty;
    }

    public string DocumentRefNo => _documentRefNo;

    // 2. must have a public setter for owned property type
    public ComputedStampDuty ComputedStampDuty { get; set; }
}

public class ComputedStampDuty
{
    // 3. cannot have empty constructor in owned type
    // 4.  constructor must have parameters mapping all property type
    // private ComputedStampDuty() { }

    public ComputedStampDuty(string propertyType, double buyersStampDuty, double additionalBuyersStampDuty)
    {
        PropertyType = propertyType;
        BuyersStampDuty = buyersStampDuty;
        AdditionalBuyersStampDuty = additionalBuyersStampDuty;
    }

    public string PropertyType { get; }

    public double BuyersStampDuty { get; }

    public double AdditionalBuyersStampDuty { get; }

    public double TotalAmountPayable => BuyersStampDuty + AdditionalBuyersStampDuty;
}

@ajcvickers
Copy link
Member

@Bo-Yee-Woods Just to be accurate, for 2) The setter could be internal or private, or it could have no setter and a writable backing field. For 3) Owned type can have an empty constructor, but if it does, then EF will use it instead of the parameterized constructor. This may change in 3.0--see #10865. 4) if the properties are read-only, then they must be set from the constructor.

@ajcvickers ajcvickers reopened this Oct 16, 2022
@ajcvickers ajcvickers closed this as not planned Won't fix, can't repro, duplicate, stale Oct 16, 2022
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