Last active
March 7, 2016 01:28
-
-
Save gongdo/c3de72a5651f93f4dfb0 to your computer and use it in GitHub Desktop.
Simple 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; | |
using System.Collections.Concurrent; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Linq.Expressions; | |
using System.Reflection; | |
/// <summary> | |
/// Interface that maps TFrom to TTo. | |
/// </summary> | |
/// <typeparam name="TFrom">Type of the source object.</typeparam> | |
/// <typeparam name="TTo">Type of the destination object.</typeparam> | |
public interface IMapper<TFrom, TTo> | |
where TFrom : class | |
where TTo : class | |
{ | |
/// <summary> | |
/// Maps the source object to a new object of the destination type. | |
/// </summary> | |
/// <param name="source">The source object to map.</param> | |
/// <returns>A new mapped object.</returns> | |
TTo Map(TFrom source); | |
} | |
public sealed class SimpleMapper<TFrom, TTo> | |
: IMapper<TFrom, TTo> | |
where TFrom : class | |
where TTo : class | |
{ | |
private static readonly ConcurrentDictionary<string, Func<TFrom, TTo>> cachedMethod | |
= new ConcurrentDictionary<string, Func<TFrom, TTo>>(); | |
public TTo Map(TFrom from) | |
{ | |
var fromType = typeof(TFrom); | |
var toType = typeof(TTo); | |
var key = fromType.AssemblyQualifiedName + ":" + toType.AssemblyQualifiedName; | |
var method = cachedMethod.GetOrAdd(key, aKey => GetMapMethod(fromType, toType)); | |
return method(from); | |
} | |
private Func<TFrom, TTo> GetMapMethod(Type fromType, Type toType) | |
{ | |
var fromProperties = fromType.GetProperties().Where(p => p.CanRead).ToArray(); | |
var toProperties = toType.GetProperties().Where(p => p.CanWrite).ToArray(); | |
var parameter = Expression.Parameter(fromType, "from"); | |
var constructor = toType.GetTypeInfo() | |
.DeclaredConstructors | |
.Where(c => c.IsPublic) | |
.OrderByDescending(c => c.GetParameters().Length) | |
.Select(c => | |
{ | |
var parameters = c.GetParameters(); | |
if (parameters.Length == 0) | |
{ | |
return new Tuple<ConstructorInfo, IEnumerable<Expression>>( | |
c, | |
Enumerable.Empty<Expression>()); | |
} | |
var matches = parameters | |
.Select(param => new | |
{ | |
Parameter = param, | |
Property = fromProperties.FirstOrDefault(prop => | |
prop.Name.Equals(param.Name, StringComparison.OrdinalIgnoreCase)), | |
}) | |
.Where(o => o.Property != null) | |
.ToList(); | |
if (parameters.Length == matches.Count) | |
{ | |
return new Tuple<ConstructorInfo, IEnumerable<Expression>>( | |
c, | |
matches.Select(m => Expression.Property(parameter, m.Property))); | |
} | |
return null; | |
}) | |
.FirstOrDefault(p => p != null); | |
if (constructor == null) | |
{ | |
throw new InvalidOperationException($"There is no matched constructor between '{fromType.Name}' and '{toType.Name}'. And '{toType.Name}' does not have a constructor without arguments."); | |
} | |
var expression = Expression.Lambda<Func<TFrom, TTo>>( | |
Expression.MemberInit( | |
Expression.New(constructor.Item1, constructor.Item2), | |
toProperties.Select(to => | |
{ | |
var source = fromProperties.FirstOrDefault(o => o.Name == to.Name); | |
if (source != null) | |
{ | |
return Expression.Bind(to, Expression.Property(parameter, source)); | |
} | |
return null; | |
}) | |
.Where(p => p != null)), | |
parameter); | |
return expression.Compile(); | |
} | |
} |
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; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Linq.Expressions; | |
using Xunit; | |
/// <summary> | |
/// Base class | |
/// </summary> | |
public abstract class Food | |
{ | |
public Guid Id { get; set; } | |
} | |
/// <summary> | |
/// Persistence model | |
/// </summary> | |
public class Foo : Food | |
{ | |
public string Name { get; set; } | |
public int Level { get; set; } | |
} | |
/// <summary> | |
/// Mutable Presentation model | |
/// </summary> | |
public class MutableFoo | |
{ | |
public Guid Id { get; set; } | |
public string Name { get; set; } | |
public int Level { get; set; } | |
} | |
/// <summary> | |
/// Immutable Presentation model | |
/// </summary> | |
public class ImmutableFoo | |
{ | |
public ImmutableFoo(Guid id, string name, int level) | |
{ | |
Id = id; | |
Name = name; | |
Level = level; | |
} | |
public Guid Id { get; private set; } | |
public string Name { get; private set; } | |
public int Level { get; private set; } | |
} | |
/// <summary> | |
/// Partially Mutable Presentation model | |
/// </summary> | |
public class PartiallyMutableFoo | |
{ | |
public PartiallyMutableFoo(Guid id, string name) | |
{ | |
Id = id; | |
Name = name; | |
} | |
public Guid Id { get; private set; } | |
public string Name { get; private set; } | |
public int Level { get; set; } | |
} | |
[Fact] | |
public void DefaultMapper_maps_mutable() | |
{ | |
var mapper = new SimpleMapper<Foo, MutableFoo>(); | |
var expected = new Foo | |
{ | |
Id = Guid.NewGuid(), | |
Name = "gongdo", | |
Level = 1 | |
}; | |
var actual = mapper.Map(expected); | |
Assert.Equal(expected.Id, actual.Id); | |
Assert.Equal(expected.Name, actual.Name); | |
Assert.Equal(expected.Level, actual.Level); | |
} | |
[Fact] | |
public void DefaultMapper_maps_immutable() | |
{ | |
var mapper = new SimpleMapper<Foo, ImmutableFoo>(); | |
var expected = new Foo | |
{ | |
Id = Guid.NewGuid(), | |
Name = "gongdo", | |
Level = 1 | |
}; | |
var actual = mapper.Map(expected); | |
Assert.Equal(expected.Id, actual.Id); | |
Assert.Equal(expected.Name, actual.Name); | |
Assert.Equal(expected.Level, actual.Level); | |
} | |
[Fact] | |
public void DefaultMapper_maps_partially_mutable() | |
{ | |
var mapper = new SimpleMapper<Foo, PartiallyMutableFoo>(); | |
var expected = new Foo | |
{ | |
Id = Guid.NewGuid(), | |
Name = "gongdo", | |
Level = 1 | |
}; | |
var actual = mapper.Map(expected); | |
Assert.Equal(expected.Id, actual.Id); | |
Assert.Equal(expected.Name, actual.Name); | |
Assert.Equal(expected.Level, actual.Level); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment