AutoMapper从多个来源转换

时间:2014-01-28 18:02:03

标签: c# automapper

假设我有两个模型类:

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

还有一个课程电话:

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

我想转换为像这样的PeoplePhoneDto:

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

我似乎无法找到这种情况的任何示例。这可能吗?

注意:示例不是真实的,仅针对此问题。

9 个答案:

答案 0 :(得分:81)

您无法直接将多个来源映射到单个目的地 - 您应该逐个应用地图,如Andrew Whitaker答案中所述。因此,您必须定义所有映射:

Mapper.CreateMap<People, PeoplePhoneDto>();
Mapper.CreateMap<Phone, PeoplePhoneDto>()
        .ForMember(d => d.PhoneNumber, a => a.MapFrom(s => s.Number));

然后通过任何这些映射创建目标对象,并将其他映射应用于创建的对象。这个步骤可以通过非常简单的扩展方法简化:

public static TDestination Map<TSource, TDestination>(
    this TDestination destination, TSource source)
{
    return Mapper.Map(source, destination);
}

用法非常简单:

var dto = Mapper.Map<PeoplePhoneDto>(people)
                .Map(phone);

答案 1 :(得分:15)

您可以使用Tuple

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

如果您有更多的源模型,您可以使用不同的表示(List,Dictionary或其他),将所有这些模型作为源集合在一起。

上面的代码应该优先放在一些AutoMapperConfiguration文件中,设置一次并全局设置,然后在适用时使用。

默认情况下,AutoMapper仅支持单个数据源。所以不可能直接设置多个源(没有将它包装在一个集合中),因为那样如果两个源模型具有相同名称的属性,我们怎么知道呢?

虽然有一些解决方法可以达到这个目的:

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

然后:

var peoplePhoneDto = EntityMapper.Map<PeoplePhoneDto>(people, phone);

但说实话,即使我已经使用AutoMapper几年了,我也从未需要使用来自多个来源的映射。 例如,当我在单视图模型中需要多个业务模型时,我只是将这些模型嵌入到视图模型类中。

所以在你的情况下,它看起来像这样:

public class PeoplePhoneDto {
    public People People { get; set; }
    public Phone Phone { get; set; }
}

答案 2 :(得分:1)

我将编写如下扩展方法:

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

那么用法是:

    mapper.Map<People, Phone, PeoplePhoneDto>(people, phone);

答案 3 :(得分:1)

也许听起来是老文章,但也许​​有些人仍在为同一问题而苦苦挣扎,请参考AutoMapper IMapper Map function documentation,我们可以重用相同的现有目标对象以从新来源进行映射,前提是您已经为配置文件中每个来源到目的地的地图创建了一个地图,那么您可以使用以下简单的扩展方法:

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

请注意,我已经为目标类型创建了一个约束,该约束说它必须是可实例化的类型。如果您的类型不是这样,请使用default(TDestination)而不是new TDestination()

警告:这种类型的映射有时会有些危险,因为目标映射属性可能会被多个源覆盖,而在大型应用程序中跟踪问题可能会让人头疼,您可以采用一种宽松的解决方法可以申请,您可以执行以下操作,但这仍然不是一个可靠的解决方案:

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

答案 4 :(得分:1)

如果您使用的是 C# 7+,请尝试此操作(@Paweł Bejger 的答案略有不同,这将使它更简单):

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

然后像这样使用它:

var peoplePhoneDto = EntityMapper.Map<PeoplePhoneDto>((people, phone));

是的,输入的每一侧都需要几个括号,这不是错误。其背后的原因是您传递的是一个(而不是两个)源,它恰好是一个(人物、电话)元组。

答案 5 :(得分:1)

使用 FluentAPI 风格以获得更好的可发现性和指导使用。

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

示例用法:

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


答案 6 :(得分:0)

如果您有一种情况,应该从一种来源映射目标类型,并且想要使用linq投影,则可以执行以下操作。

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

我主要需要这样的交叉应用查询。

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

答案 7 :(得分:0)

已经提供了很多选项,但是没有一个选项真正适合我的需求。昨晚我睡着了,心想:

假设您想将PeoplePhone这两个类映射到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;}
}

您真正需要的是另一个用于Automapper的包装器类。

public class PeoplePhone {
    public People People {get;set;}
    public Phone Phone {get;set;}
}

然后定义映射:

CreateMap<PeoplePhone, PeoplePhoneDto>()

并使用它

var dto = Map<PeoplePhoneDto>(new PeoplePhone
{
    People = people,
    Phone = phone,
});

答案 8 :(得分:0)

AutoMapper 9.0 有一个重大变化,不再为静态 Mapper 提供 API。所以,我们现在需要使用一个实例。对于那些使用较新版本的人,使用带有/不带有目标对象的推断类型的扩展方法如下:

Tuple[Callable, ...]