2

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.

Mrchief
  • 75,126
  • 20
  • 142
  • 189

2 Answers2

1

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.
Community
  • 1
  • 1
Alexey Merson
  • 414
  • 5
  • 12
0

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
Robert
  • 4,306
  • 11
  • 45
  • 95
  • You sure? I get the same exact error with `BeforeMap`. – Mrchief Mar 28 '14 at 19:42
  • `Target` doesn't have `SomeClass` or `AnotherClass`, just the flat properties. And from the error, it seems that only non-flat top level properties can be mapped as such. – Mrchief Mar 31 '14 at 02:30
  • 2
    Thanks but that is precisely what I want to avoid. I have hardwired individual properties manually right now but I would like to take advantage of "Auto-Mapping"! :) – Mrchief Mar 31 '14 at 20:35