Automapper List to CustomCollection - c#

I am trying to map 2 Entities. One has a List of strings and other one is using a custom collection. Can anybody please advice how can I map these 2 entities using AutoMapper.
public class SourceItem
{
public string Name { get; set; }
public List<string> ShipsTo { get; set; }
}
public class DestItem
{
public string Name { get; set; }
public MyCollection ShipsTo { get; set; }
}
public class MyCollection : CollectionBase
{
private readonly List<string> _list;
public MyCollection()
{
_list = new List<string>();
}
public MyCollection(List<string> list)
{
_list = list;
}
public void Add(string item)
{
_list.Add(item);
}
}
Here is my mapping code...
Mapper.Initialize(cfg =>
{
cfg.CreateMap<SourceItem, DestItem>()
.ForMember(d => d.ShipsTo, o => o.ResolveUsing<CustomResolver>());
});
Here is how my custom resolver looks like...
public class CustomResolver : IValueResolver<SourceItem, DestItem, MyCollection>
{
public MyCollection Resolve(SourceItem source, DestItem destination, MyCollection destMember, ResolutionContext context)
{
return new MyCollection(source.ShipsTo);
}
}
When I try to run this code, I get an error saying
No coercion operator is defined between types
'System.Collections.Generic.List`1[System.Object]' and 'MyCollection'.
Any help would be really appreciated.

I don't see that you need a custom resolver, try this:
Mapper.Initialize(cfg =>
{
cfg.CreateMap<SourceItem, DestItem>()
.ForMember(d => d.ShipsTo, o => o.MapFrom(src => new MyCollection(src.ShipsTo)));
});

The answer is: you cannot use custom value resolver here, because there is a bug (I have added item to backlog, you can track it). But you can specify conversion between List<string> and MyCollection as Jimmy suggested. And everything will work fine:
Mapper.Initialize(cfg =>
{
cfg.CreateMap<List<string>, MyCollection>().ConvertUsing(s => new MyCollection(s));
cfg.CreateMap<SourceItem, DestItem>();
});
BTW Seems like issue already fixed, and it should be available in next release of Automapper.

I could not really explain it, but it works, if you define explicit mapping configuration from MyCollection to MyCollection:
Mapper.Initialize(cfg =>
{
cfg.CreateMap<MyCollection, MyCollection>().ConvertUsing((a,b)=>a);
cfg.CreateMap<SourceItem, DestItem>()
.ForMember(d => d.ShipsTo, o => o.ResolveUsing<CustomResolver>());
});
I have debugged code of automapper to find why. Reason is, Automapper is looking for suitable mapper to map MyCollection to MyCollection. It looks in a collection of all mappers (defined in your config and predefined). Actually, what it should find is a AssignableMapper, that just assigns one variable to another, but first he checks an EnumerableMapper and it looks ok, because your CollectionBase is Enumerable. The exception comes from this EnumerableMapper, because it just couldn't map this two Collections. Solution is to explicit define, that it just should assign one variable to another.
But. I am agree with Sergey. This is some ugly bug, what i have suggested is exact the same ugly workaround. But i've managed get it to work :)

Related

How to map an ICollection<T> to Class derived from List<T>

I am setting up a mapping between my models and my view models and I'm trying to map from an ICollection to class that derives from List
I have tried to make a mapping between my ListItemClassVM and ICollection but get an error 'Argument types do not match'
Option one mapping works with this:
public class ParentVM
{
public List<ListItemClass> ListItemClasses { get; set; }
}
Option two mapping not working:
public class ParentVM
{
public ListItemClassVM ListItemClasses { get; set; }
}
public ListItemClassVM : List<ListItemClass>
{
}
Mapping Setup:
public ModelClass_ParentVM_Profile()
{
CreateMap<ModelClass, ParentVM>()
.ForMember(d => d.ListItemClasses, o => o.MapFrom(i => i.ModelCollection))
;
CreateMap<ParentVM, ModelClass>()
;
}
trying to setup the mapping so option two will map.
I think that there are more way to reach the solution, but you can't escape from a manual transposition from ICollection< ListItemClass > to ListItemClassVM.
The simplier way maybe is to add to your ListItemClassVM a constructor that accepts an ICollection< ListItemClass > and initialize itself with the elements in ICollection, then you could do something like:
CreateMap<ModelClass, ParentVM>()
.ForMember(d => d.ListItemClasses, o => o.MapFrom(i =>new ListItemClassVM (i.ModelCollection)))
;

Automapper - exclude some objects from mapped collection

