【问题标题】:Automapper flattening multiple complex objects using custom mappingAutomapper 使用自定义映射展平多个复杂对象
【发布时间】:2016-05-18 23:35:50
【问题描述】:

所以除了通常的 DTO 到业务映射器之外,我还有一些东西,我正在尝试用最少的映射代码来映射它们。

设置

public class Target {

    public string propA { get; set; }
    public string propB { get; set; }
    public string propC { get; set; }
    public string propD { get; set; }
    public string propE { get; set; }
    public List<KeyValuePair> Tokens { get; set; }
}

public class Source {
    public SomeClass SomeClass { get; set; }
    public AnotherClass AnotherClass { get; set; }

}

public class SomeClass {
    public string propA { get; set; }
    public string propB { get; set; }
    public string propDifferent { get; set; }
    public List<KeyValuePair> Tokens { get; set; }
}

public class AnotherClass {
    public string propC { get; set; }
    public string propD { get; set; }
    public List<KeyValuePair> Tokens { get; set; }
}

映射器配置

Mapper.CreateMap<SomeClass, Target>()
    .ForMember(dest => dest.propE, opt => opt.MapFrom(src => src.propDifferent));


Mapper.CreateMap<AnotherClass, Target>();

Mapper.CreateMap<Source, Target>()
    .ForMember(dest => dest, opt => opt.MapFrom(src => src.SomeClass))
    .ForMember(dest => dest, opt => opt.MapFrom(src => src.AnotherClass));

这样做会抛出

错误:AutoMapper.AutoMapperConfigurationException:自定义 成员的配置仅支持顶级个人 类型上的成员。

而且我还需要将AnotherClass.TokensSomeClass.Tokens 添加到Target.Tokens

我知道我可以使用.ConvertUsing,但是我必须为每个属性定义映射,并且我失去了基于约定的映射来匹配属性的优势。

有没有其他方法可以实现这一点(.ConvertUsing 或手动映射每个属性除外)?

如果不是通过Automapper,是否可以通过EmitMapper?我想通过 EmitMapper 的PostProcessing 添加到 Tokens 列表可能是可行的。

更新

经过一番hack,我找到了一种方法:

public static IMappingExpression<TSource, TDestination> FlattenNested<TSource, TNestedSource, TDestination>(this IMappingExpression<TSource, TDestination> expression)
{
    var sourceType = typeof(TNestedSource);
    var destinationType = typeof(TDestination);
    var sourceProperties = sourceType.GetProperties().ToDictionary(x => x.Name.ToLowerInvariant());
    var childPropName = typeof (TSource).GetProperties().First(x => x.PropertyType == sourceType).Name;
    var mappableProperties = destinationType.GetProperties()
        .Where(p => sourceProperties.ContainsKey(p.Name.ToLowerInvariant()) &&
                    sourceProperties[p.Name.ToLowerInvariant()].PropertyType ==
                    p.PropertyType)
        .Select(p => new {DestProperty = p.Name, SrcProperty = sourceProperties[p.Name.ToLowerInvariant()].Name});


    foreach (var property in mappableProperties)
    {
        expression.ForMember(property.DestProperty,
            opt => opt.MapFrom(src => src.GetPropertyValue(childPropName).GetPropertyValue(property.SrcProperty)));
    }

    return expression;
}

注意:我使用Name.ToLowerInvariant() 是为了能够匹配AccountID -> AccountId 和类似的。

用法

AutoMapper.Mapper.CreateMap<Source, Target>()
    .FlattenNested<Source, SomeClass, Target>()
    .FlattenNested<Source, AnotherClass, Target>()
    .ForMember(dest => dest.propE, opt => opt.MapFrom(src => src.propDifferent));

我在IMappingExpression 中发现了一些其他属性,我也许可以使用和清理很多这些属性。当我找到它们时会更新。

