AutoMapper:映射记录类型出现问题

21

我正在使用C# 9中的AutoMapper 10.1.1对这个类进行映射。

public record BFrom 
{
    public Guid Id { get; init; }
    public Guid DbExtraId { get; init; }
}

变成这样

public record ATo(Guid Id, Guid ExtraId) : BaseTo(Id);

并具有以下配置

CreateMap<BFrom, ATo>()
    .ForMember("ExtraId", opt => opt.MapFrom(src => src.DbExtraId))
    .ReverseMap();

但是当我尝试进行映射时,出现了一个异常,需要一个没有参数或只有可选参数的构造函数。是否可能在不更改记录类型的情况下解决此问题?


1
https://docs.automapper.org/en/latest/Construction.html - Lucian Bargaoanu
5个回答

11
你需要做的是,在映射配置文件中通过名称指定构造函数参数,代码如下:public AToProfile() => CreateMap<BFrom, ATo>().ForCtorParam(ctorParamName: "ExtraId", m => m.MapFrom(s => s.DbExtraId)).ReverseMap(); 这样,AutoMapper 就会知道从哪里获取值并进行相应的映射。
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();
}

1
甚至不知道ForCtorParam存在,谢谢伙计。 - Adam Hardy

9

我曾经遇到过同样的问题,最终创建了这个扩展方法来解决它:

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));
    }
}

那么你只需要像这样使用它:

CreateMap<BFrom, ATo>()
    .MapRecordMember(a => a.ExtraId, src => src.DbExtraId)
    .ReverseMap();

它将负责注册构造函数和成员,以便您不会遇到这样的问题。


1
尝试使用MyGet构建。这已经不再需要了。 - Lucian Bargaoanu
谢谢@LucianBargaoanu, 很好知道。 - Christian Genne
@LucianBargaoanu 似乎仍然需要(已检查10.1.2-alpha.0.4、11.0.0和11.0.1-alpha.0.6) - Daniel Slapman
https://github.com/AutoMapper/AutoMapper/issues/3834 - Lucian Bargaoanu
最终我使用 object? 替换了 TMember,这样编译器在调用方法时可以隐式添加泛型类型。 - Hervé

3

请尝试

CreateMap<BFrom, ATo>().DisableCtorValidation()
    .ForMember("ExtraId", opt => opt.MapFrom(src => src.DbExtraId))
    .ReverseMap()

2
但是如果你想让AutoMapper构造一个新的ATo,这种方法就不起作用了,例如:mapper.Map<ATo>(new BFrom(...))会失败并提示需要一个没有参数或只有可选参数的构造函数... - Christian Genne

1

采用更静态的方法...我们知道记录构造函数的名称..

public record ATo(Guid Id, Guid ExtraId) : BaseTo(Id);

CreateMap<BFrom, ATo>()
    .ForMember(nameof(ATo.Extra), opt => opt.MapFrom(src => src.DbExtraId))
    .ReverseMap();

解释/深入探讨

记录实际上只是一个永久属性的语法糖,这个概念已经存在了很长时间。使用记录会使与旧概念的自动映射更加方便和安全,因为通过新的语法,我们知道属性名称将是什么,即 record Example(string SomeProperty)。编译器将创建如下构造函数:

public class Example
{
    public string SomeProperty { get; init; }         
    Example(string SomeProperty) 
    {
        this.SomeProperty = SomeProperty;
    }
}

创建一个空的构造函数是没有用的,因为属性是 init 类型,而你不能真正使用对象初始化器,因为你必须使用构造函数... 而且对象初始化器只是对于 init 属性的特殊情况,它们允许属性更改,尽管对象初始化器本身实际上只是语法糖。通过反射没有真正的方法来使用对象初始化器,因此像 AutoMapper 这样的库就不存在这种漏洞,漏洞是这样的:new Example(default){SomeProperty = "SomeValue"}
现在当然可以使用反射来更改 readonly/private/internal 属性,我不确定 AutoMapper 是否支持,希望它不支持,因为这并不是一个很好的做法,原因在互联网上都有详细说明。

1
CreateMap<BFrom, ATo>().ForCtorParam(nameof(ATo.Extra), opt => opt.MapFrom(src => src.DbExtraId)).ReverseMap(); 创建一个从BFrom到ATo的映射,将ATo类的构造函数参数中名为“Extra”的属性映射到src.DbExtraId,并支持其反向映射。 - Hamid Musayev

1

我没有看到能够解决我的问题的答案:我只是将“ForMember”替换为“ForCtorParm”。

您需要将构造函数参数提供为字符串,因此变成:

CreateMap<BFrom, ATo>()
    .ForCtorParam("ExtraId", opt => opt.MapFrom(src => src.DbExtraId))
    .ForCtorParam("Id", opt => opt.MapFrom(src => default(Guid)));

需要注意以下几点:

  • 你需要提供所有参数,否则(隐式创建的)构造函数将无法被调用。
  • "Ignore"方法不再存在。你不能选择忽略构造函数参数。
  • ReverseMap不起作用:一旦编译完成,就会有一个构造函数,其参数名称刚好与属性的名称和类型相同。但在这个阶段,automapper不知道这种情况。记录只被编译为类(带有一些特殊属性)

你可以通过利用生成的构造函数参数名和属性名/类型始终相同的事实来使生活更轻松。你可以添加一个扩展方法。

   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));
                }
            }
        }
    }

这样可以让你更加类型安全地进行操作:

CreateMap<BFrom, ATo>()
    .ForCtorParam(dest => dest.ExtraId, opt => opt.MapFrom(src => src.DbExtraId))
    .ForCtorParam(dest => dest.Id, opt => opt.MapFrom(src => default(Guid)));

工作,但我不能在ForCtorParam中使用例如ConvertUsing。 - anon37894203
你可以对整个事情使用ConvertUsing。 - realbart

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接