DDD - where to put entity/contract/DTO mappings?

1.4k Views Asked by At

I have an API with response and request contracts (DocumentRequest/DocumentResponse), a domain object (Document), and dtos (DocumentDTO). I have to map the requestContract -> domain object -> dto for requests and vice versa (dto -> domain object -> responseContract) for responses. My question is where do I put these mapping functions, in an external Mappers class or inside the Document object itself?

Example of this Document class:

    public Guid DocumentGuid { get; private set; }

    public string Title { get; private set; }

    public Uri DocumentUrl { get; private set; }

    public Guid LastModifiedBy {get; private set; }

    public DateTime? DateDeleted {get; private set; }

    public Document(DocumentRequest request)
    : this(Guid.NewGuid(), request.Title, request.DocumentUrl)
    { }

    public Document(Guid documentGuid, string title, Uri url)
    {
        if (documentGuid == Guid.Empty)
            throw Exception();

        if (string.IsNullOrWhiteSpace(title))
            throw Exception();

        if (url == default)
            throw Exception();

        DocumentGuid = documentGuid;
        Title = title;
        DocumentUrl = url;
    }

    public void UpdateFromRequest(UpdateDocumentRequest request, Guid userId)
    {
        if (userId == Guid.Empty)
            throw Exception();
        if (string.IsNullOrWhiteSpace(request.Title))
            throw Exception();
        if (request.DocumentUrl == default)
            throw Exception();

        Title = request.Title;
        DocumentUrl = request.DocumentUrl;
        LastModifiedBy = userId;
    }

In the Document object I am trying to add behaviors, guard clauses, etc, and so am privately setting everything. Currently we have an external mapping class with functions that do the mapping through Document constructors, but now I want to set add a DateDeleted property from the dto -> domain.

So my problem is I am trying to accomplish this mapping and keep my control over the initialization restricted to the class itself; there is no reason for someone to set the DateDeleted property, I am just trying to get this value out to a response contract. It seems like I can either make another Document constructor just for this external mapping class (which will be public, which doesn't make sense here), or move mappings to be inside the entity.

Is it normal to have mappings in the entity? If there is a better way to be doing all this, please let me know! TY

1

There are 1 best solutions below

5
Corey Sutton On BEST ANSWER

It is generally not recommended to have mappings in the entity class itself as it can violate the Single Responsibility Principle (SRP). An entity should be responsible for representing its state and behaviors, while mapping is a separate thing entirely.

Instead, it is common to create a separate class or classes for mapping. This can be done using a mapper library or by writing custom mapper methods. These mapping classes can be organized in a separate "Mapper" or "Mapping" package within your project.

In your case, you can create a mapper class that converts the DocumentDTO to the Document domain object, and vice versa. The mapper can be responsible for adding the DateDeleted property to the domain object as well. The mapper can then be used by the API's request/response handling logic to convert between the request/response contracts and the domain object.

Here is an example of what the mapper class might look like:

public class DocumentMapper {
    public static Document FromDto(DocumentDTO dto) {
        if (dto == null) {
            return null;
        }
        return new Document(dto.Title, dto.DocumentUrl, dto.DateDeleted);
    }

    public static DocumentDTO ToDto(Document document) {
        if (document == null) {
            return null;
        }
        return new DocumentDTO {
            Title = document.Title,
            DocumentUrl = document.DocumentUrl,
            DateDeleted = document.DateDeleted
        };
    }
}

With this approach, the Document entity can remain focused on its own behavior, while the mapping logic can be encapsulated in a separate class.

Edit:

To further clarify based on the edit to your question:

If you want to keep DateDeleted private/inaccessible until it exists in the documentDTO: you could create a separate method in the Document class to set the DateDeleted property, which can be called by the mapper during the mapping process. This way, you can keep the DateDeleted property private and only set it when it exists in the DTO.

public class Document {
    public Guid DocumentGuid { get; private set; }
    public string Title { get; private set; }
    public Uri DocumentUrl { get; private set; }
    public Guid LastModifiedBy { get; private set; }
    private DateTime? dateDeleted;
    public bool IsDeleted { get { return dateDeleted.HasValue; } }

    public Document(DocumentRequest request)
        : this(Guid.NewGuid(), request.Title, request.DocumentUrl)
    { }

    public Document(Guid documentGuid, string title, Uri url)
    {
        if (documentGuid == Guid.Empty)
            throw Exception();

        if (string.IsNullOrWhiteSpace(title))
            throw Exception();

        if (url == default)
            throw Exception();

        DocumentGuid = documentGuid;
        Title = title;
        DocumentUrl = url;
    }

    public void UpdateFromRequest(UpdateDocumentRequest request, Guid userId)
    {
        if (userId == Guid.Empty)
            throw Exception();
        if (string.IsNullOrWhiteSpace(request.Title))
            throw Exception();
        if (request.DocumentUrl == default)
            throw Exception();

        Title = request.Title;
        DocumentUrl = request.DocumentUrl;
        LastModifiedBy = userId;
    }

    public void SetDateDeleted(DateTime? dateDeleted)
    {
        this.dateDeleted = dateDeleted;
    }
}

Here is how the mapper class could look:

public class DocumentMapper {
    public static Document FromDto(DocumentDTO dto)
    {
        if (dto == null) {
            return null;
        }
        var document = new Document(dto.Title, dto.DocumentUrl);
        document.SetDateDeleted(dto.DateDeleted);
        return document;
    }

    public static DocumentDTO ToDto(Document document)
    {
        if (document == null) {
            return null;
        }
        return new DocumentDTO {
            Title = document.Title,
            DocumentUrl = document.DocumentUrl,
            DateDeleted = document.IsDeleted ? document.dateDeleted : null
        };
    }
}

If you want to go even further, you can define a method like ApplyDto to take a dbDocument object as a param and use it to set the properties of the current Document object. This would effectively create a private method, since it is not directly accessible outside of the Document class! However it would still let you map from a DocumentDTO to a Document object and set the DateDeleted prop, but only if it exists in the DTO.