I have the following map rules:
CreateMap<ViewModels.ApplicationDriverAccidentFormVM, ApplicationDriverAccidentDomain>();
then I want to map ViewModels.ApplicationDriverFormVM to ApplicationDriverDomain, both are have Accidents property, which are appropriate collections for each type.
public class ApplicationDriverDomain
{
public List<ApplicationDriverAccidentDomain> Accidents { get; set; }
}
public class ApplicationDriverFormVM
{
public List<ApplicationDriverAccidentFormVM> Accidents { get; set; }
}
And I want to exclude (not map) all records, which are not satisfied some conditions
I try to write the following code:
CreateMap<ViewModels.ApplicationDriverFormVM, ApplicationDriverDomain>()
.ForMember(dest => dest.Accidents, opt => opt.MapFrom(src => GetNotNullFromCollection(src.Accidents)))
where GetNotNullFromCollection is:
List<object> GetNotNullFromCollection(object input)
{
List<object> output = new List<object>();
foreach (var item in (List<object>)input)
{
if (!Utils.IsAllNull(item))
output.Add(item);
}
return output;
}
but it says me:
Unable to cast object of type
'System.Collections.Generic.List1[Web.ViewModels.ApplicationDriverAccidentFormVM]'
to type 'System.Collections.Generic.List1[System.Object]'.
Why and how to do it?
your method GetNotNullFromCollection receives an object but you are passing it a list.
Anyway, I would recommend using Generics instead of objects.
I solved it by the following way:
CreateMap<ViewModels.ApplicationDriverFormVM, ApplicationDriverDomain>().ForMember(dest => dest.Accidents, opt => opt.MapFrom(src => src.Accidents.Where(o => !Utils.IsAllNull(o))))

How to configure a mapping of collection types with AutoMapper?

For example, I've implemented two classes like these:
public class A
{
public List<C> Items { get; set; }
}
public class B
{
public IImmutableList<C> Items { get; set; }
}
public class C
{
}
When I try to map A to B and vice versa, I get an exception because List<string> cannot be converted to IImmutable<string>.
Probably I could provide a mapping for A<->B, but since it'll be a very common pattern in my solution, I'd like to avoid to manually mapping each class that may fall into the same case.
Is there anyway I can generalize the whole mapping using generic type definitions from a collection type to another collection type?
This is what I want to avoid
mapperConfig.CreateMap<A, B>()
.ForMember(a => a.Items, opts => opts.Ignore())
.AfterMap
(
(source, target) =>
{
target.Items = source.Items.ToImmutableList();
}
);

AutoMapper convert from multiple sources

