You could combine Pawel's answer and Reflection.Emit to make it fast. Be aware that Reflection.Emit is not supported on all platforms (like iOS).
Unlike ExtensionData, this includes all property values from the source. I don't have an elegant solution to determine which properties were already mapped so I just provided an easy way to exclude certain properties.
public class PropertyDictionaryResolver<TSource> : IValueResolver<TSource, object, Dictionary<string, object>>
{
private static readonly PropertyInfo[] Properties = typeof(TSource).GetProperties(BindingFlags.Public | BindingFlags.Instance);
private static readonly ConcurrentDictionary<PropertyInfo, Func<TSource, object>> GetterCache = new ConcurrentDictionary<PropertyInfo, Func<TSource, object>>();
public HashSet<MemberInfo> ExcludedProperties;
public PropertyDictionaryResolver()
{
ExcludedProperties = new HashSet<MemberInfo>();
}
public PropertyDictionaryResolver(Expression<Func<TSource, object>> excludeMembers)
{
var members = ExtractMembers(excludeMembers);
ExcludedProperties = new HashSet<MemberInfo>(members);
}
public Dictionary<string, object> Resolve(TSource source, object destination, Dictionary<string, object> existing, ResolutionContext context)
{
var destMember = new Dictionary<string, object>();
foreach (var property in Properties)
{
if (ExcludedProperties.Contains(property)) continue;
var exp = GetOrCreateExpression(property);
var value = exp(source);
if (value != null)
{
destMember.Add(property.Name, value);
}
}
return destMember;
}
/// <summary>
/// Creates and compiles a getter function for a property
/// </summary>
/// <param name="propInfo"></param>
/// <returns></returns>
public Func<TSource, object> GetOrCreateExpression(PropertyInfo propInfo)
{
if (GetterCache.TryGetValue(propInfo, out var existing))
{
return existing;
}
var parameter = Expression.Parameter(typeof(TSource));
var property = Expression.Property(parameter, propInfo);
var conversion = Expression.Convert(property, typeof(object));
var lambda = Expression.Lambda<Func<TSource, object>>(conversion, parameter);
existing = lambda.Compile();
GetterCache.TryAdd(propInfo, existing);
return existing;
}
/// <summary>
/// Pull the used MemberInfo out of a simple expression. Supports the following expression types only:
/// s => s.Prop1
/// s => new { s.Prop1, s.Prop2 }
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="expression"></param>
/// <returns></returns>
public static IEnumerable<MemberInfo> ExtractMembers<T>(Expression<Func<T, object>> expression)
{
if (expression == null)
{
throw new ArgumentNullException(nameof(expression));
}
switch (expression.Body)
{
case MemberExpression memberExpression:
yield return memberExpression.Member;
yield break;
// s => s.BarFromBaseType
case UnaryExpression convertExpression:
if (convertExpression.Operand is MemberExpression exp)
{
yield return exp.Member;
}
yield break;
// s => new { s.Foo, s.Bar }
case NewExpression newExpression:
if (newExpression.Arguments.Count == 0)
{
yield break;
}
foreach (var argument in newExpression.Arguments.OfType<MemberExpression>())
{
yield return argument.Member;
}
yield break;
}
throw new NotImplementedException("Unrecognized lambda expression.");
}
}
And use it like one of these
[TestClass]
public class Examples
{
[TestMethod]
public void AllProperties()
{
var mapper = new Mapper(new MapperConfiguration(p =>
{
p.CreateMap<Source, Destination>()
.ForMember(x => x.A, cfg => cfg.MapFrom(x => x.A))
.ForMember(x => x.D, cfg => cfg.MapFrom<PropertyDictionaryResolver<Source>>());
}));
var source = new Source { A = 1, B = 2, C = 3 };
var d = mapper.Map<Destination>(source);
// {"A":1,"D":{"A":1,"B":2,"C":3}}
}
[TestMethod]
public void ExcludeSingleProperty()
{
var mapper = new Mapper(new MapperConfiguration(p =>
{
p.CreateMap<Source, Destination>()
.ForMember(x => x.A, cfg => cfg.MapFrom(x => x.A))
.ForMember(x => x.D, cfg => cfg.MapFrom(new PropertyDictionaryResolver<Source>(x => x.A)));
}));
var source = new Source { A = 1, B = 2, C = 3 };
var d = mapper.Map<Destination>(source);
// {"A":1,"D":{"B":2,"C":3}}
}
[TestMethod]
public void ExcludeMultipleProperties()
{
var mapper = new Mapper(new MapperConfiguration(p =>
{
p.CreateMap<Source, Destination>()
.ForMember(x => x.A, cfg => cfg.MapFrom(x => x.A))
.ForMember(x => x.D, cfg => cfg.MapFrom(new PropertyDictionaryResolver<Source>(x => new
{
x.A,
x.B
})));
}));
var source = new Source { A = 1, B = 2, C = 3 };
var d = mapper.Map<Destination>(source);
// {"A":1,"D":{"C":3}}
}
}