Automapper: 如何将参数传递给 Map 方法

75

我正在一个项目中使用Automapper,我需要动态赋值目标对象的一个字段。

在我的配置中,我有类似以下的内容:

cfg.CreateMap<Message, MessageDto>()
    // ...
    .ForMember(dest => dest.Timestamp, opt => opt.MapFrom(src => src.SentTime.AddMinutes(someValue)))
    //...
    ;

配置代码中的someValue是我需要在运行时传递给映射器的参数,而不是源对象的字段。

有没有办法实现这个?类似这样:

Mapper.Map<MessageDto>(msg, someValue));

这可能会有所帮助:https://dev59.com/QFwZ5IYBdhLWcg3wLdyz - Richard Garside
2
对于正确使用valorize,给予高分。 - HopAlongPolly
4个回答

99

你无法完全按照自己的意愿进行操作,但是当您调用Map函数时,可以通过指定映射选项来接近所需结果。在您的配置中忽略该属性:

你不能完全按照想要的方式去做,但是当你调用Map方法时,可以通过指定映射选项来接近想要的效果。忽略你配置中的该属性:

cfg.CreateMap<Message, MessageDto>()
    .ForMember(dest => dest.Timestamp, opt => opt.Ignore());

然后在调用您的地图时传入选项:

int someValue = 5;
var dto = Mapper.Map<Message, MessageDto>(message, opt => 
    opt.AfterMap((src, dest) => dest.TimeStamp = src.SendTime.AddMinutes(someValue)));

请注意,您需要使用 Mapper.Map<TSrc, TDest> 重载以使用此语法。


完美,这正是我所需要的!谢谢! - davioooh
27
在这种情况下,不是使用 AfterMap 方法,而是直接将 dto.TimeStamp 赋值为 message.SendTime.AddMinutes(someValue) 会更容易。 - Boris Lipschitz
1
当属性的setter为private/protected时,此解决方案无法使用。 - leavelllusion
3
@Window 这个解决方案并不总是发生,除非在进行 CreateMap 时在您的配置文件中定义了 AfterMap,但这里并没有这样做。 - ataravati
如何对两个列表的映射执行相同的操作?例如:var destinations = iMapper.Map<List<AuthorModel>, List<AuthorDTO>>(sources); - Dimuthu
显示剩余6条评论

28

使用 Map 方法时,另一个可能的选项是使用 Items 字典。例如:

int someValue = 5;
var dto = Mapper.Map<Message>(message, 
    opts => opts.Items["Timestamp"] = message.SentTime.AddMinutes(someValue));

这段代码稍微少了一点,并且具有动态指定字段的优点。


1
这个链接http://codebuckets.com/2016/09/24/passing-parameters-with-automapper/提供了一个更好的使用Items的解决方案。 - Dan
@Dan,这篇文章中的解决方案在属性setter为private/protected时无法使用。 - leavelllusion
根据@Dan的解决方案,如果您正在使用新版本的automapper,请注意:从automapper 8.0开始,ResovleUsing被MapFrom替换 8.0升级指南 - Kevin Hernández

14

你可以使用自定义的 ITypeConverter<TSource, TDestination> 实现完全按照你想要的方式进行操作。

  1. 在调用 Map 时,可以使用第二个回调参数来配置转换上下文中的自定义参数。
  2. 在你的自定义类型转换器的 Convert 方法中,可以从传递的上下文作为第三个参数中恢复你的参数。

完整解决方案:

namespace BegToDiffer
{
    using AutoMapper;
    using System;

    /// <summary>
    /// "Destiantion" type.
    /// </summary>
    public class MessageDto
    {
        public DateTime SentTime { get; set; }
    }

    /// <summary>
    /// "Source" type.
    /// </summary>
    public class Message
    {
        public DateTime Timestamp { get; set; }
    }

