Skip to content

Instantly share code, notes, and snippets.

@gongdo
Last active March 7, 2016 01:28
Show Gist options
  • Save gongdo/c3de72a5651f93f4dfb0 to your computer and use it in GitHub Desktop.
Save gongdo/c3de72a5651f93f4dfb0 to your computer and use it in GitHub Desktop.
Simple object mapper
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();
}
}
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