【问题标题】:Mapping multiple source properties to a single destination property将多个源属性映射到单个目标属性
【发布时间】:2015-04-22 10:40:32
【问题描述】:

我想知道是否有办法使用一些自定义类型或值解析器来处理这种情况。

public class SuperDateTime
{
    public DateTimeOffset Date { get; set; }

    public string Timezone { get; set; }
}

public class Entity 
{
    public DateTimeOffset CreationDate { get; set; }

    public string CreationDateZone { get; set; }

    public DateTimeOffset EndDate { get; set; }

    public string EndDateZone { get; set; }
}

public class Model
{
    public SuperDateTime CreationDate { get; set; }

    public SuperDateTime EndDate { get; set; }
}

当我在目标对象中有一个SuperDateTime 时,我想用源对象中的关联DateTimeOffset 和时区string 来实例化这个对象。

当然,我想做的是做一些通用的东西,所以不要想到每个CreateMap 中的每个CreateMap 中的MapFrom

我尝试使用自定义 TypeConverter 来实现,但它仅支持 SourceType -> DestinationType 就我而言,我有一个 stringDateTimeOffset 必须创建一个 SuperDateTime

【问题讨论】:

  • 除了我在下面的回复之外,定义用于源类型的结构(例如 SuperDateTimeDto)是否有任何价值?然后,您可以定义一个从结构到目标 SuperDateTime 的自定义值解析器。

标签: c# automapper


【解决方案1】:

除了 LiamK 的建议之外,下一个可能的改进是编写一个辅助方法来执行.MapFrom。根据您的要求,它可以是简单的也可以是复杂的。我将提供一个简单的假设,但您可以修改和优化它以满足您的可能要求。

static IMappingExpression<TFrom, TTo> MapSuperDateTime<TFrom, TTo>(
    this IMappingExpression<TFrom, TTo> expression, 
    Expression<Func<TTo, object>> dest)
{
    var datePropertyName = ReflectionHelper.FindProperty(dest).Name;
    var timezomePropertyName = datePropertyName + "Zone";
    var fromType = typeof (TFrom);
    var datePropertyGetter = fromType.GetProperty(datePropertyName).ToMemberGetter();
    var timezonePropertGetter = fromType.GetProperty(timezomePropertyName).ToMemberGetter();

    return expression.ForMember(dest, opt => opt.MapFrom(src => new SuperDateTime
    {
        Date = (DateTimeOffset)datePropertyGetter.GetValue(src),
        Timezone = (string)timezonePropertGetter.GetValue(src)         
    }));
}

然后你可以像这样指定你的映射:

Mapper.CreateMap<Entity, Model>()
    .MapSuperDateTime(dest => dest.CreationDate)
    .MapSuperDateTime(dest => dest.EndDate);

假设如果你的实体DateTimeOffset被称为bla,那么你对应的实体string被称为blaZone,你的模型SuperDateTime被称为bla。

【讨论】:

  • +1 用于将所有内容合并为 OP 的辅助方法。这是我能想到的最简单的重复映射方法。
  • @LiamK 谢谢,我想我们可以让它更通用。我又添加了一个答案。
【解决方案2】:

您可以为此使用客户解析器。我使用自定义解析器从 int 中获取对象,如下所示;

假设您正在创建这样的映射(尽管您没有展示如何创建它):

Mapper.CreateMap<YourSource, YourDestination>()
                .ForMember(x => x.DateTimeOffset, opt => opt.ResolveUsing(new DateTimeOffsetResolver(loadRepository)).FromMember(x => x.timezone));

这就是你的解析器的样子:

public class DateTimeOffsetResolver : ValueResolver<string, DateTimeOffset>
    {
        private DatabaseLoadRepository loadRepository;
        public personIdResolver(DatabaseLoadRepository repo)
        {
            this.loadRepository = repo;
        }
        protected override DateTimeOffset ResolveCore(string timeZone)
        {
            //Your logic for converting string into dateTimeOffset goes here
            return DateTimeOffset; //return the DateTimeOffset instance
        }
    }

如果你不需要访问它,你可以删除所有与 Nhibernate Repository 相关的代码。 您可以进一步阅读自定义解析器here

