Search code examples
c#asp.net.netautomapper

Issue with Tuple Mapping Using AutoMapper


Source/destination types

// Source
public record AuthorRequest(
    string Name,
    string Biography,
    DateTime DateOfBirth);

// Destination
public record AuthorUpdateCommand(
    Guid Id,
    string Name,
    string Biography,
    DateTime DateOfBirth
) : AuthorCommand(Name, Biography, DateOfBirth), IRequest<Result<Updated>>;

Mapping configuration

public class AuthorProfile : Profile
{
    public AuthorProfile()
    {
        CreateMap<(Guid Id, AuthorRequest AuthorRequest), AuthorUpdateCommand>()
            .ForMember(dst => dst, opt => opt.MapFrom(src => src.AuthorRequest))
            .ForMember(dst => dst.Id, opt => opt.MapFrom(src => src.Id));
    }
}

Version: 13.0.1

Expected behavior

I would like to do something similar that I do with the Mapster library to create a projection between a set of tuples and convert it to AuthorUpdateCommand. For example, I want something that, when I add a new field to AuthorRequest and AuthorUpdateCommand, I don't need to modify the mapping anymore.

Actual behavior

Exception has occurred: CLR/System.ArgumentException
An exception of type 'System.ArgumentException' occurred in AutoMapper.dll but was not handled in user code: 'Expression 'dst => dst' must resolve to top-level member and not any child object's properties. You can use ForPath, a custom resolver on the child type or the AfterMap option instead.'
   at AutoMapper.Internal.ReflectionHelper.FindProperty(LambdaExpression lambdaExpression)
   at AutoMapper.Configuration.MappingExpression`2.ForMember[TMember](Expression`1 destinationMember, Action`1 memberOptions)
   at CatalogContext.Application.Common.Mappings.AuthorProfile..ctor() in c:\Users\jose\Music\github\Projetos\BookVerse\CatalogContext\src\CatalogContext.Application\Common\Mappings\AuthorProfile.cs:line 16
   at System.RuntimeType.CreateInstanceDefaultCtor(Boolean publicOnly, Boolean wrapExceptions)

Steps to reproduce

I make a request to the controller. Just like I defined in the mapping, I am passing a tuple with Id and AuthorRequest. AuthorRequest has almost the same fields, except for the Id. That's why I am trying to create a projection.

public record AuthorRequest(
    string Name,
    string Biography,
    DateTime DateOfBirth);

[HttpPut("{id:guid}")]
    public async Task<IActionResult> Update(Guid id, AuthorRequest request)
    {
        var command = _mapper.Map<AuthorUpdateCommand>((id, request)); 

        var result = await _mediator.Send(command);

        if (result.IsError)
        {
            return BadRequest();
        }

        return Ok(result.Value);
    }

After that, it returns this exception exactly as described in the Actual behavior section.


Solution

  • Working code with constructors:

    void Main()
    {
        var config = new MapperConfiguration(cfg =>
        {
            cfg.ShouldUseConstructor = p => p.IsPublic;
            cfg.CreateMap<(Guid Id, AuthorRequest AuthorRequest), AuthorUpdateCommand>()
                .ForCtorParam(nameof(AuthorUpdateCommand.Id), opt => opt.MapFrom(src => src.Id))
                .IncludeMembers(src => src.AuthorRequest);
            cfg.CreateMap<AuthorRequest, AuthorUpdateCommand>(MemberList.None);
        });
        config.AssertConfigurationIsValid();
        //var expr = config.BuildExecutionPlan(typeof(IEnumerable<CollectionObject>), typeof(TestObject)).ToReadableString().Dump();
        var mapper = config.CreateMapper();
        try{
            mapper.Map<AuthorUpdateCommand>((Guid.NewGuid(), new AuthorRequest("name", "bio", DateTime.Now))).Dump();
        }catch(Exception ex) { ex.ToString().Dump(); }
    }
    public record AuthorRequest(string Name, string Biography, DateTime DateOfBirth);
    public record AuthorUpdateCommand(Guid Id, string Name, string Biography, DateTime DateOfBirth) : AuthorCommand(Name, DateOfBirth);
    public record AuthorCommand(string Name, DateTime DateOfBirth);