Home > Back-end >  AutoMapper fails to map record with embedded object to record
AutoMapper fails to map record with embedded object to record

Time:11-24

I have a record AuthResult with a class object User in it:

public record AuthResult(
    User User,
    string Token);


public class User
{
    public Guid Id { get; set; } = Guid.NewGuid();
    public string UserName { get; set; } = null!;
    public string Email { get; set; } = null!;
    public string Password { get; set; } = null!;
}

Which I want to map to a flat record AuthResponse:

public record AuthResponse(
    Guid Id,
    string UserName,
    string Email,
    string Token);

Mapping:

_mapper.Map<AuthResponse>(authResult)

Configurations I tried:

CreateMap<User, AuthResponse>();

// Gives "AuthResponse does not have a matching constructor with a parameter named 'Token'" exception
CreateMap<AuthResult, AuthResponse>()
    .ForCtorParam(ctorParamName: nameof(AuthResponse.Token), opt => opt.MapFrom(src => src.Token))
    .AfterMap((src, dest, context) => context.Mapper.Map(src.User, dest));

// Gives "AuthResponse needs to have a constructor with 0 args or only optional args."
CreateMap<AuthResult, AuthResponse>().IncludeMembers(src => src.User);

// Gives "AuthResponse needs to have a constructor with 0 args or only optional args."
CreateMap<AuthResult, AuthResponse>()
    .ConvertUsing((wrapper, destination, context) =>
        context.Mapper.Map<AuthResponse>(wrapper.User));

Am I missing something or that impossible to do with the AutoMapper?

CodePudding user response:

The other answer does work. It also requires you to change your mapping every time the record or class changes their structure/constructors.

If you want to create the map once, and never touch it again even when your entities change, I can think of two alternatives:

  1. Match the properties in your record with the nested class, like so:
// Include the word 'User' to reflect the nested AuthResult.User
public record AuthResponse(
    Guid UserId, 
    string UserUserName,
    string UserEmail,
    string Token);

// This makes the mapping extremely straightforward:
cfg.CreateMap<AuthResult, AuthResponse>();
  1. Or, allow your record to be created with a 0-arg constructor (respecting the error you saw)
public record AuthResponse(
    Guid Id = default,
    string UserName = default!,
    string Email = default!,
    string Token = default!);

// This allows you to use the following
cfg.CreateMap<User, AuthResponse>();
cfg.CreateMap<AuthResult, AuthResponse>()
   .IncludeMember(src => src.User);
   /* If the above doesn't work, try instead: */
   //.AfterMap((src, dest, context) => context.Mapper.Map(src.User, dest));

The 2nd option will likely be negligibly less efficient.

CodePudding user response:

I tried the following code, and it seems to work,

IConfigurationProvider configuration = new MapperConfiguration(cfg =>
            cfg.CreateMap<AuthResult, AuthResponse>()
               .ForCtorParam(ctorParamName: nameof(AuthResponse.Id), m => m.MapFrom(s => s.User.Id))
               .ForCtorParam(ctorParamName: nameof(AuthResponse.UserName), m => m.MapFrom(s => s.User.UserName))
               .ForCtorParam(ctorParamName: nameof(AuthResponse.Email), m => m.MapFrom(s => s.User.Email)));

var mapper = configuration.CreateMapper();

var result = mapper.Map<AuthResponse>(new AuthResult(new User { UserName = "Test", Email = "sas" }, "token"));
  • Related