skip to Main Content

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:UsersjoseMusicgithubProjetosBookVerseCatalogContextsrcCatalogContext.ApplicationCommonMappingsAuthorProfile.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.

2

Answers


  1. At first I thought that this answer could help you, but your requirement has the additional challenge of the target AuthorUpdateCommand record not having a default/parameterless constructor.

    The solution in the linked answer would work in case AuthorUpdateCommand would have a default constructor, but fails here on instantiating an AuthorUpdateCommand because of the tuple member names not matching the constructor parameter names.

    One way to solve all this is by using ForCtorParam as shown below.

    public class AuthorProfile : Profile
    {
        public AuthorProfile()
        {
            CreateMap<(Guid Id, AuthorRequest AuthorRequest), AuthorUpdateCommand>()
                .ForCtorParam(nameof(AuthorUpdateCommand.Id),
                    opt => opt.MapFrom(src => src.Id))
                .ForCtorParam(nameof(AuthorUpdateCommand.Name),
                    opt => opt.MapFrom(src => src.AuthorRequest.Name))
                .ForCtorParam(nameof(AuthorUpdateCommand.Biography),
                    opt => opt.MapFrom(src => src.AuthorRequest.Biography))
                .ForCtorParam(nameof(AuthorUpdateCommand.DateOfBirth),
                    opt => opt.MapFrom(src => src.AuthorRequest.DateOfBirth));
        }
    }
    

    As an alternative — and following up on the comments below — you can use IncludeMembers as shown in above linked answer, but the target models/records need a default constructor and an additional mapping; CreateMap<AuthorRequest, AuthorUpdateCommand>().

    public class AuthorProfile : Profile
    {
        public AuthorProfile()
        {
            CreateMap<(Guid Id, AuthorRequest AuthorRequest), AuthorUpdateCommand>()
                .ForMember(o => o.Id, o => o.MapFrom(src => src.Id))
                .IncludeMembers(o => o.AuthorRequest);
            CreateMap<AuthorRequest, AuthorUpdateCommand>();
        }
    }
    
    public record AuthorUpdateCommand : AuthorCommand, IRequest<Result<Updated>>
    {
        public string Biography { get; init; } = default!;
    }
    
    public record AuthorCommand
    {
        public Guid Id { get; init; }
        public string Name { get; init; } = default!;
        public DateTime DateOfBirth { get; init; }
    }
    
    Login or Signup to reply.
  2. 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);
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search