【讨论】:

  • OP 的问题实际上是针对添加获取 => SuperDateTime 的自定义值解析器。此解决方案解决了映射字符串 => DateTimeOffset 的非常不同的问题
【解决方案3】:

您的问题的简短回答是“否”,没有办法使用自定义值解析器来映射 => SuperDateTime 并避免重复的 .MapFrom 调用。在上面的示例中,这样的值解析器将无法区分 哪些 字符串和 DateTimeOffsets 在映射期间一起出现。

不确定您自己是否有 .MapFrom 代码,但如果没有,以下是解决您问题的最佳方法:

Mapper.CreateMap<Entity, Model>()
      .ForMember(
           dest => dest.CreationDate,
           opt => opt.MapFrom(
               src => new SuperDateTime()
                     {
                           Date = src.CreationDate, 
                           TimeZone = src.CreationDateZone
                     };
            ));

如果您真的想避免过多的 MapFrom 声明,请在此处查看是否有利用映射继承的方法。

编辑:修改 SuperDateTime 的实例以匹配提供的源代码。

【讨论】:

  • 根据 OP SuperDateTime 没有带有两个参数的构造函数。你建议加一个吗? (如果你是,从你的回答中不清楚)
  • 我的假设是帖子中没有包含完整的来源。如果确实没有可行的参数化构造函数,那么使用 setter 实例化属性就很容易了。 .MapFrom 函数采用 Func 参数。 OP 可以做任何必要的事情来实例化匿名函数内的对象。实例化 SuperDateTime 对象的细节似乎与问题无关。
【解决方案4】:

睡在上面之后,这是一个感觉更通用的替代方案。

假设,你想做这样的事情:

Mapper.CreateMap<Entity, Model>()
    .ForMemberType((member,src) => new SuperDateTime
            {
                Date = (DateTimeOffset)GetPropertyValue(src, member),
                Timezone = (string)GetPropertyValue(src, member+"Zone")
            });

这看起来比我的第一个答案好一点,因为在这里您指定要一次映射SuperDateTime 的所有成员。 (类型是从 lambda 的返回类型推断出来的。)实际上,类似于 AutoMapper 已经拥有的ForAllMembers。您无法将标准memberOptions 用作IMemberConfigurationExpression&lt;TSource&gt; 的唯一问题不会让您访问当前正在配置的成员。为简洁起见,我从ForMemberType 签名中完全删除了memberOptions 参数,但如果您需要,很容易将其添加回来(也就是设置一些其他选项 - 参见示例here)。

因此,为了能够编写上述所有内容,您需要的是GetPropertyValue 方法,它可以如下所示:

public object GetPropertyValue(object o, string memberName)
{
    return o.GetType().GetProperty(memberName).ToMemberGetter().GetValue(o);
}

还有ForMemberType 方法本身,看起来像这样:

public static IMappingExpression<TSource, TDestination> ForMemberType<TSource, TDestination, TMember>(
    this IMappingExpression<TSource, TDestination> expression,
    Func<string, TSource, TMember> memberMapping
    )
{
    return new TypeInfo(typeof(TDestination))
        .GetPublicReadAccessors()
        .Where(property => property.GetMemberType() == typeof(TMember))
        .Aggregate(expression, (current, property)
            => current.ForMember(property.Name, 
               opt => opt.MapFrom(src => memberMapping(property.Name, src))));
}

就是这样。为避免每次都重新编译属性 getter,您可能需要添加一个非常简单的缓存层,该缓存层为每种类型编译(执行ToMemberGetter)一次并在某处记住结果。使用 AutoMapper 自己的 DictonaryFactory 然后 IDictionary.GetOrAdd 可能是最直接的方法:

private readonly IDictionary<string, IMemberGetter> _getters 
    = new DictionaryFactory().CreateDictionary<string, IMemberGetter>();        
public object GetPropertyValue(object o, string memberName)
{
    var getter = _getters.GetOrAdd(memberName + o.GetType().FullName, x => o.GetType()
        .GetProperty(memberName).ToMemberGetter());
    return getter.GetValue(o);
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2014-03-31
    • 1970-01-01
    • 1970-01-01
    • 2015-01-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-06-27
    相关资源
    最近更新 更多