Skip to content

Instantly share code, notes, and snippets.

@roobie
Created March 13, 2022 14:18
Show Gist options
  • Save roobie/a98d99a6f2b6641b1d0dd266fda7d820 to your computer and use it in GitHub Desktop.
Save roobie/a98d99a6f2b6641b1d0dd266fda7d820 to your computer and use it in GitHub Desktop.
mapping things
using System;
using System.Linq;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Reflection;
using Xunit;
namespace Shovel.Lib
{
public class SmartMapper_Test
{
[Fact]
public void Test1()
{
var source = new DataObject { Value1 = "Value1", Value2 = "Value2", Value3 = new DateTime(1999, 12, 31) };
var target1 = new ViewObject1();
var target2 = new ViewObject2();
var target3 = new ViewObject3();
var target3_bad = new ViewObject3_Bad();
SmartMapper.CopyProperties(source, target1);
SmartMapper.CopyProperties(source, target2);
SmartMapper.CopyProperties(source, target3);
// Misconfiguration, types are not assignable (DateTime vs string)
Assert.Throws<ArgumentException>(() => {
SmartMapper.CopyProperties(source, target3_bad);
});
Assert.Equal(source.Value1, target1.Value);
Assert.Equal(source.Value2, target2.Value);
Assert.Equal(source.Value3, target3.Value);
}
[Fact]
public void Test2()
{
var context = new SmartMapperContext();
SmartMapperContextBuilder.Build(context, Assembly.GetExecutingAssembly());
var source = new DataObject { Value1 = "Value1", Value2 = "Value2", Value3 = new DateTime(1999, 12, 31) };
var target1 = new ViewObject1();
var target2 = new ViewObject2();
var target3 = new ViewObject3();
var target3_bad = new ViewObject3_Bad();
SmartMapper.CopyProperties(source, target1, context);
SmartMapper.CopyProperties(source, target2, context);
SmartMapper.CopyProperties(source, target3, context);
// Misconfiguration, types are not assignable (DateTime vs string)
Assert.Throws<ArgumentException>(() => {
SmartMapper.CopyProperties(source, target3_bad, context);
});
Assert.Equal(source.Value1, target1.Value);
Assert.Equal(source.Value2, target2.Value);
Assert.Equal(source.Value3, target3.Value);
}
}
public class DataObject
{
[MapProperty("field1")]
public string? Value1 { get; set; }
[MapProperty("field2")]
public string? Value2 { get; set; }
[MapProperty("field3")]
public DateTime Value3 { get; set; }
}
public class ViewObject1
{
[MapProperty("field1")]
public string? Value { get; set; }
}
public class ViewObject2
{
[MapProperty("field2")]
public string? Value { get; set; }
}
public class ViewObject3_Bad
{
[MapProperty("field3")]
public string? Value { get; set; }
}
public class ViewObject3
{
[MapProperty("field3")]
public DateTime? Value { get; set; }
}
[System.AttributeUsage(System.AttributeTargets.Property)]
public class MapPropertyAttribute : Attribute
{
public string Identifier { get; }
public MapPropertyAttribute(string identifier)
{
this.Identifier = identifier;
}
}
/// This class should really be used in a way that it is built at once, during initialization.
public class SmartMapperContext
{
internal IDictionary<Type, PropertyInfo[]> PropertyInfos { get; }= new ConcurrentDictionary<Type, PropertyInfo[]>();
internal IDictionary<Type, ISet<string>> MapIdentifiers { get; } = new ConcurrentDictionary<Type, ISet<string>>();
internal IDictionary<(string, Type), PropertyInfo> PropertyLookup { get; } = new ConcurrentDictionary<(string, Type), PropertyInfo>();
internal IDictionary<(Type, Type), bool> CompatibilityCheck { get; } = new ConcurrentDictionary<(Type, Type), bool>();
}
public static class SmartMapperContextBuilder
{
public static void Build(SmartMapperContext context, Assembly assembly)
{
foreach (var type in assembly.GetTypes().Where(IsApplicable))
{
var propertyInfos = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
context.PropertyInfos[type] = propertyInfos;
var mapIdentifiers = propertyInfos
.Select(prop => prop.GetCustomAttribute<MapPropertyAttribute>()?.Identifier ?? string.Empty)
.Where(item => item != string.Empty)
.ToHashSet();
context.MapIdentifiers[type] = mapIdentifiers;
foreach (var mapIdentifier in mapIdentifiers)
{
context.PropertyLookup[(mapIdentifier, type)] = propertyInfos
.First(prop => prop.GetCustomAttribute<MapPropertyAttribute>()?.Identifier == mapIdentifier);
}
}
}
private static bool IsApplicable(Type type)
{
return type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Any(prop => prop.GetCustomAttribute<MapPropertyAttribute>() != null);
}
}
public class SmartMapper
{
public static void CopyProperties<TSource, TTarget>(
TSource sourceObject,
TTarget targetObject,
SmartMapperContext context)
{
var tsource = typeof(TSource);
var ttarget = typeof(TTarget);
var sourceProperties = context.PropertyInfos[tsource];
var targetProperties = context.PropertyInfos[ttarget];
var parameterContainer = new object[1];
var intersection = context.MapIdentifiers[tsource].Intersect(context.MapIdentifiers[ttarget]);
foreach (var mapIdentifier in intersection)
{
var sourceProp = context.PropertyLookup[(mapIdentifier, tsource)];
var targetProp = context.PropertyLookup[(mapIdentifier, ttarget)];
if (!context.CompatibilityCheck.ContainsKey((tsource, ttarget)))
{
if (!targetProp.PropertyType.IsAssignableFrom(sourceProp.PropertyType))
{
throw new ArgumentException($@"The target property is not assignable from the source property.
TSource:
{typeof(TSource).Name}.{sourceProp.Name} is a {sourceProp.PropertyType}
TTarget:
{typeof(TTarget).Name}.{targetProp.Name} is a {targetProp.PropertyType}");
}
context.CompatibilityCheck.Add((tsource, ttarget), true);
}
var value = sourceProp.GetGetMethod()?.Invoke(sourceObject, null);
if (value is null)
{
continue;
}
parameterContainer[0] = value;
targetProp.GetSetMethod()?.Invoke(targetObject, parameterContainer);
}
}
/**
Possible optimizations:
- store the types' lists of PropertyInfos, so it doesn't have to invoke .GetProperties each time.
- store the attribute identifiers, so that it doesn't have to invoke .GetCustomAttribute each time.
- store a lookup table so that it doesn't have to loop over all properties to check which ones map to which.
- the test to see whether the source properties are assignable to the target properties is not needed more than once.
The above data might be stored in an instance of SmartMapper, or in a separate SmartMapperContext instance
**/
public static void CopyProperties<TSource, TTarget>(
TSource sourceObject,
TTarget targetObject)
{
var sourceProperties = typeof(TSource).GetProperties(
BindingFlags.Public | BindingFlags.Instance);
var targetProperties = typeof(TTarget).GetProperties(
BindingFlags.Public | BindingFlags.Instance);
// allocated on the stack, so not really a performance issue.
var parameterContainer = new object[1];
foreach (PropertyInfo sourceProp in sourceProperties)
{
var sourceAttr = sourceProp.GetCustomAttribute<MapPropertyAttribute>();
if (sourceAttr is null)
{
continue;
}
var mappingId = sourceAttr.Identifier;
foreach (PropertyInfo targetProp in targetProperties)
{
var targetAttr = targetProp.GetCustomAttribute<MapPropertyAttribute>();
if (targetAttr is null
|| targetAttr.Identifier != mappingId)
{
continue;
}
if (!targetProp.PropertyType.IsAssignableFrom(sourceProp.PropertyType))
{
throw new ArgumentException(
$@"The target property is not assignable from the source property.
TSource:
{typeof(TSource).Name}.{sourceProp.Name} is a {sourceProp.PropertyType}
TTarget:
{typeof(TTarget).Name}.{targetProp.Name} is a {targetProp.PropertyType}");
}
var value = sourceProp.GetGetMethod()?
.Invoke(sourceObject, null);
if (value is null)
{
continue;
}
parameterContainer[0] = value;
targetProp.GetSetMethod()?
.Invoke(targetObject, parameterContainer);
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment