使用AutoMapper对嵌套类型进行内联(覆盖)映射

时间:2019-05-23 11:34:38

标签: c# mapping automapper

我想使用AutoMapper并在运行时提供一些值。

例如,我有DTO和ViewModel。

DTO中不存在该属性之一,并且不能使用Converters / Resolvers / Transformers直接对其进行映射;

namespace Lab.So.Sample
{
    public class UserDto
    {
        public int UserId { get; set; }
        public string UserCode { get; set; }
    }

    public class UserGroupDto
    {
        public List<UserDto> Users { get; set; }
    }

    public class UserViewModel
    {
        public int UserId { get; set; }
        public string FullName { get; set; }
    }

    public class UserGroup
    {
        public List<UserViewModel> Users { get; set; }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var src = new Fixture().Create<UserGroupDto>();

            Mapper.Initialize(cfg =>
            {
                cfg.CreateMissingTypeMaps = true;
                cfg.ValidateInlineMaps = false;
            });

            // How to hook mapping of nested object User->UserViewModel to provide value of FullName in runtime
            var dst = Mapper.Map<UserGroupDto, UserGroup>(src);
        }
    }
}

我有一些解决方法,但就我而言,这并不友好:

class Program
    {
        internal class UserViewModelFullNameResolver : IValueResolver<UserDto, UserViewModel, string>
        {
            public string Resolve(UserDto source, UserViewModel destination, string destMember, ResolutionContext context)
            {
                var names = context.Items["ctx.Names"] as IDictionary<int, string>;

                if (names == null || !names.TryGetValue(source.UserId, out var fullName))
                {
                    return null;
                }

                return fullName;
            }
        }

        static void Main(string[] args)
        {
            var src = new Fixture().Create<UserGroupDto>();

            Mapper.Initialize(cfg =>
            {
                cfg.CreateMissingTypeMaps = true;
                cfg.ValidateInlineMaps = false;

                cfg.CreateMap<UserDto, UserViewModel>()
                    .ForMember(d => d.FullName, opt => opt.MapFrom<UserViewModelFullNameResolver>());
            });

            var names = new Dictionary<int, string>
            {
                { 10, "FullName-10" },
                { 20, "FullName-20" },
            };

            var dst = Mapper.Map<UserGroupDto, UserGroup>(src, opt=>opt.Items["ctx.Names"] = names);
        }
    }

在这种解决方法中,最不方便的是opt.Items中的键名一致; 如果由于意外而导致拼写错误,则很难进行调查和修复;

我看起来像这样:

var dst = Mapper.Map<UserGroupDto, UserGroup>(src, opt=>opt.Use(new UserViewModelFullNameResolver());

换句话说,它在运行时为每种唯一情况定义了一个解析器实例;

如果我能够定义钩子来映射对象图中的特定类型,我也将接受:

var dst = Mapper.Map<UserGroupDto, UserGroup>(src, opt=>opt.Hook<UserDto,UserViewModel>((s,d)=> { /* any logic to read external data */ });

用法示例:

var srcA = readDataA();
var srcB = readDataB();

var dst = Mapper.Map<UserGroupDto, UserGroup>(
  src, 
  opt=>opt.Hook<UserDto,UserViewModel>(
  (s,d)=> 
  {
      d.FullName = srcA + srcB; 
  });

请提出一些建议,如果源在目的地中没有嵌套对象的所有必需数据,则将有助于读取数据以完成映射。

1 个答案:

答案 0 :(得分:0)

我能够使用现有的API归档目标:

public static class InlineMappingExtensions
    {
        public static void EnableInlineMapping(this IMapperConfigurationExpression cfg)
        {
            cfg.ForAllMaps((t, i) =>
            {
                i.AfterMap((s, d, ctx) =>
                    {
                        ctx.ApplyInlineMap(s, d);
                    }
                );
            });
        }

        public static IMappingOperationOptions OnMap<TSrc, TDst>(this IMappingOperationOptions opts,
            Action<TSrc, TDst> resolve)
        {
            var srcTypeName = typeof(TSrc);
            var dstTypeName = typeof(TDst);

            var ctxKey = $"OnMap_{srcTypeName}_{dstTypeName}";

            opts.Items.Add(ctxKey, resolve);

            return opts;
        }

        private static void ApplyInlineMap(this ResolutionContext opts, object src, object dst)
        {
            if (src == null)
            {
                return;
            }

            if (dst == null)
            {
                return;
            }

            var srcTypeName = src.GetType();
            var dstTypeName = dst.GetType();

            var ctxKey = $"OnMap_{srcTypeName}_{dstTypeName}";

            if (!opts.Items.TryGetValue(ctxKey, out var inlineMap))
            {
                return;
            }

            var act = inlineMap as Delegate;
            act?.DynamicInvoke(src, dst);
        }
    }

要启用它:

            Mapper.Initialize(cfg =>
            {
                cfg.CreateMissingTypeMaps = true;
                cfg.ValidateInlineMaps = false;


                cfg.EnableInlineMapping();
            });

用法示例:

// read data from external sources
            var names = new Dictionary<int, string>
            {
                { 10, "FullName-10" },
                { 20, "FullName-20" },
            };

            Action<UserDto, UserViewModel> mapA = (s, d) =>
            {
                if (names.TryGetValue(s.UserId, out var name))
                {
                    d.FullName = name;
                }
            };

            Action<UserGroupDto, UserGroup> mapB = (s, d) =>
            {
                if (DateTime.Now.Ticks > 0)
                {
                    d.Users = null;
                }
            };

            var dst = Mapper.Map<UserGroupDto, UserGroup>(src, opt => opt.OnMap(mapA).OnMap(mapB));