Reputation: 341
I am mapping with automapper 10.1.1 in c# 9 from this class
public record BFrom
{
public Guid Id { get; init; }
public Guid DbExtraId { get; init; }
}
into this
public record ATo(Guid Id, Guid ExtraId) : BaseTo(Id);
And have the following configuration
CreateMap<BFrom, ATo>()
.ForMember("ExtraId", opt => opt.MapFrom(src => src.DbExtraId))
.ReverseMap();
But I have a problem when I am trying to map it throws an exception with a message that needs to have a constructor with 0 args or only optional args. Is it possible to fix this problem without changing the record types?
Upvotes: 24
Views: 19259
Reputation: 3965
I didn't see the answer that fixed the problems for me: I simply replaced "ForMember" by "ForCtorParm".
You need to provide the constructor parameter as a string, so that becomes:
CreateMap<BFrom, ATo>()
.ForCtorParam("ExtraId", opt => opt.MapFrom(src => src.DbExtraId))
.ForCtorParam("Id", opt => opt.MapFrom(src => default(Guid)));
There's a few things to notice:
You could make your life easier by taking advantage of the fact that in practice the generated constructor parameters and the properties always have the same names/types. You can add an extension method.
public static class AutoMapperExtensions
{
public static IMappingExpression<TSource, TDestination> ForCtorParm<TSource, TDestination, TMember>(
this IMappingExpression<TSource, TDestination> mappingExpression,
Expression<Func<TDestination, TMember>> destinationMember,
Action<ICtorParamConfigurationExpression<TSource>> paramOptions)
{
var memberInfo = FindProperty(destinationMember);
var memberName = memberInfo.Name;
return mappingExpression
.ForCtorParam(memberName, paramOptions);
}
private static MemberInfo FindProperty(LambdaExpression lambdaExpression)
{
Expression expressionToCheck = lambdaExpression.Body;
while (true)
{
switch (expressionToCheck)
{
case MemberExpression { Member: var member, Expression: { NodeType: ExpressionType.Parameter or ExpressionType.Convert } }:
return member;
case UnaryExpression { Operand: var operand }:
expressionToCheck = operand;
break;
default:
throw new ArgumentException(
$"Expression '{lambdaExpression}' 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.",
nameof(lambdaExpression));
}
}
}
}
This allows you to do things a bit more type safe:
CreateMap<BFrom, ATo>()
.ForCtorParam(dest => dest.ExtraId, opt => opt.MapFrom(src => src.DbExtraId))
.ForCtorParam(dest => dest.Id, opt => opt.MapFrom(src => default(Guid)));
Upvotes: 1
Reputation: 1066
Use a more static approach.. we know the record constructor name..
public record ATo(Guid Id, Guid ExtraId) : BaseTo(Id);
CreateMap<BFrom, ATo>()
.ForMember(nameof(ATo.Extra), opt => opt.MapFrom(src => src.DbExtraId))
.ReverseMap();
Records are really just syntax sugar for a concept that's been around forever, that is they offer immutable properties. Records actually make AutoMapper with the old concept easier, and safer because with the new syntax we know what the property names will be, i.e record Example(string SomeProperty)
. The compiler will just create a constructor like so,
public class Example
{
public string SomeProperty { get; init; }
Example(string SomeProperty)
{
this.SomeProperty = SomeProperty;
}
}
Making an empty constructor wont help you because the properties are init
only, and you cant really use object initializer because you have to use the constructor... and well object initializers are a special case for init
properties, that is they allow the properties to change even though object initializers are really just syntax sugar themselves. There is not really a way via reflection to use object initializers, so that loophole is gone for libraries like AutoMapper, the loophole being..
new Example(default){SomeProperty = "SomeValue"}
.
Now of course you can use reflection to change readonly/private/internal properties, which I am not sure if AutoMapper supports or not, I hope it doesn't because its not a very good thing to do for reasons well documented on the internet.
Upvotes: 1
Reputation: 2880
What you need, is to specify the constructor-parameter by name in your mapping profile like so public AToProfile() => CreateMap<BFrom, ATo>().ForCtorParam(ctorParamName: "ExtraId", m => m.MapFrom(s => s.DbExtraId)).ReverseMap();
That way, AutoMapper will know, from where the value should be taken and will map accordingly.
public class StackoverflowQuestionTest
{
[Fact]
public void BFrom_MapTo_ATo()
{
// Arrange
IConfigurationProvider configuration = new MapperConfiguration(cfg => cfg.AddProfile<AToProfile>());
var source = new BFrom {Id = Guid.NewGuid()};
// Act
var target = new Mapper(configuration).Map<ATo>(source);
// Assert
target.Id.Should().Be(source.Id);
}
}
public record BaseTo (Guid Id); // this is an assumption, as you did not specify BaseTo in you question
public record BFrom
{
public Guid Id { get; init; }
public Guid DbExtraId { get; init; }
}
public record ATo(Guid Id, Guid ExtraId) : BaseTo(Id);
public class AToProfile : Profile
{
public AToProfile() =>
CreateMap<BFrom, ATo>()
.ForCtorParam(ctorParamName: "ExtraId", m => m.MapFrom(s => s.DbExtraId))
.ReverseMap();
}
Upvotes: 14
Reputation: 210
I had the same issue, and ended up creating this extension method to solve it:
public static class AutoMapperExtensions
{
public static IMappingExpression<TSource, TDestination> MapRecordMember<TSource, TDestination, TMember>(
this IMappingExpression<TSource, TDestination> mappingExpression,
Expression<Func<TDestination, TMember>> destinationMember, Expression<Func<TSource, TMember>> sourceMember)
{
var memberInfo = ReflectionHelper.FindProperty(destinationMember);
string memberName = memberInfo.Name;
return mappingExpression
.ForMember(destinationMember, opt => opt.MapFrom(sourceMember))
.ForCtorParam(memberName, opt => opt.MapFrom(sourceMember));
}
}
Then you simply use it like this:
CreateMap<BFrom, ATo>()
.MapRecordMember(a => a.ExtraId, src => src.DbExtraId)
.ReverseMap();
and it will take care of registering both constructor and member so that you don't run into issues like these.
Upvotes: 9
Reputation: 26352
Try
CreateMap<BFrom, ATo>().DisableCtorValidation()
.ForMember("ExtraId", opt => opt.MapFrom(src => src.DbExtraId))
.ReverseMap()
Upvotes: 4