5
votes

Consider these simple classes. They belong to a simple application with Domain Driven Design (DDD) principles, and as such every Entity and ValueObject receives its property values through the constructor while hiding the default, parameter-less constructor. Properties will also be read-only.

    public class MyClass
    {
       public Guid Id {get;}
       public ValueObject ValueObject1 {get;}
       public ValueObject ValueObject2 {get;}
       public MyClass(ValueObject valueObject1, ValueObject valueObject2) 
       {
          ValueObject1 = valueObject1;
          ValueObject2 = valueObject2;
       }
       private MyClass(){}
    }

    public class ValueObject
    {
       public string Value {get;}
       public ValueObject(string value) 
       {
          Value = value;
       }
       private ValueObject(){}
    }

I want to be able to create a database based on this model, using EntityFramework Core 2.2.6.

Apparently EF Core 2.2.6 can automatically feed property values for these classes through their parametrized constructors, as long as constructor parameters and class properties have the same name (case-insensitive). Great.

Now I want the ValueObjects to be stored in the same table as the MyClass. To make that happen, I am told, I should use modelBuilder.OwnsOne<> in OnModelCreating of the DBContext, instead of modelBuilder.Property<>

The DBContext configuration in OnModelCreating would look like something this:

modelBuilder.Entity<MyClass>(b => b.HasKey(mc => mc.Id));
modelBuilder.Entity<MyClass>(b => b.OwnsOne(mc => mc.ValueObject1,rb =>
        {
            rb.Property(vo => vo.Value);
        }));
modelBuilder.Entity<MyClass>(b => b.OwnsOne(mc => mc.ValueObject2, rb =>
        {
            rb.Property(vo => vo.Value);
        }));

Now it seems modelBuilder.OwnsOne<> and modelBuilder.Property<> are mutually exclusive, meaning you can't use them both together because every time I try to Add-Migration with both of them I get:

'ValueObject' cannot be used as a property on entity type 'MyClass' because it is configured as a navigation.

But if I don't use modelBuilder.Property<> and only use modelBuilder.OwnsOne<>, I get:

No suitable constructor found for entity type 'MyClass'. The following constructors had parameters that could not be bound to properties of the entity type: cannot bind 'valueObject1', 'valueObject2' in 'MyClass(ValueObject valueObject1, ValueObject valueObject2)'.

Which means the constructor to property binding pattern only works only if I use modelBuilder.Property<> to configure the properties on MyClass.

So my question is: how should I configure the DBContext to allow EF Core to both set property values through the parametrized constructor, and store ValueObjects in the same table as the Entity?

1
I can use your model with context.Database.EnsureCreated() alright. The only thing is, I have to add private set to the properties, just as is done here. If I don't, I get an exception that a field "is readonly and so cannot be set" when EF tries to build the model. Which is not the perfect implementation of a value object but seems necessary anyway.Gert Arnold
Tried your classes and had the same result as @GertArnold. You need to add private set to your properties to make it work properly, as explained here. Probably owned types as readonly are not supported yet...Fabio M.
@GertArnold So did you add private setters to all properties both on MyClass and ValueObject?TheAgent
@TheAgent I do doubt the usefulness of value objects in an EF class model though. In the end, the class model is just part of the data layer, it's not a domain model. It should be optimized for its task and not be torn between two responsibilities. Its task is to channel data and their changes between the database and the application. If you want to modify and save only one property of a MyClass object now you have to jump through hoops to get it done, if it can be done at all. An EF class model should be mutable because the whole architecture is based on tracking changed properties.Gert Arnold
You are welcome. Please post self answer, I'm not a fan of direct usage of DDD model as EF Core data (storage) model.Ivan Stoev

1 Answers

3
votes

So here is what happened. As @Gert Arnold pointed out:

1. You need to have private setters on all properties of your domain models. EF Core can't work with read-only properties as of version 2.2.6.

But that was not my problem. It turned out I had forgotten to include a private constructor on the equivalent of MyClass in my own project. I just wish I had seen @Ivan Stoev's comment before I spent hours of work and figured it out. The error message that EF Core gave me was too cryptic, and didn't point out the issue:

No suitable constructor found for entity type 'MyClass'. The following constructors had parameters that could not be bound to properties of the entity type: cannot bind 'valueObject1', 'valueObject2' in 'MyClass(ValueObject valueObject1, ValueObject valueObject2)'.

When in reality, there is no problem with that particular constructor.

2. You just have to have a private, parameter-less constructor if you want EF Core to properly use constructor binding and feed values to your properties through constructor parameters. This is not the case. EF Core simply can't inject entities into other entities using constructor binding. It is basically telling us that particular constructor can't be used, and because it can't find a suitable constructor to use, at all, by providing a parameter-less constructor you are giving it a way to create objects without constructor binding.

3. You should use modelBuilder.OwnsOne<> in your DbContext.OnModelCreating and NOT modelBuilder.Property<> to configure Value Objects for an Entity (in DDD) to be stored in the same database table as the Entity.

I think EF Core needs to give you a clearer message about how it is confused as to which constructor it should use when you don't have a private, parameter-less constructor. I'll bring it up with the EF Core team.