    /// <summary>
    /// Extension methods to make things very explicit.
    /// </summary>
    static class MessageConversionExtensions
    {
        // Key used to acccess time offset parameter within context.
        static readonly string TimeOffsetContextKey = "TimeOffset";

        /// <summary>
        /// Recovers the custom time offset parameter from the conversion context.
        /// </summary>
        /// <param name="context">conversion context</param>
        /// <returns>Time offset</returns>
        public static TimeSpan GetTimeOffset(this ResolutionContext context)
        {
            if (context.Items.TryGetValue(TimeOffsetContextKey, out var timeOffset))
            {
                return (TimeSpan)timeOffset;
            }

            throw new InvalidOperationException("Time offset not set.");
        }

        /// <summary>
        /// Configures the conversion context with a time offset parameter.
        /// </summary>
        /// <param name="options"></param>
        /// <param name="timeOffset"></param>
        public static IMappingOperationOptions SetTimeOffset(this IMappingOperationOptions options, TimeSpan timeOffset)
        {
            options.Items[TimeOffsetContextKey] = timeOffset;
            // return options to support fluent chaining.
            return options; 
        }
    }

    /// <summary>
    /// Custom type converter.
    /// </summary>
    class MessageConverter : ITypeConverter<Message, MessageDto>
    {
        public MessageDto Convert(Message source, MessageDto destination, ResolutionContext context)
        {
            if (destination == null)
            {
                destination = new MessageDto();
            }

            destination.SentTime = source.Timestamp.Add(context.GetTimeOffset());

            return destination;
        }
    }

    public class Program
    {
        public static void Main()
        {
            // Create a mapper configured with our custom type converter.
            var mapper = new MapperConfiguration(cfg =>
                cfg.CreateMap<Message, MessageDto>().ConvertUsing(new MessageConverter()))
                    .CreateMapper();

            // Setup example usage to reflect original question.
            int someValue = 5;
            var msg = new Message { Timestamp = DateTime.Now };

            // Map using custom time offset parameter.
            var dto = mapper.Map<MessageDto>(msg, options => options.SetTimeOffset(TimeSpan.FromMinutes(someValue)));

            // The proof is in the pudding:
            Console.WriteLine("msg.Timestamp = {0}, dto.SentTime = {1}", msg.Timestamp, dto.SentTime);
        }
    }
}

这是一个非常好的答案!代码清晰,内置扩展方法使最终代码非常易读。干得好! - Suncat2000
这很棒。但是我还在其他应用程序中使用映射器,而我不想在同一对象上发送参数。因此,在不使用SetTimeOffset()和GetTimeOffset的情况下使用Map会抛出异常,因为Items未定义。我尝试了一个if检查context.Items.IsNullOrEmpty(),但它仍然会抛出此异常:必须使用带有Action<IMappingOperationOptions>的Map重载! - Mihai Socaciu

1

我有一个通用的扩展方法版本:

    public static class AutoMapperExtensions
    {
        public static TDestination Map<TSource, TDestination>(this IMapper mapper, TSource value,
            params (string, object)[] additionalMap)
        {
            return mapper.Map<TSource, TDestination>(value,
                opt => opt.AfterMap(
                    (src, dest) => additionalMap.ForEach(am =>
                    {
                        var (propertyName, value) = am;
                        var property = typeof(TDestination).GetProperty(propertyName);
                        property.SetValue(dest, value, null);
                    })));
        }
    }

在使用之前,您必须忽略额外的属性:

CreateMap<User, AuthenticateResponse>().ForMember(ar => ar.Token, opt => opt.Ignore());

使用中:
private readonly IMapper _mapper;
...
return _mapper.Map<User, AuthenticateResponse>(user, (nameof(AuthenticateResponse.Token), token));

同时你需要IEnumerable扩展:
public static void ForEach<T>(this IEnumerable<T> source, Action<T> action)
{
    foreach (var item in source)
    {
        action(item);
    }
}

或者您可以将additionalMap.ForEach更改为foreach(..){..}


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接