Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions ValueMapper/ValueMapperCore/ValueMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@
/// <summary>
/// Maps a single source object to a new destination object
/// </summary>
public static TDestination Map<TSource, TDestination>(TSource source, ISet<string> ignoredProperties = null)

Check warning on line 122 in ValueMapper/ValueMapperCore/ValueMapper.cs

View workflow job for this annotation

GitHub Actions / Build

Cannot convert null literal to non-nullable reference type.
where TDestination : new()
{
if (source == null) throw new ArgumentNullException(nameof(source));
Expand All @@ -141,7 +141,7 @@
/// <summary>
/// Maps a collection of source objects to a list of destination objects
/// </summary>
public static List<TDestination> MapList<TSource, TDestination>(IEnumerable<TSource> list, ISet<string> ignoredProperties = null)

Check warning on line 144 in ValueMapper/ValueMapperCore/ValueMapper.cs

View workflow job for this annotation

GitHub Actions / Build

Cannot convert null literal to non-nullable reference type.
where TDestination : new()
{
if (list == null) throw new ArgumentNullException(nameof(list));
Expand Down Expand Up @@ -231,17 +231,17 @@
continue;

// Determine source property
PropertyInfo src = null;

Check warning on line 234 in ValueMapper/ValueMapperCore/ValueMapper.cs

View workflow job for this annotation

GitHub Actions / Build

Converting null literal or possible null value to non-nullable type.
var attrDst = dst.GetCustomAttribute<ValueMapperMappingAttribute>(true);
if (attrDst != null)
{
// Attribute on destination: use specified source name
srcProps.TryGetValue(attrDst.SourcePropertyName, out src);

Check warning on line 239 in ValueMapper/ValueMapperCore/ValueMapper.cs

View workflow job for this annotation

GitHub Actions / Build

Converting null literal or possible null value to non-nullable type.
}
else if (!srcProps.TryGetValue(dst.Name, out src))

Check warning on line 241 in ValueMapper/ValueMapperCore/ValueMapper.cs

View workflow job for this annotation

GitHub Actions / Build

Converting null literal or possible null value to non-nullable type.
{
// No match by name: look for attribute on source property mapping to this dst
src = srcProps.Values.FirstOrDefault(sp =>

Check warning on line 244 in ValueMapper/ValueMapperCore/ValueMapper.cs

View workflow job for this annotation

GitHub Actions / Build

Converting null literal or possible null value to non-nullable type.
sp.GetCustomAttribute<ValueMapperMappingAttribute>(true)?.SourcePropertyName
.Equals(dst.Name, StringComparison.OrdinalIgnoreCase) == true);
}
Expand All @@ -268,7 +268,7 @@
var setterExpr = Expression.Lambda<Action<TDestination, object>>(
Expression.Call(
dstParam,
dst.GetSetMethod(true),

Check warning on line 271 in ValueMapper/ValueMapperCore/ValueMapper.cs

View workflow job for this annotation

GitHub Actions / Build

Possible null reference argument for parameter 'method' in 'MethodCallExpression Expression.Call(Expression? instance, MethodInfo method, params Expression[]? arguments)'.
Expression.Convert(valParam, dstType)
),
dstParam,
Expand Down Expand Up @@ -312,6 +312,18 @@
}
}

// Check if this is a complex object that needs deep mapping
if (IsComplexType(srcType) && IsComplexType(dstType) && srcType != dstType)
{
// Create deep mapping for complex objects
var deepMapping = CreateDeepMapping<TSource, TDestination>(dst.Name, src, dst, srcType, dstType);
if (deepMapping.Getter != null && deepMapping.Setter != null)
{
mappings.Add(deepMapping);
continue;
}
}

// Normal property mapping
var converter = CreateConverter(srcType, dstType);
if (converter == null)
Expand All @@ -324,7 +336,7 @@
// Compile setter
var parDst = Expression.Parameter(typeof(TDestination), "d");
var parVal = Expression.Parameter(dstType, "v");
var setterCall = Expression.Call(parDst, dst.GetSetMethod(true), parVal);

Check warning on line 339 in ValueMapper/ValueMapperCore/ValueMapper.cs

View workflow job for this annotation

GitHub Actions / Build

Possible null reference argument for parameter 'method' in 'MethodCallExpression Expression.Call(Expression? instance, MethodInfo method, params Expression[]? arguments)'.

// Get or create the conversion function
var isNullable = !IsNonNullable(dstType);
Expand Down Expand Up @@ -376,7 +388,7 @@
var elementType = dstType.GetGenericArguments()[0];

// Create a copy delegate
var copyMethod = typeof(ValueMapper).GetMethod(

Check warning on line 391 in ValueMapper/ValueMapperCore/ValueMapper.cs

View workflow job for this annotation

GitHub Actions / Build

Dereference of a possibly null reference.
nameof(CopyList),
BindingFlags.NonPublic | BindingFlags.Static)
.MakeGenericMethod(elementType);
Expand All @@ -403,7 +415,7 @@
var setterExpr = Expression.Lambda<Action<TDestination, object>>(
Expression.Call(
dstParam,
dstProp.GetSetMethod(true),

Check warning on line 418 in ValueMapper/ValueMapperCore/ValueMapper.cs

View workflow job for this annotation

GitHub Actions / Build

Possible null reference argument for parameter 'method' in 'MethodCallExpression Expression.Call(Expression? instance, MethodInfo method, params Expression[]? arguments)'.
Expression.Convert(valueParam, dstType)
),
dstParam, valueParam
Expand Down Expand Up @@ -859,6 +871,103 @@

return typeof(object);
}

// Check if a type is a complex object that needs deep mapping
private static bool IsComplexType(Type type)
{
if (type == null) return false;

// Handle nullable types
Type underlyingType = Nullable.GetUnderlyingType(type) ?? type;

// Skip primitive types, strings, and enums
if (underlyingType.IsPrimitive ||
underlyingType == typeof(string) ||
underlyingType == typeof(DateTime) ||
underlyingType == typeof(TimeSpan) ||
underlyingType == typeof(DateTimeOffset) ||
underlyingType == typeof(Guid) ||
underlyingType == typeof(decimal) ||
underlyingType.IsEnum)
{
return false;
}

// Skip collections
if (IsCollection(type))
{
return false;
}

// Check if it's a class with properties (complex object)
return underlyingType.IsClass &&
underlyingType != typeof(object) &&
GetTypeProperties(underlyingType).Length > 0;
}

// Create deep mapping for complex nested objects
private static MappingEntry<TSource, TDestination> CreateDeepMapping<TSource, TDestination>(
string name, PropertyInfo srcProp, PropertyInfo dstProp, Type srcType, Type dstType)
{
// Create a getter that retrieves the source object
var srcParam = Expression.Parameter(typeof(TSource), "s");
var srcPropExpr = Expression.Property(srcParam, srcProp);

// Create the deep mapping method call
var mapMethod = typeof(ValueMapper).GetMethod(nameof(Map), new[] { srcType, typeof(ISet<string>) })
?? typeof(ValueMapper).GetMethods()
.Where(m => m.Name == nameof(Map) && m.GetGenericArguments().Length == 2)
.FirstOrDefault()?.MakeGenericMethod(srcType, dstType);

if (mapMethod == null)
{
// Fallback: try to get the generic Map method and make it generic
mapMethod = typeof(ValueMapper).GetMethods(BindingFlags.Public | BindingFlags.Static)
.Where(m => m.Name == nameof(Map) &&
m.IsGenericMethodDefinition &&
m.GetGenericArguments().Length == 2 &&
m.GetParameters().Length == 2)
.FirstOrDefault()?.MakeGenericMethod(srcType, dstType);
}

if (mapMethod == null)
{
return new MappingEntry<TSource, TDestination>(name, null, null, dstType, true);
}

// Create getter that performs deep mapping
Func<TSource, object> getter = source =>
{
try
{
var sourceValue = srcProp.GetValue(source);
if (sourceValue == null)
return null;

// Use reflection to call the generic Map method
return mapMethod.Invoke(null, new object[] { sourceValue, null });
}
catch
{
return null;
}
};

// Create setter for the destination property
var dstParam = Expression.Parameter(typeof(TDestination), "d");
var valueParam = Expression.Parameter(typeof(object), "v");
var setterExpr = Expression.Lambda<Action<TDestination, object>>(
Expression.Call(
dstParam,
dstProp.GetSetMethod(true),
Expression.Convert(valueParam, dstType)
),
dstParam, valueParam
);
var setter = setterExpr.Compile();

return new MappingEntry<TSource, TDestination>(name, getter, setter, dstType, true);
}
}
}
// Internal implementation detail - hidden from API
Expand Down