Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

EF Core and DDD: Store ValueObjects in the same table as Entities, also use parametrized constructors to set property values on Entities

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?

like image 991
TheAgent Avatar asked Aug 12 '19 19:08

TheAgent


1 Answers

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.

like image 65
TheAgent Avatar answered Sep 18 '22 17:09

TheAgent