Let's say I have two model classes:
public class People {
public string FirstName {get;set;}
public string LastName {get;set;}
}
Also have a class Phone:
public class Phone {
public string Number {get;set;}
}
And I want to convert to a PeoplePhoneDto like this:
public class PeoplePhoneDto {
public string FirstName {get;set;}
public string LastName {get;set;}
public string PhoneNumber {get;set;}
}
Let's say in my controller I have:
var people = repository.GetPeople(1);
var phone = repository.GetPhone(4);
// normally, without automapper I would made
return new PeoplePhoneDto(people, phone) ;
Is this possible ?
You cannot directly map many sources to single destination - you should apply maps one by one, as described in Andrew Whitaker answer. So, you have to define all mappings:
Mapper.CreateMap<People, PeoplePhoneDto>();
Mapper.CreateMap<Phone, PeoplePhoneDto>()
.ForMember(d => d.PhoneNumber, a => a.MapFrom(s => s.Number));
Then create destination object by any of these mappings, and apply other mappings to created object. And this step can be simplified with very simple extension method:
public static TDestination Map<TSource, TDestination>(
this TDestination destination, TSource source)
{
return Mapper.Map(source, destination);
}
Usage is very simple:
var dto = Mapper.Map<PeoplePhoneDto>(people)
.Map(phone);
You could use a Tuple for this:
Mapper.CreateMap<Tuple<People, Phone>, PeoplePhoneDto>()
.ForMember(d => d.FirstName, opt => opt.MapFrom(s => s.Item1.FirstName))
.ForMember(d => d.LastName, opt => opt.MapFrom(s => s.Item1.LastName))
.ForMember(d => d.Number, opt => opt.MapFrom(s => s.Item2.Number ));
In case you would have more source models you can use a different representation (List, Dictionary or something else) that will gather all these models together as a source.
The above code should preferaby be placed in some AutoMapperConfiguration file, set once and globally and then used when applicable.
AutoMapper by default supports only a single data source. So there is no possibility to set directly multiple sources (without wrapping it up in a collection) because then how would we know what in case if for example two source models have properties with the same names?
There is though some workaround to achieve this:
public static class EntityMapper
{
public static T Map<T>(params object[] sources) where T : class
{
if (!sources.Any())
{
return default(T);
}
var initialSource = sources[0];
var mappingResult = Map<T>(initialSource);
// Now map the remaining source objects
if (sources.Count() > 1)
{
Map(mappingResult, sources.Skip(1).ToArray());
}
return mappingResult;
}
private static void Map(object destination, params object[] sources)
{
if (!sources.Any())
{
return;
}
var destinationType = destination.GetType();
foreach (var source in sources)
{
var sourceType = source.GetType();
Mapper.Map(source, destination, sourceType, destinationType);
}
}
private static T Map<T>(object source) where T : class
{
var destinationType = typeof(T);
var sourceType = source.GetType();
var mappingResult = Mapper.Map(source, sourceType, destinationType);
return mappingResult as T;
}
}
And then:
var peoplePhoneDto = EntityMapper.Map<PeoplePhoneDto>(people, phone);
But to be quite honest, even though I am using AutoMapper for already a few years I have never had a need to use mapping from multiple sources.
In cases when for example I needed multiple business models in my single view model I simply embedded these models within the view model class.
So in your case it would look like this:
public class PeoplePhoneDto {
public People People { get; set; }
public Phone Phone { get; set; }
}
Try this if you're using C# 7+ (a slight variation of #Paweł Bejgerthat's answer that will make it even simpler):
Mapper.CreateMap<(People people, Phone phone), PeoplePhoneDto>()
.ForMember(d => d.FirstName, opt => opt.MapFrom(s => s.people.FirstName))
.ForMember(d => d.LastName, opt => opt.MapFrom(s => s.people.LastName))
.ForMember(d => d.Number, opt => opt.MapFrom(s => s.phone.Number ));
And then use it like this:
var peoplePhoneDto = EntityMapper.Map<PeoplePhoneDto>((people, phone));
And yes, you will need a couple of brackets around the arguments, it's not a mistake. The reason behind it is that you're passing one single source (not two) which happens to be a (People, Phone) tuple.
I'd write an extension method as below:
public static TDestination Map<TSource1, TSource2, TDestination>(
this IMapper mapper, TSource1 source1, TSource2 source2)
{
var destination = mapper.Map<TSource1, TDestination>(source1);
return mapper.Map(source2, destination);
}
Then usage would be:
mapper.Map<People, Phone, PeoplePhoneDto>(people, phone);
perhaps it sounds to be an old post but there might be some guys still struggling with the same issue, referring to AutoMapper IMapper Map function documentation, we can reuse the same existing destination object for mapping from a new source, provided that you already created a map for each source to destination in profile, then you can use this simple Extention method:
public static class MappingExtentions
{
public static TDestination Map<TDestination>(this IMapper mapper, params object[] sources) where TDestination : new()
{
return Map(mapper, new TDestination(), sources);
}
public static TDestination Map<TDestination>(this IMapper mapper, TDestination destination, params object[] sources) where TDestination : new()
{
if (!sources.Any())
return destination;
foreach (var src in sources)
destination = mapper.Map(src, destination);
return destination;
}
}
please note that I have created a constraint for destination type which says it must be an instantiate-able type. if your type is not like that use default(TDestination) instead of new TDestination().
Warning: this type of mapping is a bit dangerous sometimes because the destination mapping properties might be overwritten by multiple sources and tracing the issue in larger apps can be a headache, there is a loose workaround you can apply, you can do as below, but again it is not a solid solution at all:
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string PhoneNumber { get; set; }
}
public class Contact
{
public string Address { get; set; }
public string PhoneNumber { get; set; }
public string Other{ get; set; }
}
public class PersonContact
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Address{ get; set; }
public string PhoneNumber { get; set; }
}
public class PersonMappingProfile : MappingProfile
{
public PersonMappingProfile()
{
this.CreateMap<Person, PersonContact>();
this.CreateMap<Phone, PersonContact>()
.ForMember(dest => dest.PhoneNumber, opt => opt.MapFrom((src, dest) => dest.PhoneNumber ?? src.PhoneNumber)) // apply mapping from source only if the phone number is not already there, this way you prevent overwriting already initialized props
.ForAllOtherMembers(o => o.Ignore());
}
}
There is a breaking change in AutoMapper 9.0 that no longer provides an API for static Mapper. So, we need to use an instance now. For those using the newer versions, an extension method that uses inferred types with/without a destination object follow:
public static class AutoMapperExtensions
{
public static TDestination Map<TDestination>(this IMapper mapper, params object[] source) where TDestination : class
{
TDestination destination = mapper.Map<TDestination>(source.FirstOrDefault());
foreach (var src in source.Skip(1))
destination = mapper.Map(src, destination);
return destination;
}
public static TDestination Map<TDestination>(this IMapper mapper, TDestination destination, params object[] source) where TDestination : class
{
foreach (var src in source)
destination = mapper.Map(src, destination);
return destination;
}
}
Using FluentAPI style for better discoverability and guidance usage.
public static class MapperExtensions
{
public static IMultiMapBuilder<TDestination> StartMultiMap<TDestination>(this IMapper mapper, object source)
{
return new MultiMapBuilder<TDestination>(mapper, source);
}
}
public interface IMultiMapBuilder<T>
{
IMultiMapBuilder<T> Then<TSource>(TSource source);
T Map();
}
public class MultiMapBuilder<T> : IMultiMapBuilder<T>
{
private readonly IMapper _mapper;
private readonly T _mappedObject;
public MultiMapBuilder(IMapper mapper, object source)
{
_mapper = mapper;
_mappedObject = mapper.Map<T>(source);
}
public IMultiMapBuilder<T> Then<TSource>(TSource source)
{
_mapper.Map(source, _mappedObject);
return this;
}
public T Map()
{
return _mappedObject;
}
}
Sample Usage:
//-- By IMapper Extension
var mapped = _mapper.StartMultiMap<SomeType>(source1)
.Then(source2)
.Then(source3)
.Map();
or
//-- new instance of MultiMapBuilder
var mapped = new MultiMapBuilder<SomeType>(_mapper, source1)
.Then(source2)
.Then(source3)
.Map();
If you have a scenario when destination type should be mapped from one of sources and you want to use linq projections, you can do following.
Mapper.CreateMap<People, PeoplePhoneDto>(MemberList.Source);
Mapper.CreateMap<Phone, PeoplePhoneDto>(MemberList.Source)
.ForMember(d => d.PhoneNumber, a => a.MapFrom(s => s.Number));
CreateMap<PeoplePhoneDto,(People,Phone)>(MemberList.Destination)
.ForMember(x => x.Item1, opts => opts.MapFrom(x => x))
.ForMember(x => x.Item2, opts => opts.MapFrom(x => x.PhoneNumber))
.ReverseMap();
I needed this mostly for cross apply queries like this.
var dbQuery =
from p in _context.People
from ph in _context.Phones
.Where(x => ...).Take(1)
select ValueTuple.Create(p, ph);
var list = await dbQuery
.ProjectTo<PeoplePhoneDto>(_mapper.ConfigurationProvider)
.ToListAsync();
There's lots of options already provided, but none of them really fit what I wanted. I was falling asleep last night and had the thought:
Lets say you want to map your two classes, People and Phone to PeoplePhoneDto
public class People {
public string FirstName {get;set;}
public string LastName {get;set;}
}
+
public class Phone {
public string Number {get;set;}
}
=
public class PeoplePhoneDto {
public string FirstName {get;set;}
public string LastName {get;set;}
public string PhoneNumber {get;set;}
}
All you really need is another wrapper class for Automapper purposes.
public class PeoplePhone {
public People People {get;set;}
public Phone Phone {get;set;}
}
And then define the mapping:
CreateMap<PeoplePhone, PeoplePhoneDto>()
And use it
var dto = Map<PeoplePhoneDto>(new PeoplePhone
{
People = people,
Phone = phone,
});

