简体   繁体   中英

Automapper flattening multiple complex objects using custom mapping

So I have something besides the usual DTO to business mapper and I'm trying to map them with minimal amount of mapping code.

Setup

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 Config

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

Doing this throws

Error: AutoMapper.AutoMapperConfigurationException: Custom configuration for members is only supported for top-level individual members on a type.

And I also need to take AnotherClass.Tokens , SomeClass.Tokens and add it to Target.Tokens .

I know I can use .ConvertUsing but then I have to define mapping for every property and I lose the advantage of convention based mapping for matching properties.

Is there any other way of achieving this (other than .ConvertUsing or mapping every property by hand)?

If not via Automapper , is it doable via EmitMapper ? I guess adding to Tokens list is probably doable via EmitMapper's PostProcessing .

Update

After a bit of hacking, I found a way:

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

Note: I do the Name.ToLowerInvariant() to be able to match AccountID -> AccountId and similar.

Usage

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

I spotted some other properties in IMappingExpression that I maybe able to use and cleanup a lot of this. Will update as I find them.

That's how I solved similar problem:

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

Usage

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() found here .

Though it's not universal solution it should be enough for simple cases.

Advantages are:

  1. You use AutoMapper to create nested map so you rely on trusted code and also you can use stuff like RecognizePrefixes and so on.
  2. As you need to specify nested property selector you avoid possible ambiguity when you have multiple nested properties of same type.

You want to use BeforeMap to instantiate the object:

UPDATE:

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

That would allow you to map the parent before mapping the individual objects properties.

I think I am getting lost in your base class names but you can call the mapper.Map properties to map the objects.

UPDATE 2:

Based on this code:

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

Dest there is trying to resolve an object. If you want to resolve only properties on those objects then I would suggest that you specify them.

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM