dco
dco

Reputation: 327

Mapping multiple source properties to a single destination property

I'd like to know if there would be to a way to handle this kind of scenario with some custom type or value resolver.

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

When i have a SuperDateTime in the destination object, i'd like to instantiate this object with the associated DateTimeOffset and the timezone string in the source object.

Of course what i'd like to do is to make something generic, so not going thought the MapFrom in each CreateMap of every Entity

I tried to do it with a custom TypeConverter but it supports only a SourceType -> DestinationType In my case i have a string and DateTimeOffset that has to create a SuperDateTime

Upvotes: 1

Views: 1724

Answers (4)

Andrew Savinykh
Andrew Savinykh

Reputation: 26280

After sleeping on it, here is an alternative that feels more generic.

Assume, you want to do something like this:

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

This looks a bit nicer then my first answer, because here you specify that you want to map all members of SuperDateTime at once. (The type is inferred from the return type of the lambda.) Really, similar to ForAllMembers that AutoMapper already has. The only problem that you cannot use standard memberOptions as IMemberConfigurationExpression<TSource> does not give you access to the member currently being configured. For brevity, I removed the memberOptions parameter from ForMemberType signature completely, but it is very easy to add it back if you need that (that is to set some other options too - see example here).

So in order to be able to write the above all you need is GetPropertyValue method, that can look like this:

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

And the ForMemberType method itself, which would look like that:

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

And that's it. To avoid recompiling the property getter every time, you might want to add a very simple caching layer that compiles (executes ToMemberGetter) once for each type and remembers the result somewhere. Using AutoMapper own DictonaryFactory and then IDictionary.GetOrAdd is probably the most straight forward way of doing this:

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

Upvotes: 0

Andrew Savinykh
Andrew Savinykh

Reputation: 26280

In addition to what LiamK is suggesting, the next possible improvement is to write a helper method for doing .MapFrom. Depending on your requirements it can be simple or complex. I'm going to offer a simple one that makes a lot of assumptions, but you can modify and optimize it to suit your possible requirements.

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

And then you can specify your mapping like this:

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

The assumption is that if your Entity DateTimeOffset is called bla, then your corresponding Entity string is called blaZone, and your Model SuperDateTime is called bla.

Upvotes: 2

LiamK
LiamK

Reputation: 815

The short answer to you question is 'No', there isn't way to use a custom value resolver to map < string, DateTimeOffset > => SuperDateTime and avoid the repeated .MapFrom calls. In your example above, such a value resolver wouldn't be able to distinguish which strings and DateTimeOffsets went together during mapping.

Not sure if you have the .MapFrom code yourself, but if not, below is the best solution to your problem:

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

If you really want to avoid excessive MapFrom declarations, see if there's a way to leverage mapping inheritance here.

EDIT: Modified instantiation of SuperDateTime to match the provided source code.

Upvotes: 0

Capri82
Capri82

Reputation: 418

You can use the Customer Resolver for this. I used custom resolver for getting an object from int something like this;

Lets say you are creating a mapping like this(Althoug you didn't show how you are creating it):

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

And this how your resolver will look like:

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

You can remove all the code related to Nhibernate Repository if you not need to access it. You can further read about custom resolvers here

Upvotes: 1

Related Questions