I have a class Destination
with a constructor that takes parameters, and I'm trying to map a Source
object to Destination
using AutoMapper. The mapping for Id
works fine, but the mapping for InStore
doesn't seem to work when I use a custom mapping. Here is my code:
using AutoMapper;
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<Source, Destination>()
.ForMember(dest => dest.Id, opt => opt.MapFrom(src => int.Parse(src.Id)))
.ForMember(dest => dest.InStore, opt => opt.MapFrom(src => src.InStore == "1"));
});
var mapper = config.CreateMapper();
var source = new Source { Id = "123", InStore = "1" };
var destination = mapper.Map<Destination>(source);
Console.WriteLine($"Id: {destination.Id}, InStore: {destination.InStore}");
public class Source
{
public string Id { get; set; }
public string InStore { get; set; }
}
public class Destination
{
public int Id { get; private set; }
public bool InStore { get; private set; }
public Destination(int id, bool inStore)
{
Id = id;
InStore = inStore;
}
}
Throw exception:
AutoMapper.AutoMapperMappingException: 'Error mapping types.'
FormatException: String '1' was not recognized as a valid Boolean.
When I remove the parameterized constructor from Destination
, the mapping works correctly. Also, when var source = new Source { Id = "123", InStore = "true" };
is used, the mapping also works. These observations confuse me as to why the mapping fails with a constructor present and when InStore
is set to "1"
.
The expression tree is as follows:
(Source src, Destination dest, ResolutionContext ctxt) => {
Destination typeMapDestination;
return src == default(Source) ? default(Destination) : (
typeMapDestination = dest ?? new Destination((
string resolvedValue,
resolvedValue = false || src == null ? default(string) : src.Id,
Convert.ToInt32(resolvedValue)
), (
string resolvedValue,
resolvedValue = false || src == null ? default(string) : src.InStore,
Convert.ToBoolean(resolvedValue)
)),
if (dest != null) {
try {
(
string resolvedValue,
int propertyValue,
resolvedValue = false || src == null ? default(string) : src.Id,
propertyValue = Convert.ToInt32(resolvedValue),
typeMapDestination.Id = propertyValue
)
} catch (Exception ex) {
throw new AutoMapperMappingException("Error mapping types.", ex, #TypePair, #TypeMap, #PropertyMap);
}
},
if (dest != null) {
try {
(
string resolvedValue,
bool propertyValue,
resolvedValue = false || src == null ? default(string) : src.InStore,
propertyValue = Convert.ToBoolean(resolvedValue),
typeMapDestination.InStore = propertyValue
)
} catch (Exception ex) {
throw new AutoMapperMappingException("Error mapping types.", ex, #TypePair, #TypeMap, #PropertyMap);
}
},
try {
(
string resolvedValue,
resolvedValue = try {
false || src == null ? default(string) : src.Id;
} catch (NullReferenceException) {
default(string);
} catch (ArgumentNullException) {
default(string);
},
typeMapDestination.Name = resolvedValue
)
} catch (Exception ex) {
throw new AutoMapperMappingException("Error mapping types.", ex, #TypePair, #TypeMap, #PropertyMap);
},
typeMapDestination
);
}
It seems like ForMember
isn't working as expected. How should I modify it to achieve the desired results?
(as at AutoMapper 13.0.1)
Seems that constructor parameter matching simply does NOT use custom ForMember
mappings.
Further simplifying the example:
var mapper = new MapperConfiguration(cfg =>
cfg.CreateMap<Source, Destination>()
.ForMember(dest => dest.Id, opt => opt.MapFrom(
src => int.Parse(src.Id) * 100 // Custom expression
))
).CreateMapper();
var dest = mapper.Map<Destination>(new Source("1"));
Console.WriteLine($"{dest.Id}"); // =1. Should be 100.
public record Source(string Id);
public class Destination {
public int Id;
public Destination(int id) => Id = id;
}
The exec plan reflects that the id
param is mapped using Convert.ToInt32()
instead of the custom ForMember
expression.
To get around it you have to manually specify a ForCtorParam
expression as well in the mapping, or completely disable constructor mapping.
.ForCtorParam("id", opt => opt.MapFrom(src => int.Parse(src.Id) * 100))
// or
.DisableConstructorMapping()
It's not ideal, and I don't know if this is intended behaviour or a bug.