将多个源属性映射到单个目标 属性

Mapping multiple source properties to a single destination property

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

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 实例化该对象。

当然我想做的是做一些通用的东西,所以不会想到每个 Entity

的每个 CreateMap 中的 MapFrom

我尝试使用自定义 TypeConverter 来实现,但它仅支持 SourceType -> DestinationType 在我的例子中,我有一个 stringDateTimeOffset 必须创建一个 SuperDateTime

您可以为此使用客户解决程序。我使用自定义解析器从 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,您可以删除所有与Nhibernate Repository 相关的代码。 您可以进一步阅读自定义解析器 here

对您问题的简短回答是 'No',无法使用自定义值解析器映射 => 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 的实例以匹配提供的源代码。

除了 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。

睡过头了,这里有一个感觉更通用的替代方案。

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

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

这看起来比我的第一个答案好一点,因为在这里您指定要一次映射 SuperDateTime 的所有成员。 (类型是从 lambda 的 return 类型推断出来的。)真的,类似于 AutoMapper 已经具有的 ForAllMembers。您不能使用标准 memberOptions 的唯一问题是 IMemberConfigurationExpression<TSource> 不允许您访问当前正在配置的成员。为简洁起见,我从 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);
}