Reputation: 327
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
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
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
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
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