vkhorikov / DddAndEFCore

Source code for the DDD and EF Core Pluralsight course
https://enterprisecraftsmanship.com/ps-ef-core
MIT License
249 stars 90 forks source link

Question: How to model complex and nested structure of VOs (with more than one level of nesting VOs) in EF Core with Fluent API? #6

Open Synergia503 opened 3 years ago

Synergia503 commented 3 years ago

Hi Vladimir! Thanks for your course, it's really great with amazing knowledge! I would like to ask you about a quite complex (IMO) case. Let's say I have a bunch of Value Objects (I removed some of the nested VOs and I removed business logic from all of them as well to make things clear):

public class Area : ValueObject
{
    private static readonly IReadOnlyList<AreaUnit> _supportedUnits =
        new List<AreaUnit>() { AreaUnit.SquareCentimeter, AreaUnit.SquareMeter };

    public AreaUnit Unit { get; private set; }

    public DecimalValue AreaValue { get; }

    public DecimalSeparator DecimalSeparator { get; }

    public Area(AreaUnit unit, DecimalValue areaValue, DecimalSeparator decimalSeparator)
        // Validation

    // GetEqualityComponents    
}

public class AreaUnit : ValueObject
{
    public static readonly AreaUnit SquareMeter
        = new AreaUnit(AreaUnitAbbrevation.SquareMeter, AreaUnitFullName.SquareMeter);

    public static readonly AreaUnit SquareCentimeter
        = new AreaUnit(AreaUnitAbbrevation.SquareCentimeter, AreaUnitFullName.SquareCentimeter);

    public AreaUnitAbbrevation Abbreviation { get; private set; }
    public AreaUnitFullName FullName { get; private set; }

    public AreaUnit(AreaUnitAbbrevation abbreviation, AreaUnitFullName fullName)
        // Validation

    // GetEqualityComponents
}

public class AreaUnitAbbrevation : ValueObject
{
    private static readonly List<string> _supportedUnitAbbrevations = new List<string>() { "m2", "cm2" };

    public static readonly AreaUnitAbbrevation SquareMeter = new AreaUnitAbbrevation("m2");
    public static readonly AreaUnitAbbrevation SquareCentimeter = new AreaUnitAbbrevation("cm2");

    public string Value { get; private set; }

    public AreaUnitAbbrevation(string abbrevation)
        // Validation       

    // GetEqualityComponents
    // implicit operators
}

public class AreaUnitFullName : ValueObject
{
    private readonly List<string> _supportedUnitFullNames = new List<string>() { "square meter", "square centimeter" };

    public string Value { get; private set; }

    public AreaUnitFullName(string fullName)
        // Validation

    // GetEqualityComponents
    // implicit operators
}

public class DecimalValue : ValueObject
{
    public uint ValueBeforeSeparator { get; private set; }
    public uint ValueAfterSeparator { get; private set; }

    public DecimalValue(int valueBeforeSeparator, int valueAfterSeparator)       
        // Validation

     // GetEqualityComponents
}

public class DecimalSeparator : ValueObject
{
    private readonly IReadOnlyList<char> _allowedSeparators = new List<char>() { '.', ',' };

    public char Value { get; private set; }

    public DecimalSeparator(char separator)
        // Validation

    // GetEqualityComponents
    // implicit operators
}

public class Apartment : AggregateRoot
{
    // some Entities, more VOs, business methods, etc.

    public Area Space { get; }

    // some Entities, more VOs, business methods, etc.
}

// EF Core part
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Apartment>(a =>
    {
        a.ToTable("Apartment").HasKey(p => p.EntityId);
        a.OwnsOne(p => p.Space, o =>
        {
            // Is that below even possible - more than simple property access (I mentioned that problem a little bit below)?
            o.Property(oo => oo.Unit).HasColumnName("AreaUnit").OwnsOne(???? we have here more nested VO like AreaUnitFullName and AreaUnitAbbrevation);
            o.Property(oo => oo.AreaValue).HasColumnName("AreaValue").HasConversion(...some conversion here...);
            o.Property(oo => oo.DecimalSeparator).HasColumnName("DecimalSeparator").HasConversion(x=>x.subVO.subVO.value ??? let's assume we want get only one property from subVO and make conversion here for whole tree of nested VOs);
            o.Property(oo => oo.NestedVO.NestedVO).HasColumnName(...).HasConversion(...)
        });
    });
} 

How could I model this structure of VOs for EF Core DbContext in OnModelCreating() methods? I tried several things and I had been only getting errors like this: https://stackoverflow.com/questions/54328216/ef-core-modelbuilder-not-a-valid-expression-fluent-api but of course my error was not exactly like this and the answer from this link is not the solution for me. I mean, I've just got an exception similar to "The expression 'a => a.xyz' is not a valid property expression. The expression should represent simple property access: 't => t.MyProperty'.". I see of course this extract: "simple property access" thus my question here, because I have a complex structure of VOs with more nested VOs which equals in the end in more than simple property access requirements. Have you ever designed such a complex and deep structure of VOs? With EF Core, am I only able to have one level of nested VOs?

To make thing more generic, this kind of structure does not work for me with EF Core Fluent API:

class ParentAggregateRoot
{
    Entity1;
    Entity2;
    ValueObject1;
    ValueObject2;
    ValueObject3;
    ValueObject4;
    ValueObject5;
}

class ValueObject1
{
    ValueObject1a;
    ValueObject1b;
    ValueObject1c;
}

class ValueObject1a
{
    ValueObject1aa;
    ValueObject1ab;
    ValueObject1ac;
}
class ValueObject1b
{
    ValueObject1ba;
    ValueObject1bb;
    ValueObject1bc;
}

class ValueObject1aa
{
    ValueObject1aaa
    ValueObject1ab
    ValueObject1d
    Entity3
}

OR it could work, it can be modeled but I just don't know how to properly use OwnsOne() methods and other methods from EF Core Fluent API? Do you know how one can solve this kind of complex structure with more levels of nested VOs and Entities? Maybe am I doing something wrong in Domain Modeling, like there should be more Entities instead of VOs? For my non-generic example, VOs: AreaUnit, DecimalValue, DecimalSeparator (and more nested VOs with more nested VOs, etc.) have to be Entities (or at least some of them)? The problem is they are not Entities in my Domain, but maybe to workaround EF Core, they should be Entities? Or maybe I have to disentangle nested VOs and move them up? But then, I would end up with a class with about 1000 lines - so much logic I have inside all the nested VOs. Hopefully, I've presented my issue clearly, if not, I'll try to give more clues, just let me know. Thank you very very much in advance!

vkhorikov commented 3 years ago

I haven't tried this with EF Core. Try to configure each value object separately, it might work this way: https://docs.microsoft.com/en-us/ef/core/modeling/owned-entities#nested-owned-types

Regarding the domain model itself -- it looks good, except for one thing. Looks like some of the VOs are interdependent. It's best to represent them as enumerations where you hard-code the dependency instead of allowing the clients to manually tie one VO to another. Here's an example from my recent project: https://gist.github.com/vkhorikov/8b966da515928a6f96f2d7fa28bcd72f In your case, the inheriting VOs will return other VOs instead of primitive types like in my case.

Converting some VOs into entities may also help to work around these issues, but try the above link first.

And of course, an obligatory reference: NHibernate doesn't have any of these issues and supports nested VOs with no issues.