自动映射器从多个源转换

假设我有两个模型类:

public class People {
public string FirstName {get;set;}
public string LastName {get;set;}
}

也有一个类电话:

public class Phone {
public string Number {get;set;}
}

我想转换成这样的 PeoplePhone:

public class PeoplePhoneDto {
public string FirstName {get;set;}
public string LastName {get;set;}
public string PhoneNumber {get;set;}
}

比方说,在我的控制器里,我有:

var people = repository.GetPeople(1);
var phone = repository.GetPhone(4);


// normally, without automapper I would made
return new PeoplePhoneDto(people, phone) ;

这可能吗?

75582 次浏览

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

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

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

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

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

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

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.

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