Automapper and class hierarchy

Given the following sources:
public class SourceBase { public string TheString { get; set; } }
public class SourceDerived : SourceBase { }
and destinations:
public class DestBase { public string MyString { get; set; } }
public class DestDerived : DestBase { }
And this mapping:
CreateMap<SourceBase, DestBase>()
.ForMember(dest => dest.MyString, o => o.MapFrom(x => x.TheString))
.Include<SourceDerived, DestDerived>();
CreateMap<SourceDerived, DestDerived>();
Mapper.AssertConfigurationIsValid(); // Exception is thrown here
However, this gives a mapping error saying MyString isn't mapped on DestDerived. What gives? Do I really need to repeat the mappings for base class properties in all derived types (I do have more than one subclass in my actual code).
EDIT:
The exact exception is The following 1 properties on DestDerived could not be mapped: MyString. Add a custom mapping expression, ignore, or rename the property on DestDerived.
Please check this post:
http://groups.google.com/group/automapper-users/browse_thread/thread/69ba514a521e9599
It works fine if you declare it like in the code below (using AutoMapper 1.1.0.188). I am not sure if this solves your problem.
var result = Mapper.CreateMap<SourceBase, DestBase>()
.ForMember(dest => dest.MyString, o => o.MapFrom(x => x.TheString));
//.Include<SourceDerived, DestDerived>();
Mapper.CreateMap<SourceDerived, DestDerived>();
var source = new SourceDerived();
var destDerived = new DestDerived();
source.TheString = "teststring";
var mapResult = Mapper.Map<SourceBase, DestBase>(source, destDerived).MyString;
Console.WriteLine(mapResult);

Categories