使用AutoMapper的ConstructUsing映射子属性:生产可用但测试配置无效
我在项目中配置AutoMapper,需要完成两种映射:
- 将Source对象映射到Destination对象(实际包含约30个属性,示例简化为2个)
- 将包含Source子属性及额外字段的SourceWithChild对象也映射到Destination
参考方案使用ConstructUsing实现,生产环境映射功能正常,但测试时执行AssertConfigurationIsValid()会触发配置验证失败。由于手动映射30个子属性会大幅增加维护成本,希望找到一种在测试和生产环境均有效的实现方式,或确认是否为AutoMapper的Bug。
复现代码
internal class Source { public int IntValue { get; set; } public string? StringValue { get; set; } } internal class SourceWithChild { public Source Source { get; set; } public string AdditionalField { get; set; } } internal class Destination { public int IntValue { get; set; } public string? StringValue { get; set; } public string AdditionalField { get; set; } } internal class ChildMappingProfile : Profile { public ChildMappingProfile() { CreateMap<Source, Destination>() .ForMember(dest => dest.IntValue, opt => opt.MapFrom(src => src.IntValue)) // 无需手动配置,属性名一致 .ForMember(dest => dest.StringValue, opt => opt.MapFrom(src => src.StringValue)) // 无需手动配置,属性名一致 .ForMember(dest => dest.AdditionalField, opt => opt.Ignore()) ; } } internal class ConstructUsingMappingProfile : Profile { public ConstructUsingMappingProfile() { CreateMap<SourceWithChild, Destination>() .ForMember(dest => dest.AdditionalField, opt => opt.MapFrom(src => src.AdditionalField)) // 无需手动配置,属性名一致 .ConstructUsing((src, ctx) => ctx.Mapper.Map<Destination>(src.Source)) ; } } public class ConstructUsingTests { [Fact] public void AutoMapper_ChildMapping_IsValid() { var config = new MapperConfiguration(cfg => cfg.AddProfile(new ChildMappingProfile())); config.AssertConfigurationIsValid(); } [Fact] public void AutoMapper_ConstructUsing_Test_IsValid() { var config = new MapperConfiguration(cfg => { cfg.AddProfile(new ChildMappingProfile()); cfg.AddProfile(new ConstructUsingMappingProfile()); }); config.AssertConfigurationIsValid(); } [Fact] public void AutoMapper_ConstructUsing_Test_Maps_correctly() { var config = new MapperConfiguration(cfg => { cfg.AddProfile(new ChildMappingProfile()); cfg.AddProfile(new ConstructUsingMappingProfile()); }); var destination = config.CreateMapper().Map<Destination>(new SourceWithChild { AdditionalField = "2", Source = new Source { IntValue = 1, StringValue = "Test" } }); destination.Should().NotBeNull(); destination.AdditionalField.Should().Be("2"); destination.IntValue.Should().Be(1); destination.StringValue.Should().Be("Test"); } }
解决方案
方法一:使用IncludeMembers复用已有映射(推荐)
修改ConstructUsingMappingProfile,用IncludeMembers指定从SourceWithChild.Source子属性继承映射规则,同时保留额外字段的映射。这种方式无需手动维护大量属性,配置验证和生产映射均正常:
internal class ConstructUsingMappingProfile : Profile { public ConstructUsingMappingProfile() { CreateMap<SourceWithChild, Destination>() .IncludeMembers(src => src.Source) .ForMember(dest => dest.AdditionalField, opt => opt.MapFrom(src => src.AdditionalField)); } }
方法二:显式忽略未映射属性(不推荐)
如果坚持使用ConstructUsing,需要显式忽略Destination中从Source继承的属性,告诉AutoMapper这些属性已通过ConstructUsing完成赋值:
internal class ConstructUsingMappingProfile : Profile { public ConstructUsingMappingProfile() { CreateMap<SourceWithChild, Destination>() .ForMember(dest => dest.AdditionalField, opt => opt.MapFrom(src => src.AdditionalField)) .ForMember(dest => dest.IntValue, opt => opt.Ignore()) .ForMember(dest => dest.StringValue, opt => opt.Ignore()) .ConstructUsing((src, ctx) => ctx.Mapper.Map<Destination>(src.Source)); } }
注意:这种方式需要手动忽略所有从Source来的属性,属性数量多的时候维护成本极高,不推荐使用。
方法三:使用AfterMap完成后续映射
先创建空的Destination对象,再通过AfterMap调用已有映射填充Source子属性的内容,最后处理额外字段:
internal class ConstructUsingMappingProfile : Profile { public ConstructUsingMappingProfile() { CreateMap<SourceWithChild, Destination>() .AfterMap((src, dest, ctx) => ctx.Mapper.Map(src.Source, dest)) .ForMember(dest => dest.AdditionalField, opt => opt.MapFrom(src => src.AdditionalField)); } }
这种方式同样能通过配置验证,且复用已有映射规则。
问题原因
AutoMapper的配置验证逻辑会检查Destination的每个属性是否在当前映射中存在明确的映射规则。原方案中,Destination的IntValue和StringValue虽然通过ConstructUsing中的另一个映射完成赋值,但验证逻辑仅检查当前SourceWithChild -> Destination的映射配置,未识别到这两个属性的映射来源,因此触发验证失败。
内容的提问来源于stack exchange,提问作者Wolfware

