74

I'm using Automapper in a project and I need to dynamically valorize a field of my destination object.

In my configuration I have something similar:

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

The someValue in the configuration code is a parameter that I need to pass at runtime to the mapper and is not a field of the source object.

Is there a way to achieve this? Something like this:

Mapper.Map<MessageDto>(msg, someValue));
davioooh
  • 23,742
  • 39
  • 159
  • 250

4 Answers4

97

You can't do exactly what you want, but you can get pretty close by specifying mapping options when you call Map. Ignore the property in your config:

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

Then pass in options when you call your map:

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

Note that you need to use the Mapper.Map<TSrc, TDest> overload to use this syntax.

wonea
  • 4,783
  • 17
  • 86
  • 139
Richard
  • 29,854
  • 11
  • 77
  • 120
  • Perfect, this is exactly what I need! Thanks! – davioooh Dec 22 '15 at 15:54
  • 27
    wouldn't it be easier in this case instead of AfterMap just assign dto.TimeStamp = message.SendTime.AddMinutes(someValue); – Boris Lipschitz Mar 30 '16 at 04:47
  • This solution works only for flat mappings as if you map a tree of objects you cannot manage this manually like this...along with having the "opt.AfterMap" code multiplies by the amount of time you map the object in your project and is harder to manage and fix in one location unless you encapsulate your whole map function. – Royi Mindel Jun 13 '16 at 08:55
  • @BorisLipschitz That's easier for a one time use; but, if you plan to use your mapping in more than one spot then you will need to write your code every time. This solution happens every time you do `Mapper.Map` – Window Feb 26 '19 at 02:00
  • 1
    This solution does not work when a property setter is private/protected. – leavelllusion Mar 26 '19 at 18:23
  • @Window, no, this doesn't happen every time you do `Mapper.Map`. – ataravati Jun 15 '20 at 23:38
  • @ataravati Can you elaborate? – Window Jun 17 '20 at 00:46
  • 2
    @Window This solution, as is, doesn't happen every time, unless you define `AfterMap` in your profile when doing `CreateMap`, which is not the case here. – ataravati Jun 17 '20 at 00:55
  • How do I do the same for a mapping of two Lists? ex: `var destinations = iMapper.Map, List>(sources);` – Dimuthu Mar 26 '21 at 14:44
  • @Dimuthu Same syntax: add the options action in your Map call. – Suncat2000 Oct 22 '21 at 20:26
  • Thanks for this. I was hunting for an example of using the Map overload with options for ages. – Steve Crane Mar 08 '23 at 09:47
28

Another possible option while using the Map method would be the usage of the Items dictionary. Example:

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

It's a little bit less code and has the advantage of dynamically specified fields.

Sven
  • 281
  • 3
  • 4
  • 1
    This link http://codebuckets.com/2016/09/24/passing-parameters-with-automapper/ has a better solution for using Items – Dan Sep 03 '18 at 11:32
  • @Dan, the solution from this article does not work when a property setter is private/protected. – leavelllusion Mar 26 '19 at 18:25
  • According to @Dan solution, and if you are using a new version of automapper note this: **From automapper 8.0, ResovleUsing was replaced by MapFrom** [8.0 Upgrade Guide](https://docs.automapper.org/en/stable/8.0-Upgrade-Guide.html) – Kevin Hernández Jun 19 '20 at 00:54
13

You can absolutely do exactly what you want using a custom ITypeConverter<TSource, TDestination> implementation.

  1. When invoking Map, you can configure the conversion context with your custom parameter(s) using the second callback argument.
  2. In the Convert method of your customer type converter, you can recover your parameter(s) from the context which is passed as the third parameter.

Complete solution:

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);
        }
    }
}
Sam Goldmann
  • 131
  • 1
  • 3
  • That is an exceptionally good answer! The code is clear and the built-in extension method makes the end code very readable. Well done! – Suncat2000 Oct 06 '21 at 20:14
  • This is great. But I also use mapper in other app places where I don't want to send parameters on the same object. So, use Map without SetTimeOffset() and GetTimeOffset throws because Items is undefined. I tried an if check context.Items.IsNullOrEmpty() but it still throws this exception: You must use a Map overload that takes Action! – Mihai Socaciu Apr 10 '23 at 21:17
1

I have generic extension method version:

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

Begore using you must ignore additional properties:

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

Using:

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

Also you need IEnumerable extension:

public static void ForEach<T>(this IEnumerable<T> source, Action<T> action)
{
    foreach (var item in source)
    {
        action(item);
    }
}

Or you can change additionalMap.ForEach to foreach (..){..}