Automapper: passing parameter to Map method

C#.NetAutomapper 3

C# Problem Overview


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

C# Solutions


Solution 1 - C#

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.

Solution 2 - C#

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.

Solution 3 - C#

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

Solution 4 - C#

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 (..){..}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestiondaviooohView Question on Stackoverflow
Solution 1 - C#RichardView Answer on Stackoverflow
Solution 2 - C#SvenView Answer on Stackoverflow
Solution 3 - C#Sam GoldmannView Answer on Stackoverflow
Solution 4 - C#Игорь КириченкоView Answer on Stackoverflow