Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Automapper refer to existing subtype mapping in custom member mapping

Tags:

c#

automapper

My database entities look as follows (redacted down to relevant information):

public class Task
{
    public Guid? StatusId { get; set; }
    public Status Status { get; set; }

    public DateTime? StatusLastModified { get; set; }
}

public class Status
{
    public string Name { get; set; }
    // several other properties redacted
}

Tasks initially have no status (StatusLastModified is then also null), and once the first status has been assigned, tasks will always have a status (and StatusLastModified is then also not null).

For the sake of this question, you can assume that these properties will always both be null or not-null, I don't need to account for anomalies where one is null and the other isn't.

For my DTOs, I want to map this to the following structure:

public struct TaskDto
{
    public TaskStatusDto? Status { get; set; }
}

public struct TaskStatusDto
{
    public DateTime Timestamp { get; set; } // maps from Task.StatusLastModified 
    public StatusDto Status { get; set; } // maps from Task.Status
}

public struct StatusDto
{
    public string Name { get; set; }
    // and all the other properties from the Status entity
}

The goal here is that my TaskDto only has an underlying TaskStatusDto when there is an actual status, and otherwise it should be null. This is helpful as it negates the need to constantly null check both the status object and the last modified date.

However, I am struggling to configure this mapping in Automapper. I have created the mapping between Status and StatusDto:

configuration
     .CreateMap<Status, StatusDto>();

My real mapping is more complicated but it's also proven to work so you can assume that this is a valid and working mapping.

The problem I'm faced with is how I can refer to this mapping when creating the custom mapping between Task and TaskDto. I've currently tried the following:

configuration
    .CreateMap<Task, TaskDto>()
    .ForMember(
            dto => dto.Status,
            opt => opt.MapFrom(entity => 
                entity.Status == null
                    ? null as TaskStatusDto?
                    : new TaskStatusDto()
                    {
                        TimeStamp = entity.StatusLastModified.Value,
                        Status = // ??? make me a StatusDto from entity.Status
                    }
        ));

The part I don't know how to fill in is the comment. I have access to the original entity (entity.Status), but I don't know how to tell Automapper to convert this object to a StatusDto according to a mapping that it should already know.

My approach also feels more contrived than it should be, but I don't know how to otherwise introduce this intermediary TaskStatusDto object if it isn't backed by an actual entity in my source data.


A small footnote, unsure if relevant: I am using Heroic.Automapper, which means that the TaskDto and StatusDto maps are being configured in separate locations (i.e. inside the TaskDto and StatusDto class, respectively) and the Heroic framework will combine all these maps at runtime.

public class TaskDto : IMapFrom<Task>, IHaveCustomMappings
{
     public void CreateMappings(IMapperConfigurationExpression configuration)
     {
         // mapping from Task to TaskDto
     }
}

public class StatusDto : IMapFrom<Status>, IHaveCustomMappings
{
     public void CreateMappings(IMapperConfigurationExpression configuration)
     {
         // mapping from Status to StatusDto
     }
}

However, as far as I understand it, the setting up of the map itself isn't Heroic-specific and should be purely Automapper syntax.

like image 347
Flater Avatar asked Sep 06 '25 03:09

Flater


1 Answers

I think a cleaner solution would be to create your own CustomResolver:

public class TaskStatusDtoResolver : IValueResolver<Task, TaskDto, TaskStatusDto?>
{
    public TaskStatusDto? Resolve(Task source, TaskDto destination, TaskStatusDto? member, ResolutionContext context)
    {
        if (source.Status == null)
        {
            return null;
        }

        return new TaskStatusDto
        {
            Status = context.Mapper.Map<StatusDto>(source.Status),
            Timestamp = source.StatusLastModified.Value
        };
    }
}

And you can use the following configuration:

cfg.CreateMap<Status, StatusDto>();
cfg.CreateMap<Task, TaskDto>()
    .ForMember(dest => dest.Status, opt => opt.MapFrom<TaskStatusDtoResolver>());

See the working example here


Alternatively you can define all the mappings in the configuration:

var configuration = new MapperConfiguration(cfg =>
{
    cfg.CreateMap<Status, StatusDto>();
    cfg.CreateMap<Task, TaskDto>().ForMember(
         dest => dest.Status,
         src => src.MapFrom((task, taskDto, member, context) =>
         {
             return task.Status == null ? null as TaskStatusDto? : new TaskStatusDto()
             {
                 Timestamp = task.StatusLastModified.Value,
                 Status = context.Mapper.Map<StatusDto>(task.Status)
             };
         }
     ));
});
like image 164
Hooman Bahreini Avatar answered Sep 09 '25 00:09

Hooman Bahreini