Last active
February 19, 2025 13:09
-
-
Save mahelbir/b06badad137a145549a9d00c8074ad4e to your computer and use it in GitHub Desktop.
.NET Object Mapper
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Collections; | |
using System.Reflection; | |
namespace Common; | |
public static class Mapper | |
{ | |
public static TDestination Map<TSource, TDestination>(TSource source) | |
where TSource : class | |
where TDestination : class, new() | |
{ | |
ArgumentNullException.ThrowIfNull(source); | |
var result = (TDestination?)MapObject(source, typeof(TSource), typeof(TDestination), null, includeNulls: true); | |
if (result == null) | |
throw new InvalidOperationException($"Cannot map {typeof(TSource)} to {typeof(TDestination)}"); | |
return result; | |
} | |
public static void Update<TSource, TDestination>(TSource source, TDestination destination) | |
where TSource : class | |
where TDestination : class | |
{ | |
ArgumentNullException.ThrowIfNull(source); | |
ArgumentNullException.ThrowIfNull(destination); | |
MapObject(source, typeof(TSource), typeof(TDestination), destination, includeNulls: true); | |
} | |
public static void UpdateNonNull<TSource, TDestination>(TSource source, TDestination destination) | |
where TSource : class | |
where TDestination : class | |
{ | |
ArgumentNullException.ThrowIfNull(source); | |
ArgumentNullException.ThrowIfNull(destination); | |
MapObject(source, typeof(TSource), typeof(TDestination), destination, includeNulls: false); | |
} | |
private static object? MapObject(object? source, Type srcType, Type dstType, object? existingInstance, bool includeNulls) | |
{ | |
if (source == null) | |
return null; | |
if (dstType == srcType) | |
return source; | |
if (IsSimpleType(srcType) && IsSimpleType(dstType)) | |
{ | |
if (IsNullableType(dstType)) | |
{ | |
var underlying = Nullable.GetUnderlyingType(dstType); | |
if (underlying != null) | |
{ | |
return Convert.ChangeType(source, underlying); | |
} | |
} | |
return Convert.ChangeType(source, dstType); | |
} | |
if (IsDictionary(srcType) && IsDictionary(dstType)) | |
{ | |
return MapDictionary(source, srcType, dstType, existingInstance, includeNulls); | |
} | |
if (IsEnumerableButNotString(srcType) && IsEnumerableButNotString(dstType)) | |
{ | |
return MapEnumerable(source, srcType, dstType, existingInstance, includeNulls); | |
} | |
return MapByReflection(source, srcType, dstType, existingInstance, includeNulls); | |
} | |
private static object? MapDictionary(object source, Type srcType, Type dstType, object? existingInstance, bool includeNulls) | |
{ | |
var srcGenericArgs = srcType.GetGenericArguments(); | |
var dstGenericArgs = dstType.GetGenericArguments(); | |
var dictInstance = existingInstance ?? Activator.CreateInstance(dstType); | |
if (dictInstance == null) return null; | |
var srcEnumerable = (IEnumerable)source; | |
var addMethod = dstType.GetMethod("Add", new[] { dstGenericArgs[0], dstGenericArgs[1] }); | |
if (addMethod == null) | |
return null; | |
foreach (var item in srcEnumerable) | |
{ | |
var kvpType = item.GetType(); | |
var keyProp = kvpType.GetProperty("Key"); | |
var valueProp = kvpType.GetProperty("Value"); | |
if (keyProp == null || valueProp == null) continue; | |
var srcKey = keyProp.GetValue(item); | |
var srcValue = valueProp.GetValue(item); | |
if (srcKey == null || (srcValue == null && !includeNulls)) continue; | |
var dstKey = MapObject(srcKey, srcGenericArgs[0], dstGenericArgs[0], null, includeNulls); | |
var dstValue = MapObject(srcValue, srcGenericArgs[1], dstGenericArgs[1], null, includeNulls); | |
if (dstKey == null || dstValue == null) continue; | |
addMethod.Invoke(dictInstance, new[] { dstKey, dstValue }); | |
} | |
return dictInstance; | |
} | |
private static object? MapEnumerable(object source, Type srcType, Type dstType, object? existingInstance, bool includeNulls) | |
{ | |
var srcArg = srcType.IsArray | |
? srcType.GetElementType() | |
: srcType.GetGenericArguments().FirstOrDefault(); | |
var dstArg = dstType.IsArray | |
? dstType.GetElementType() | |
: dstType.GetGenericArguments().FirstOrDefault(); | |
if (srcArg == null || dstArg == null) | |
return null; | |
var srcEnumerable = (IEnumerable)source; | |
var listInstance = existingInstance ?? Activator.CreateInstance(dstType); | |
if (listInstance == null) return null; | |
var addMethod = dstType.GetMethod("Add"); | |
if (addMethod == null) | |
return null; | |
foreach (var item in srcEnumerable) | |
{ | |
var mappedItem = MapObject(item, srcArg, dstArg, null, includeNulls); | |
addMethod.Invoke(listInstance, new[] { mappedItem }); | |
} | |
return listInstance; | |
} | |
private static object? MapByReflection(object source, Type srcType, Type dstType, object? existingInstance, bool includeNulls) | |
{ | |
var obj = existingInstance ?? Activator.CreateInstance(dstType); | |
if (obj == null) return null; | |
var srcProps = srcType.GetProperties(BindingFlags.Public | BindingFlags.Instance); | |
var dstProps = dstType.GetProperties(BindingFlags.Public | BindingFlags.Instance) | |
.Where(p => p.CanWrite) | |
.ToList(); | |
foreach (var sProp in srcProps) | |
{ | |
var dProp = dstProps.FirstOrDefault(dp => dp.Name == sProp.Name); | |
if (dProp == null) | |
continue; | |
var srcVal = sProp.GetValue(source); | |
if (srcVal == null && !includeNulls) | |
continue; | |
var mappedVal = MapObject(srcVal, sProp.PropertyType, dProp.PropertyType, dProp.GetValue(obj), includeNulls); | |
dProp.SetValue(obj, mappedVal); | |
} | |
return obj; | |
} | |
private static bool IsSimpleType(Type type) | |
{ | |
if (IsNullableType(type)) | |
{ | |
var underlying = Nullable.GetUnderlyingType(type); | |
if (underlying != null) | |
type = underlying; | |
} | |
return | |
type.IsPrimitive || | |
type.IsEnum || | |
type == typeof(string) || | |
type == typeof(decimal) || | |
type == typeof(DateTime) || | |
type == typeof(Guid) || | |
type == typeof(TimeSpan) || | |
type == typeof(DateTimeOffset); | |
} | |
private static bool IsNullableType(Type type) | |
{ | |
return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); | |
} | |
private static bool IsDictionary(Type t) | |
{ | |
if (!t.IsGenericType) return false; | |
var genericTypeDef = t.GetGenericTypeDefinition(); | |
return genericTypeDef == typeof(Dictionary<,>); | |
} | |
private static bool IsEnumerableButNotString(Type t) | |
{ | |
return t != typeof(string) && typeof(IEnumerable).IsAssignableFrom(t); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment