3

I faced the issue of mapping the database entity to the domain entity in DDD.

Here are the details (simplified).


Domain layer with domain aggregate (entity) and the interface for the repository of this aggregate.

Domain aggregate (contains domain logic)

public class DomainBankAccount
{
    public DomainBankAccount(int id, DateTimeOffset openedAt)
    {
        if (openedAt > DateTimeOffset.Now)
            throw new ArgumentOutOfRangeException(nameof(openedAt),
                                                  "Bank account opened date shouldn't be in the future");
        OpenedAt = openedAt;
    }

    public int Id { get; }
    public DateTimeOffset OpenedAt { get; }
    public DateTimeOffset? ClosedAt { get; private set; }

    public bool IsClosed => ClosedAt.HasValue;

    public void Close()
    {
        if (IsClosed)
            throw new InvalidOperationException($"Bank account '{AccountNumber.Value}' is already closed");

        ClosedAt = DateTimeOffset.Now;
    }
}

Domain repository interface

public interface IRepository
{    
    Task<DomainBankAccount> GetAsync(int id, CancellationToken cancellation);
}

Now, making the implementation of the IRepository interface I need to:

  1. Retrieve the database entity from the storage
  2. Map database entity to domain entity

And here is the place where the problems come. Because all I want - is to map data from database table to domain object. But domain object contains logic and prohibit plain creation with all fields initialization (ClosedAt can't be straightforward initialized via domain constructor).

Repository interface implementation (in the infrastructure\persistence layer)

var dbEntity = new
        {
            Id = 1,
            OpenedAt = DateTimeOffset.Parse("2023-03-17"),
            ClosedAt = DateTimeOffset.Parse("2023-03-18")
        };
var result = new DomainBankAccount(
                               dbEntity.Id,
                               dbEntity.OpenedAt);

var closedAt = result.ClosedAt; // Still null 

So, I can't set the ClosedAt property in the domain object directly -> can't map it exactly from the database.


How can I map the database object to the domain object without violating the domain logic and encapsulation and without bringing any logic into the mapper (repository)?

Any ideas would be very appreciated. Thanks!

2 Answers 2

1

You will either have to:

  1. change the entity to have a public constructor which allows setting all the data, and mark that constructor with documentation that it's for data construction and shouldn't be used elsewhere. I don't advise this approach.
  2. Use some kind of factory which uses reflection to fill the properties of the entity. I don't advise this approach either.
  3. Use a object-database mapper library like Entity Framework Core which will do that automatically for you. You can query the domain entity directly from the database inside your Infrastructure layer. You only need to add private setters for every property, and to have a private default constructor which the framework will use to instantiate your object and then fill it using reflection.

A simple example of point 3's changes needed for your entity to work with EFCore:

public class DomainBankAccount
{
    // Properties
    public int Id { get; private set; }
    public DateTimeOffset OpenedAt { get; private set; }
    public DateTimeOffset? ClosedAt { get; private set; }

    // Computed properties
    public bool IsClosed => ClosedAt.HasValue;

    // Constructors
    public DomainBankAccount(DateTimeOffset openedAt)
    {
        if (openedAt > DateTimeOffset.Now)
            throw new ArgumentOutOfRangeException(nameof(openedAt),
                                                    "Bank account opened date shouldn't be in the future");
        OpenedAt = openedAt;
    }

    /// <summary>Constructor for EntityFrameworkCore.</summary>
    /// <remarks>EntityFrameworkCore chooses the constructor with the least amount of parameters (including 0) where all of them correspond to a property.</remarks>
    private DomainBankAccount() { }

    // Methods
    public void Close()
    {
        if (IsClosed)
            throw new InvalidOperationException($"Bank account '{AccountNumber.Value}' is already closed");

        ClosedAt = DateTimeOffset.Now;
    }
}

I've removed the id from the constructor as a showcase that if you don't set the id upon creation of the entity, the framework will autogenerate it for you when inserting it into the database.

Sign up to request clarification or add additional context in comments.

Comments

0

@ErroneousFatality, thanks for the suggestions!

I solved the issue by reflection keeping some restrictions in mind.

var dbEntity = new
        {
            Id = 1,
            OpenedAt = DateTimeOffset.Parse("2023-03-17"),
            ClosedAt = DateTimeOffset.Parse("2023-03-18")
        };
var result = new DomainBankAccount(
                               dbEntity.Id,
                               dbEntity.OpenedAt);

// that's the way to set the property value 
// only in the mapper - no violation of domain logic
var closedAtProp = typeof(DomainBankAccount).GetProperty(nameof(DomainBankAccount.ClosedAt));
closedAtProp!.SetValue(result, dbEntity.ClosedAt);

Here is the benefit of this approach:

  1. Domain class still has no changes. No extra private constructors for the Entity Framework -> no implicit dependencies of infrastructure in the domain layer (e.g. I can use any ORM library)

  2. Set the property only in mapper. Just the place to map from database object to domain -> no violations of domain rules.

  3. Domain object still has only one way to initialize -> via his constructor. No factories, and no special ways to init just for mapping.

Hope this helps someone!

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.