AutoMapper 自定义映射

16

我有两个非常简单的对象:

public class CategoryDto
{
    public string Id { get; set; }

    public string MyValueProperty { get; set; }
}

public class Category
{
    public string Id { get; set; }

    [MapTo("MyValueProperty")]
    public string Key { get; set; }
}

使用AutoMapper将Category映射到CategoryDto时,我希望实现以下行为:

属性应像往常一样进行映射,除了那些带有MapTo属性的属性。在这种情况下,我需要读取属性值以找到目标属性。源属性的值用于查找要注入到目标属性中的值(借助于字典)。一个例子胜过千言万语...

例如:

Dictionary<string, string> keys = 
    new Dictionary<string, string> { { "MyKey", "MyValue" } };

Category category = new Category();
category.Id = "3";
category.Key = "MyKey";

CategoryDto result = Map<Category, CategoryDto>(category);
result.Id               // Expected : "3"
result.MyValueProperty  // Expected : "MyValue"

通过MapTo属性,Key属性映射到MyValueProperty,并且分配的值为"MyValue",因为源属性值是"MyKey",它通过字典映射到"MyValue"。

使用AutoMapper是否可以实现这一点?当然,我需要一个适用于每个对象的解决方案,而不仅仅是Category/CategoryDto。


为什么需要属性,在第一步中,您可以设置自定义映射并将属性键映射到值。这是可能的吗? - whymatter
我想创建一个通用的映射器,可以在任何地方使用...这样我就可以将任何实体映射到任何DTO,而不需要任何额外的代码... - Bidou
在我看来,您让实体负责它不应该负责的事情。视图模型应该定义它获取构建自身所需数据的位置,而不是相反。 - Peter
3个回答

11

经过好几个小时的努力,我终于找到了一个解决方案。我将其分享给社区,希望它能帮助其他人...

编辑:请注意,现在更简单了(AutoMapper 5.0+),您可以按照我在此帖子中回答的方式操作:如何使 AutoMapper 根据 MaxLength 属性截断字符串?

public static class Extensions
{
    public static IMappingExpression<TSource, TDestination> MapTo<TSource, TDestination>(this IMappingExpression<TSource, TDestination> expression)
    {
        Type sourceType = typeof(TSource);
        Type destinationType = typeof(TDestination);

        TypeMap existingMaps = Mapper.GetAllTypeMaps().First(b => b.SourceType == sourceType && b.DestinationType == destinationType);
        string[] missingMappings = existingMaps.GetUnmappedPropertyNames();

        if (missingMappings.Any())
        {
            PropertyInfo[] sourceProperties = sourceType.GetProperties();
            foreach (string property in missingMappings)
            {
                foreach (PropertyInfo propertyInfo in sourceProperties)
                {
                    MapToAttribute attr = propertyInfo.GetCustomAttribute<MapToAttribute>();
                    if (attr != null && attr.Name == property)
                    {
                        expression.ForMember(property, opt => opt.ResolveUsing(new MyValueResolve(propertyInfo)));
                    }
                }
            }
        }

        return expression;
    }
}

public class MyValueResolve : IValueResolver
{
    private readonly PropertyInfo pInfo = null;

    public MyValueResolve(PropertyInfo pInfo)
    {
        this.pInfo = pInfo;
    }

    public ResolutionResult Resolve(ResolutionResult source)
    {
        string key = pInfo.GetValue(source.Value) as string;
        string value = dictonary[key];
        return source.New(value);
    }
}

9
很高兴你找到了解决方案。你能否稍微扩展一下你的答案,解释一下这种方法的步骤,并提供一个使用示例? - JohnnyHK

0
假设我有以下类:
public class foo
{
  public string Value;
}
public class bar
{
    public string Value1;
    public string Value2;
}

您可以将 lambda 表达式传递给 ResolveUsing:

.ForMember(f => f.Value, o => o.ResolveUsing(b =>
{
    if (b.Value1.StartsWith("A"));)
    {
        return b.Value1;
    }
    return b.Value2;
}


 ));

不,你不能这样做... Value 是未知的,我必须使用通用的TSource和TDestination进行处理...否则,在一个大项目中,我必须为所有的Dto<-->Entity对创建一个 "ForMember"(在一个大项目中,这可能会有很多!)。而采用我的解决方案,我只需要做一次... - Bidou

0

使用IValueResolver的实现和ResolveUsing()方法应该是相当简单的。你基本上只需要为解析器编写一个构造函数,该构造函数接受属性信息(或者如果你想要更高级一些,可以接受lambda表达式并类似于如何获取特定属性的PropertyInfo?来解析属性信息)。虽然我自己没有测试过,但我想以下代码应该可以工作:

public class PropertyBasedResolver : IValueResolver
{
     public PropertyInfo Property { get; set; }

     public PropertyBasedResolver(PropertyInfo property)
     {
          this.Property = property;
     }

     public ResolutionResult Resolve(ResolutionResult source)
     {
           var result = GetValueFromKey(property, source.Value); // gets from some static cache or dictionary elsewhere in your code by reading the prop info and then using that to look up the value based on the key as appropriate
           return source.New(result)
     }
}

然后要设置该映射,您需要执行:

AutoMapper.Mapper.CreateMap<Category, CategoryDto>()
    .ForMember(
         dest => dest.Value, 
         opt => opt.ResolveUsing(
              src => 
                   new PropertyBasedResolver(typeof(Category.Key) as PropertyInfo).Resolve(src)));

当然,这是一个相当粗糙的lambda表达式,我建议您通过让属性解析器根据属性/属性信息确定应查看源对象上的属性,从而清理它,以便您可以将一个干净的新PropertyBasedResolver(property)传递给ResolveUsing(),但希望这足以让您走上正确的轨道。


谢谢你的回答。实际上,Value属性在编译时是未知的。我必须借助MapTo属性来发现它,所以这个解决方案行不通... - Bidou
我不明白。你是说 Value 属性本身未知(也就是说,你根本没有 dto 的概念)吗?问题是如何将动态 dto 映射到已知模型? - Daniel King
我更新了问题,现在可能更清楚了一些。在这个例子中,Id被映射到Id(默认映射),而Key应该被映射到MyValueProperty,因为它具有指向MyValuePropertyMapTo属性。模型本身的值是“MyKey”,所以在Dto中的值将是“MyValue”,因为在字典中是这样映射的。现在更清楚了吗? - Bidou
我欣赏第二个代码块中复杂的多运算符语句 :) - Max von Hippel

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