Can AutoMapper implicitly flatten this mapping? - automapper

I am trying to map between two lists of objects. The source type has a complex property of type A; the destination type is a flattened subset of type A plus an additional scalar property that is in the source type.
public class A
{
public int Id { get; set; }
public string Name { get; set; }
}
public class Source
{
public A MyA { get; set; }
public int SomeOtherValue { get; set; }
}
public class Destination
{
public string Name { get; set; }
public int SomeOtherValue { get; set; }
}
If it's not clear, I'd like Source.MyA.Name to map to Destination.Name and Source.SomeOtherValue to map to Destination.SomeOtherValue.
In reality, type A has a dozen or so properties, about which 80% map over to properties of the same name in Destination. I can get things to work if I explicitly spell out the mappings in CreateMap like so:
CreateMap<Source, Destination>()
.ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.MyA.Name));
The downside here is I want to avoid having to add a ForMember line for each of A's properties that need to get copied over to Destination. I was hoping I could do something like:
CreateMap<Source, Destination>()
.ForMember(dest => dest, opt => opt.MapFrom(src => src.MyA));
But if I try the above I get a runtime error when the mapping is registered: "Custom configuration for members is only supported for top-level individual members on a type."
Thanks

create mappings between A and Destination, and Source and Destination, and then use AfterMap() to use first mapping in second
Mapper.CreateMap<A, Destination>();
Mapper.CreateMap<Source, Destination>()
.AfterMap((s, d) => Mapper.Map<A, Destination>(s.MyA, d));
then use it like this:
var res = Mapper.Map<Source, Destination>(new Source { SomeOtherValue = 7, MyA = new A { Id = 1, Name = "SomeName" } });

As a workaround you can use custom type converter with additional property in the destination type to avoid recursion.
[TestFixture]
public class MapComplexType
{
[Test]
public void Map()
{
Mapper.CreateMap<A, Destination>();
Mapper.CreateMap<Source, Destination>().ConvertUsing(new TypeConvertor());
var source = new Source
{
MyA = new A
{
Name = "Name"
},
SomeOtherValue = 5
};
var dest = new Destination();
Mapper.Map(source, dest);
Assert.AreEqual(dest.Name, "Name");
}
}
public class TypeConvertor : ITypeConverter<Source, Destination>
{
public Destination Convert(ResolutionContext context)
{
var destination = (Destination) context.DestinationValue;
if (!((Destination)context.DestinationValue).IsMapped || destination == null)
{
destination = destination ?? new Destination();
destination.IsMapped = true; // To avoid recursion
Mapper.Map((Source)context.SourceValue, destination);
destination.IsMapped = false; // If you want to map the same object few times
}
Mapper.Map(((Source)context.SourceValue).MyA, destination);
return (Destination)context.DestinationValue;
}
}
public class A
{
public int Id { get; set; }
public string Name { get; set; }
}
public class Source
{
public A MyA { get; set; }
public int SomeOtherValue { get; set; }
}
public class Destination
{
public string Name { get; set; }
public int SomeOtherValue { get; set; }
// Used only for mapping purposes
internal bool IsMapped { get; set; }
}

Try this,
Mapper.CreateMap<A, Destination>();
Mapper.CreateMap<Source, Destination>()
.ForMember(destination => destination.Name, options => options.MapFrom(source => Mapper.Map<A, Destination>(source.MyA).Name));
var objSource = new Source { SomeOtherValue = 7, MyA = new A { Id = 1, Name = "SomeName" } };
var result = Mapper.Map<Source, Destination>(objSource);

Related

Generic Automapper function with custom convension for underscored properties

