C# DDD with EF Core use same domain entity / aggregate for mapping table or separate class

96 Views Asked by At

I don't know which option is most suitable between those 2.

Option 1: Use aggregateroot or entity (aggregate sub-entity) to map table.

public class User : AggregateRoot 
{
    string Name { get; }
    string Email { get; }

    // Logic ...
}

And now configure EF Core entity:

public class EntityTypeConfiguration : IEntityTypeConfiguration<User> 
{
    public void Configure(EntityTypeBuilder<User> builder) 
    {
        // ... map props.
    }
}

Option 2: duplicate code and create 2 classes but with own scope.

public class User : AggregateRoot 
{
    string Name { get; }
    string Email { get; }

    // Logic ...
}

public class UserTable 
{
    Guid Id { get; }
    string Name { get; }
    string Email { get; }

    // No logic here ...
}
public class EntityTypeConfiguration : IEntityTypeConfiguration<UserTable> 
{
    public void Configure(EntityTypeBuilder<UserTable> builder) 
    {
        // ... map props.
    }
}

With second option, I can keep my entities in same folder for example EFCoreEntities and code should be cleaner, but with lots of duplicated code.

With second approach, I have to write extra mappers between UserTable and User (maybe using mapster / automapper)

For example, this would be IUserRepository with method:

// Here parameter is an User but have to map into UserTable
Add(User user) 
{
    var entity = user.MapToUserTable();
    dbContext.Users.Add(entity)
    // ...
}

Which option do you use, any advantages and disadvantages?

1

There are 1 best solutions below

0
Yorro On

This is a very good question that people rarely talk about. Many will tell you to abandon DDD altogether.

I've worked on a relatively large system using Option 2 with separate entities and domains. The following is my experience.

Option 2 PROS ✅:

  • Domain is not restricted to the database schema.
    • This was our original motivation. However, we NEVER encountered a situation where the domain deviated from the schema. It was always 1-to-1. This means the mapping became an additional "indirection" where the expected deviation never happened.

Option 2 CONS :

  • Tons of mapping code that gets complex real quick. We also started doing tons of unit testing just for the mapping.
  • Extra layer with no tangible benefit. If the mapping stays 1-to-1. This means the mapping became an additional "indirection" where the expected deviation never happened.
    • I also found the extra layer made it harder to navigate.

Option 1 is a cleaner approach BUT with two caveats

  1. First: Your domain becomes restricted by the database schema. Make sure your database schema is well-designed with proper normalization. If your team doesn't have a good background in database design, the domain becomes harder to change as it gets complex.

  2. Second: Referencing the 1-to-many navigational properties within itself. For example, if use the collection navigational properties for validation and you forget to load them, your validations will silently fail as there is no way to know from the domain side that the navigational properties are either loaded or not loaded. This adds a level of coupling between the domain and persistence, and the domain isn't supposed to care about persistence.

The solution for the second caveat is either

  1. Get comfortable with lazy loading
  2. Enable auto-loading from the IEntityTypeConfiguration.

Both loading options have different performance implications when it comes to scaling, but there are ways to mitigate this. Example: When fetching large data, apply projections when needed, use paginations, and consider pre-computations stored in the database.

If you ever decide to enable lazy loading, preemptively catch any "1-N select problem" as soon as they happen. Setup a performance monitoring, integration, and load testing

Lastly, with option 2, explore shadow properties and only expose fields that need to be exposed. Apply proper encapsulation so your entities/domain stays nice and pretty.

Its really about the trade offs.