【问题讨论】:

    标签: c# automapper emitmapper


    【解决方案1】:

    这就是我解决类似问题的方法:

    public static IMappingExpression<TSource, TDestination> FlattenNested<TSource, TNestedSource, TDestination>(
        this IMappingExpression<TSource, TDestination> expression,
        Expression<Func<TSource, TNestedSource>> nestedSelector,
        IMappingExpression<TNestedSource, TDestination> nestedMappingExpression)
    {
        var dstProperties = typeof(TDestination).GetProperties().Select(p => p.Name);
    
        var flattenedMappings = nestedMappingExpression.TypeMap.GetPropertyMaps()
                                                        .Where(pm => pm.IsMapped() && !pm.IsIgnored())
                                                        .ToDictionary(pm => pm.DestinationProperty.Name,
                                                                        pm => Expression.Lambda(
                                                                            Expression.MakeMemberAccess(nestedSelector.Body, pm.SourceMember),
                                                                            nestedSelector.Parameters[0]));
    
        foreach (var property in dstProperties)
        {
            if (!flattenedMappings.ContainsKey(property))
                continue;
    
            expression.ForMember(property, opt => opt.MapFrom((dynamic)flattenedMappings[property]));
        }
    
        return expression;
    }
    

    用法

    public class Customer
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public Address Address { get; set; }
    }
    
    public class Address
    {
        public string City { get; set; }
        public string Street { get; set; }
    }
    
    public class CustomerDto
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string City { get; set; }
        public string Street { get; set; }
    }
    
    public class CustomerProfile : Profile
    {
        protected override void Configure()
        {
            var nestedMap = CreateMap<Address, CustomerDto>()
                .IgnoreAllNonExisting();
    
            CreateMap<Customer, CustomerDto>()
                .FlattenNested(s => s.Address, nestedMap);
        }
    }
    
    [TestFixture]
    public class CustomerProfileTests
    {
        [Test]
        public void Test()
        {
            Mapper.Initialize(c => c.AddProfile<CustomerProfile>());
            Mapper.AssertConfigurationIsValid();
        }
    }
    

    IgnoreAllNonExisting() 找到here

    虽然它不是通用的解决方案,但对于简单的情况应该足够了。

    优点是:

    1. 您使用 AutoMapper 创建嵌套地图,因此您可以依赖受信任的代码,也可以使用 RecognizePrefixes 等内容。
    2. 由于您需要指定嵌套属性选择器,因此您可以避免在具有多个相同类型的嵌套属性时可能出现的歧义。

    【讨论】:

    • 谢谢。虽然我现在正试图摆脱 AutoMapper。
    【解决方案2】:

    您想使用 BeforeMap 实例化对象:

    更新:

    Mapper.CreateMap<Source, Target>()
    .BeforeMap(( Source, Target) => {
         Source.SomeClass = new SomeClass();
         Source.AnotherClass = new AnotherClass();
     })  
     .AfterMap(( Source, Target) => {
         Target.SomeClass = Mapper.Map<AnotherClass, Target>(Target);
         Target.AnotherClass = Mapper.Map<SomeClass, Target>(Target);
     })
    

    这将允许您在映射单个对象属性之前映射父对象。

    我想我迷失在你的基类名称中,但你可以调用 mapper.Map 属性来映射对象。

    更新 2:

    基于此代码:

    Mapper.CreateMap<Source, Target>()
    .ForMember(dest => **dest**, opt => opt.MapFrom(src => src.SomeClass))
    .ForMember(dest => **dest**, opt => opt.MapFrom(src => src.AnotherClass));
    

    Dest 正在尝试解析对象。如果您只想解析这些对象的属性,那么我建议您指定它们。

    Mapper.CreateMap<Source, Target>()
     .ForMember(dest => dest.propA, opt => opt.MapFrom(src => src.SomeClass.propA
     .ForMember(dest => dest.propB, opt => opt.MapFrom(src => src.SomeClass.propB
     .ForMember(dest => dest.propC, opt => opt.MapFrom(src => src.AnotherClass.propC
     .ForMember(dest => dest.propD, opt => opt.MapFrom(src => src.AnotherClass.propD
    

    【讨论】:

    • 你确定?我得到与BeforeMap 相同的错误。
    • Target 没有SomeClassAnotherClass,只有平面属性。从错误来看,似乎只有非平面顶级属性可以这样映射。
    • 谢谢,但这正是我想要避免的。我现在手动硬连线各个属性,但我想利用“自动映射”! :)
    猜你喜欢
    • 2017-06-13
    • 1970-01-01
    • 1970-01-01
    • 2012-10-16
    • 1970-01-01
    • 1970-01-01
    • 2020-04-03
    • 2012-04-19
    • 2015-10-20
    相关资源
    最近更新 更多