I simply need to map some auto generated classes from database to domain/viewmodels classes. The autogenerated class may have names like customer_id that I want to be mapped with CustomerId. Somehow I want to register my own convention with auto mapper. I have started with following code however the mapped object properties are not populated:
// Generic method that should map source to target
public static TTarget MapWithUnderScoreConvension(TSource source, TTarget target)
{
Mapper.Initialize(cfg=> cfg.AddProfile<AutoMapperUnderScoreProfile>());
Mapper.CreateMap<TSource, TTarget>();
var mapped = Mapper.Map(source, target);
return mapped;
}
// New underscore profile
public class AutoMapperUnderScoreProfile : Profile
{
public AutoMapperUnderScoreProfile()
{
Mapper.Initialize(configuration => configuration.CreateProfile("UnderScoreProfile", UnderScoreProfile));
Mapper.AssertConfigurationIsValid();
}
private void UnderScoreProfile(IProfileExpression profile)
{
profile.SourceMemberNamingConvention = new PascalCaseNamingConvention();
profile.DestinationMemberNamingConvention = new SourceUnderScoreNamingConvension();
}
}
// Convention class
public class SourceUnderScoreNamingConvension : INamingConvention
{
private readonly string _separatorCharacter="_";
private readonly Regex _splittingExpression = new Regex(#"[\p{Lu}0-9]+(?=_?)");
public Regex SplittingExpression { get { return _splittingExpression;} private set{} }
public string SeparatorCharacter { get { return _separatorCharacter; } private set{} }
}
// Test cases
[TestMethod()]
public void Test_Map_Db_Generated_Class_To_Model()
{
var dbGenerated = new QuestionTypeFromDb()
{
QuestionType_Description = "1",
QuestionType_Id = 1,
QuestionType_Is_Default = true,
QuestionType_Is_TimeBased = true,
QuestionType_Sequence = 1,
QuestionType_Time_In_Seconds = 1
};
var mappedObject = AutoMapperHelper<QuestionTypeFromDb, QuestionType>
.MapWithUnderScoreConvension(dbGenerated, new QuestionType());
mappedObject.QuestionTypeId.Should().Be(dbGenerated.QuestionType_Id);
mappedObject.QuestionTypeDescription.Should().Be(dbGenerated.QuestionType_Description);
mappedObject.TimeInSeconds.Should().Be(dbGenerated.QuestionType_Time_In_Seconds);
mappedObject.QuestionTypeSequence.Should().Be(dbGenerated.QuestionType_Sequence);
}
public class TestQuestionWithAnswerType
{
public int QuestionTypeId { get; set; }
public string QuestionTypeDescription { get; set; }
public int QuestionTypeSequence { get; set; }
public bool QuestionTypeIsTimeBased { get; set; }
public int? QuestionTypeTimeInSeconds { get; set; }
public bool QuestionTypeIsDefault { get; set; }
}
any comments will be appreciated.
Update
I have found that the following workaround works:
I simply replaced used this -> to replace 'underscore' with nothing (Mapper.Initialize(c => c.ReplaceMemberName("_", ""));
public static TTarget MapWithUnderScoreConvension(TSource source, TTarget target)
{
Mapper.Initialize(c => c.ReplaceMemberName("_", ""));
//Mapper.Initialize(cfg => cfg.AddProfile<AutoMapperUnderScoreProfile>());
Mapper.CreateMap<TSource, TTarget>();
var mapped = Mapper.Map(source, target);
return mapped;
}
Your regex needs to be changed to : [\p{L}}0-9]+(?=_?)
This will take care of Customer_Id, CUSTOMER_ID, customer_id
The {L} unicode category includes Lu, Lt, Ll, Lm and Lo characters as mentioned here.
Answer is added in the Update section of the question. Basically the solution for me was very simple -> Mapper.Initialize(c => c.ReplaceMemberName("_", ""));

Problems mapping a type that inherits from IEnumerable

I have a problem mapping a property containing a custom list that inherits from IEnumerable (if i remove that inheritance, this example works). I have simplified the problem into this model:
public interface IMyEnumerable<T> : IEnumerable<T> { }
public class MyIEnumerable<T> : IMyEnumerable<T>
{
private readonly IEnumerable<T> _items;
public MyIEnumerable(IEnumerable<T> items)
{
_items = items;
}
public IEnumerator<T> GetEnumerator()
{
return _items.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
public class Source
{
public List<SourceItem> Items { get; set; }
}
public class Destination
{
public IMyEnumerable<DestinationItem> Items { get; set; }
}
public class SourceItem
{
public string Name { get; set; }
}
public class DestinationItem
{
public string Name { get; set; }
}
Then i try to use is this way:
public class MyResolver : ValueResolver<Source, IMyEnumerable<DestinationItem>>
{
protected override IMyEnumerable<DestinationItem> ResolveCore(Source source)
{
var destinationItems = Mapper.Map<List<SourceItem>, IEnumerable<DestinationItem>>(source.Items);
return new MyIEnumerable<DestinationItem>(destinationItems);
}
}
// Mappings
Mapper.CreateMap<Source, Destination>()
.ForMember(x => x.Items, m => m.ResolveUsing<MyResolver>());
Mapper.CreateMap<SourceItem, DestinationItem>();
// Using the mappings
var source = // not really relevant
var destination = Mapper.Map<Destination>(source);
This gives me the following exception (slightly edited for readability):
Mapping types:
MyIEnumerable`1 -> IMyEnumerable`1
MyIEnumerable`1[[DestinationItem]] -> IMyEnumerable`1[[DestinationItem]]
Destination path:
Destination.Items.Items
Source value:
MyIEnumerable`1[DestinationItem]
----> System.ArgumentException : Object of type System.Collections.Generic.List`1[DestinationItem] cannot be converted to type IMyEnumerable`1[DestinationItem].
Any idea how i can fix the mapping so that i can get this to work?
Assuming the following:
var source = new Source
{
Items = new List<SourceItem>
{
new SourceItem { Name = "foo" },
new SourceItem { Name = "bar" },
new SourceItem { Name = "cow" },
}
};
Then the following work:
// Method 1: Straight up mapping the collections:
Mapper.CreateMap<List<SourceItem>, IMyEnumerable<DestinationItem>>()
.ConstructUsing(list => new MyEnumerable<DestinationItem>(list.ConvertAll(Mapper.Map<SourceItem, DestinationItem>)));
// Method 2: Ignore the property and do it ourselves after the rest of the mapping:
Mapper.CreateMap<Source, Destination>()
.ForMember(q => q.Items, r => r.Ignore())
.AfterMap((s, d) => d.Items = new MyEnumerable<DestinationItem>(
s.Items.Select(Mapper.Map<SourceItem, DestinationItem>)));
Nothing else seems to work due to some combination of covariance and contravariance between List<T>, IEnumerable<T> and IMyEnumerable<T>

AutoMapper property with TypeName start with Tbl in Source and property start with FK in destination

i have problem with automapper for convert property with TypeName start with Tbl in Source and property start with FK in destination, but this error occured:
Missing type map configuration or unsupported mapping. Tbl_Child -> Int32 Tbl_Child -> System.Int32 Destination path: Destination.FK_Child.FK_Child Source value: Tbl_Child
code is:
public class Source
{
public Tbl_Child Child { get; set; }
public string SourceName { get; set; }
}
public class Tbl_Child
{
public int ID_Child { get; set; }
public string ChildName { get; set; }
}
public class Destination
{
public int FK_Child { get; set; }
public string ChildName { get; set; }
public string SourceName { get; set; }
}
static void Main(string[] args)
{
var src = new Source()
{
Child = new Tbl_Child()
{
ChildName = "ch",
ID_Child = 1
},
SourceName = "src"
};
AutoMapper.Mapper.CreateMap<Source, Destination>();
var dest = AutoMapper.Mapper.Map<Source, Destination>(src);
Console.ReadKey();
}
i test AutoMapper version 2.0.0.0, and 3.1.1.0
Out of the box, Automapper is able to map properties with the same name and flattening in properties. For other properties you must specify the mapping that you want.
For example mapping from Source.Child.ID_Child to Destination.FK_Child would be accomplished like this:
Mapper.CreateMap<Source, Destination>()
.ForMember(d => d.FK_Child, o => o.MapFrom(s => s.Child.ID_Child));
And you can chain all your custom mappings like so
Mapper.CreateMap<Source, Destination>()
.ForMember(d => d.FK_Child, o => o.MapFrom(s => s.Child.ID_Child))
.ForMember(d => d.ChildName, o => o.MapFrom(s => s.Child.ChildName));
This mapping should be enough in this case but I think you may need to read the wiki a bit before going on

What expression is needed for mapping this property?

I am trying to map two objects that are mostly similar with AutoMapper but one member (AudioSummary) raises the following exception :
The following property on EchoNestModel.AudioSummary cannot be mapped: AudioSummary
Add a custom mapping expression, ignore, add a custom resolver, or modify the destination type EchoNestModel.AudioSummary.
Context:
- Mapping to property AudioSummary from EchoNest.Api.AudioSummary to EchoNestModel.AudioSummary
- Mapping from type EchoNest.Api.TrackProfile to EchoNestModel.Profile
Exception of type 'AutoMapper.AutoMapperConfigurationException' was thrown.
Mapping definition
var map = Mapper.CreateMap<TrackProfile, Profile>();
map.ForMember(dest => dest.ForeignIds, opt => opt.ResolveUsing<ForeignIdResolver>());
map.ForMember(dest => dest.ForeignReleaseIds, opt => opt.ResolveUsing<ForeignReleaseIdResolver>());
map.ForMember(s => s.Media, t => t.Ignore());
map.ForMember(s => s.ProfileId, t => t.Ignore());
map.ForMember(s => s.AudioSummary, t => t.MapFrom(s => s.AudioSummary));
I've added the following two lines but a totally different error occurs :
map.ForMember(s => s.AudioSummary.Profile, t => t.Ignore());
map.ForMember(s => s.AudioSummary.AudioSummaryId, t => t.Ignore());
Expression 's => s.AudioSummary.Profile' must resolve to top-level member and not any child object's properties.
Use a custom resolver on the child type or the AfterMap option instead.
Parameter name: lambdaExpression
How can I successfully map AudioSummary ?
Source object
Target object
EDIT: In general, try AutoMapper.Mapper.AssertConfigurationIsValid();, this will show you all possible problems in your mapper setup.
From the information you provided, it looks like you need to define map for the AudioSummary classes (dest and source) as well:
[TestFixture]
public class MappingTest
{
public class SourceAudioSummary
{
public int Id { get; set; }
public string OtherData { get; set; }
}
public class TrackProfile
{
public string Whatever { get; set; }
public SourceAudioSummary AudioSummary { get; set; }
}
public class DestAudioSummary
{
public int Id { get; set; }
public string OtherData { get; set; }
}
public class Profile
{
public string Whatever { get; set; }
public DestAudioSummary AudioSummary { get; set; }
}
[Test]
public void Mapping()
{
Mapper.CreateMap<SourceAudioSummary, DestAudioSummary>();
Mapper.CreateMap<TrackProfile, Profile>();
var trackProfile = new TrackProfile
{
Whatever = "something",
AudioSummary = new SourceAudioSummary
{
Id = 1,
OtherData = "other"
}
};
var profile = Mapper.Map<TrackProfile, Profile>(trackProfile);
Assert.That(profile.Whatever == "something");
Assert.That(profile.AudioSummary.Id == 1);
Assert.That(profile.AudioSummary.OtherData == "other");
}
}

Automapper: How to leverage a custom INamingConvention?

I am working with a database where the designers really seemed to enjoy capital letters and the underscore key. Since I have a simple ORM, my data models use these names as well. I need to build DTOs and I would prefer to give them standard names since we are exposing them through services.
The code below is now corrected! The test passes so use this as a reference if you need to use multiple naming conventions
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using AutoMapper;
using NUnit.Framework;
namespace AutomapperTest
{
public class DATAMODEL
{
public Guid ID { get; set; }
public string FIRST_NAME { get; set; }
public List<CHILD_DATAMODEL> CHILDREN { get; set; }
}
public class CHILD_DATAMODEL
{
public Guid ID { get; set; }
public int ORDER_ID { get; set; }
}
public class DataModelDto
{
public Guid Id { get; set; }
public string FirstName { get; set; }
public List<ChildDataModelDto> Children { get; set; }
}
public class ChildDataModelDto
{
public Guid Id { get; set; }
public int OrderId { get; set; }
}
public class UpperUnderscoreNamingConvention : INamingConvention
{
private readonly Regex _splittingExpression = new Regex(#"[\p{Lu}0-9]+(?=_?)");
public Regex SplittingExpression { get { return _splittingExpression; } }
public string SeparatorCharacter { get { return "_"; } }
}
public class Profile1 : Profile
{
protected override void Configure()
{
SourceMemberNamingConvention = new UpperUnderscoreNamingConvention();
DestinationMemberNamingConvention = new PascalCaseNamingConvention();
CreateMap<DATAMODEL, DataModelDto>();
CreateMap<CHILD_DATAMODEL, ChildDataModelDto>();
}
}
[TestFixture]
public class Tests
{
[Test]
public void CanMap()
{
//tell automapper to use my convention
Mapper.Initialize(x => x.AddProfile<Profile1>());
//make a dummy source object
var src = new DATAMODEL();
src.ID = Guid.NewGuid();
src.FIRST_NAME = "foobar";
src.CHILDREN = new List<CHILD_DATAMODEL>
{
new CHILD_DATAMODEL()
{
ID = Guid.NewGuid(),
ORDER_ID = 999
}
};
//map to destination
var dest = Mapper.Map<DATAMODEL, DataModelDto>(src);
Assert.AreEqual(src.ID, dest.Id);
Assert.AreEqual(src.FIRST_NAME, dest.FirstName);
Assert.AreEqual(src.CHILDREN.Count, dest.Children.Count);
Assert.AreEqual(src.CHILDREN[0].ID, dest.Children[0].Id);
Assert.AreEqual(src.CHILDREN[0].ORDER_ID, dest.Children[0].OrderId);
}
}
}
Create your mappings in profiles, and define the INamingConvention parameters as appropriate.
I don't like the global/static, so I prefer using Initialize and define all of my mappings together. This also has the added benefit of allowing a call to AssertConfiguration... which means if I've borked my mapping I'll get the exception at launch instead of whenever my code gets around to using the problematic mapping.
Mapper.Initialize(configuration =>
{
configuration.CreateProfile("Profile1", CreateProfile1);
configuration.CreateProfile("Profile2", CreateProfile2);
});
Mapper.AssertConfigurationIsValid();
in the same class with that initialization method:
public void CreateProfile1(IProfileExpression profile)
{
// this.CreateMap (not Mapper.CreateMap) statements that do the "normal" thing here
// equivalent to Mapper.CreateMap( ... ).WithProfile("Profile1");
}
public void CreateProfile2(IProfileExpression profile)
{
profile.SourceMemberNamingConvention = new PascalCaseNamingConvention();
profile.DestinationMemberNamingConvention = new LowerUnderscoreNamingConvention();
// this.CreateMap (not Mapper.CreateMap) statements that need your special conventions here
// equivalent to Mapper.CreateMap( ... ).WithProfile("Profile2");
}
if you do it this way, and don't define the same mapping in both profiles, I don't think you need anything to "fill in the blank" from the original question, it should already be setup to do the right thing.
What about
public class DATAMODELProfile : Profile
{
protected override void Configure()
{
Mapper.CreateMap<DATAMODEL, DATAMODEL>();
Mapper.CreateMap<DATAMODEL, SOMETHINGELSE>();
Mapper.CreateMap<DATAMODEL, DataModelDto>()
.ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.ID))
.ForMember(dest => dest.FirstName, opt => opt.MapFrom(src => src.FIRST_NAME))
.ForMember(dest => dest.ChildDataModels, opt => opt.MapFrom(src => src.CHILD_DATAMODELS));
}
}

Resources