Skip to content

Commit

Permalink
Merge pull request #89 from JonPSmith/master
Browse files Browse the repository at this point in the history
Request to include flattening feature into ExpressMapper
  • Loading branch information
anisimovyuriy committed Mar 28, 2016
2 parents 471d2e9 + ad5105b commit 6a9a06f
Show file tree
Hide file tree
Showing 60 changed files with 1,806 additions and 12 deletions.
2 changes: 2 additions & 0 deletions ExpressMapper NET40/DestinationTypeMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace ExpressMapper
{
Expand Down Expand Up @@ -37,6 +38,7 @@ protected override void CompileInternal()

ProcessCustomMembers();
ProcessCustomFunctionMembers();
ProcessFlattenedMembers();
ProcessAutoProperties();

var expressions = new List<Expression>();
Expand Down
3 changes: 3 additions & 0 deletions ExpressMapper NET40/ExpressMapper NET40.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,13 @@
<Compile Include="DestinationTypeMapper.cs" />
<Compile Include="ExpressmapperException.cs" />
<Compile Include="ExpressmapperExtensions.cs" />
<Compile Include="FlattenMemberInfo.cs" />
<Compile Include="FlattenMapper.cs" />
<Compile Include="IMappingContext.cs" />
<Compile Include="IMappingServiceProvider.cs" />
<Compile Include="Ioc\DependencyResolver.cs" />
<Compile Include="Ioc\IContainer.cs" />
<Compile Include="FlattenLinqMethod.cs" />
<Compile Include="MapNotImplemented.cs" />
<Compile Include="MappingServiceProvider.cs">
<SubType>Code</SubType>
Expand Down
82 changes: 82 additions & 0 deletions ExpressMapper NET40/FlattenLinqMethod.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace ExpressMapper
{
internal class FlattenLinqMethod
{
//Any name starting with ~ means the return type should not be checked because it is the same as source
private static readonly string[] ListOfSupportedLinqMethods = new[]
{
"Any", "Count", "LongCount",
"~FirstOrDefault"
//"~First", "~Last", "~LastOrDefault", "~Single", "~SingleOrDefault" - not supported by Entity Framework
};

private static readonly List<FlattenLinqMethod> EnumerableMethodLookup;

static FlattenLinqMethod()
{
EnumerableMethodLookup =
(from givenName in ListOfSupportedLinqMethods
let checkReturnType = givenName[0] != '~'
let name = checkReturnType ? givenName : givenName.Substring(1)
select new FlattenLinqMethod(name, checkReturnType) ).ToList();
}

/// <summary>
/// Method name
/// </summary>
private readonly string _methodName;

/// <summary>
/// If true then the return type should be checked to get the right version of the method
/// </summary>
private readonly bool _checkReturnType ;

private FlattenLinqMethod(string methodName, bool checkReturnType)
{
_methodName = methodName;
_checkReturnType = checkReturnType;
}

/// <summary>
/// This can be called on enumerable properly to see if the ending is a valid Linq method
/// </summary>
/// <param name="endOfName"></param>
/// <param name="stringComparison"></param>
/// <returns></returns>
public static FlattenLinqMethod EnumerableEndMatchsWithLinqMethod(string endOfName, StringComparison stringComparison)
{
return EnumerableMethodLookup.SingleOrDefault(x => string.Equals(x._methodName, endOfName, stringComparison));
}

public override string ToString()
{
return $".{_methodName}()";
}

public MethodCallExpression AsMethodCallExpression(Expression propertyExpression, PropertyInfo propertyToActOn, PropertyInfo destProperty)
{
var ienumerableType = propertyToActOn.PropertyType.GetGenericArguments().Single();

var foundMethodInfo = typeof (Enumerable).GetMethods()
.SingleOrDefault(m => m.Name == _methodName && m.GetParameters().Length == 1
&& (!_checkReturnType || m.ReturnType == destProperty.PropertyType));

if (foundMethodInfo == null)
throw new ExpressmapperException(
$"We could not find the Method {_methodName}() which matched the property {destProperty.Name} of type {destProperty.PropertyType}.");

var method = foundMethodInfo.IsGenericMethod
? foundMethodInfo.MakeGenericMethod(ienumerableType)
: foundMethodInfo;

return Expression.Call(method, propertyExpression);
}

}
}
123 changes: 123 additions & 0 deletions ExpressMapper NET40/FlattenMapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace ExpressMapper
{
internal class FlattenMapper<TSource, TDest>
{
private readonly StringComparison _stringComparison;

private readonly PropertyInfo[] _allDestProps;
private readonly PropertyInfo[] _allSourceProps;

private readonly List<PropertyInfo> _filteredDestProps;

private List<FlattenMemberInfo> _foundFlattens;

public FlattenMapper(ICollection<string> namesOfPropertiesToIgnore, StringComparison stringComparison)
{
_stringComparison = stringComparison;
_allSourceProps = GetPropertiesRightAccess<TSource>();
_allDestProps = GetPropertiesRightAccess<TDest>();

//ExpressMapper with match the top level properties, so we ignore those
_filteredDestProps = FilterOutExactMatches(_allDestProps, _allSourceProps);

if (!namesOfPropertiesToIgnore.Any()) return;

//we also need to remove the destinations that have a .Member or .Ignore applied to them
if (stringComparison == StringComparison.OrdinalIgnoreCase)
namesOfPropertiesToIgnore = namesOfPropertiesToIgnore.Select(x => x.ToLowerInvariant()).ToList();
_filteredDestProps = _filteredDestProps.Where(x => !namesOfPropertiesToIgnore.Contains(
_stringComparison == StringComparison.OrdinalIgnoreCase ? x.Name.ToLowerInvariant() : x.Name)).ToList();
}

public List<FlattenMemberInfo> BuildMemberMapping()
{
_foundFlattens = new List<FlattenMemberInfo>();
var filteredSourceProps = FilterOutExactMatches(_allSourceProps, _allDestProps);
ScanSourceProps(filteredSourceProps);
return _foundFlattens;
}

private void ScanSourceProps(List<PropertyInfo> sourcePropsToScan,
string prefix = "", PropertyInfo[] sourcePropPath = null)
{
foreach (var destProp in _filteredDestProps.ToList())
//scan source property name against dest that has no direct match with any of the source property names
if (_filteredDestProps.Contains(destProp))
//This allows for entries to be removed from the list
ScanSourceClassRecursively(sourcePropsToScan, destProp, prefix, sourcePropPath ?? new PropertyInfo [] {});
}

private void ScanSourceClassRecursively(IEnumerable<PropertyInfo> sourceProps, PropertyInfo destProp,
string prefix, PropertyInfo [] sourcePropPath)
{

foreach (var matchedStartSrcProp in sourceProps.Where(x => destProp.Name.StartsWith(prefix+x.Name, _stringComparison)))
{
var matchStart = prefix + matchedStartSrcProp.Name;
if (string.Equals(destProp.Name, matchStart, _stringComparison))
{
//direct match of name

var underlyingType = Nullable.GetUnderlyingType(destProp.PropertyType);
if (destProp.PropertyType == matchedStartSrcProp.PropertyType ||
underlyingType == matchedStartSrcProp.PropertyType ||
Mapper.MapExists(matchedStartSrcProp.PropertyType, destProp.PropertyType))
{
//matched a) same type, or b) dest is a nullable version of source
_foundFlattens.Add( new FlattenMemberInfo(destProp, sourcePropPath, matchedStartSrcProp));
_filteredDestProps.Remove(destProp); //matched, so take it out
}

return;
}

if (matchedStartSrcProp.PropertyType == typeof (string))
//string can only be directly matched
continue;

if (matchedStartSrcProp.PropertyType.IsClass)
{
var classProps = GetPropertiesRightAccess(matchedStartSrcProp.PropertyType);
var clonedList = sourcePropPath.ToList();
clonedList.Add(matchedStartSrcProp);
ScanSourceClassRecursively(classProps, destProp, matchStart, clonedList.ToArray());
}
else if (matchedStartSrcProp.PropertyType.GetInterfaces().Any(i => i.Name == "IEnumerable"))
{
//its an enumerable class so see if the end relates to a LINQ method
var endOfName = destProp.Name.Substring(matchStart.Length);
var enumeableMethod = FlattenLinqMethod.EnumerableEndMatchsWithLinqMethod(endOfName, _stringComparison);
if (enumeableMethod != null)
{
_foundFlattens.Add(new FlattenMemberInfo(destProp, sourcePropPath, matchedStartSrcProp, enumeableMethod));
_filteredDestProps.Remove(destProp); //matched, so take it out
}
}
}
}

private static PropertyInfo[] GetPropertiesRightAccess<T>()
{
return GetPropertiesRightAccess(typeof (T));
}

private static PropertyInfo[] GetPropertiesRightAccess(Type classType)
{
return classType.GetProperties(BindingFlags.Instance | BindingFlags.Public);
}

private List<PropertyInfo> FilterOutExactMatches(PropertyInfo[] propsToFilter, PropertyInfo[] filterAgainst)
{
var filterNames = filterAgainst
.Select(x => _stringComparison == StringComparison.OrdinalIgnoreCase ? x.Name.ToLowerInvariant() : x.Name).ToArray();
return propsToFilter.Where(x => !filterNames
.Contains(_stringComparison == StringComparison.OrdinalIgnoreCase ? x.Name.ToLowerInvariant() : x.Name)).ToList();

}
}
}
76 changes: 76 additions & 0 deletions ExpressMapper NET40/FlattenMemberInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace ExpressMapper
{
internal class FlattenMemberInfo
{
/// <summary>
/// The Destination property in the DTO
/// </summary>
private readonly PropertyInfo _destMember;

/// <summary>
/// The list of properties in order to get to the source property we want
/// </summary>
private readonly ICollection<PropertyInfo> _sourcePathMembers;

/// <summary>
/// Optional Linq Method to apply to an enumerable source (null if no Linq method on the end)
/// </summary>
private readonly FlattenLinqMethod _linqMethodSuffix;

public FlattenMemberInfo(PropertyInfo destMember, PropertyInfo[] sourcePathMembers, PropertyInfo lastMemberToAdd,
FlattenLinqMethod linqMethodSuffix = null)
{
_destMember = destMember;
_linqMethodSuffix = linqMethodSuffix;

var list = sourcePathMembers.ToList();
list.Add(lastMemberToAdd);
_sourcePathMembers = list;
}

public override string ToString()
{
var linqMethodStr = _linqMethodSuffix?.ToString() ?? "";
return $"dest => dest.{_destMember.Name}, src => src.{string.Join(".",_sourcePathMembers.Select(x => x.Name))}{linqMethodStr}";
}

public MemberExpression DestAsMemberExpression<TDest>()
{
return Expression.Property(Expression.Parameter(typeof(TDest), "dest"), _destMember);
}

public Expression SourceAsExpression<TSource>()
{
var paramExpression = Expression.Parameter(typeof(TSource), "src");
return NestedExpressionProperty(paramExpression, _sourcePathMembers.Reverse().ToArray());
}

//-------------------------------------------------------
//private methods

private Expression NestedExpressionProperty(Expression expression, PropertyInfo [] properties)
{
if (properties.Length > 1)
{
return Expression.Property(
NestedExpressionProperty(
expression,
properties.Skip(1).ToArray()
),
properties[0]);
}

//we are at the end
var finalProperty = Expression.Property(expression, properties[0]);

return _linqMethodSuffix == null
? (Expression)finalProperty
: _linqMethodSuffix.AsMethodCallExpression(finalProperty, properties[0], _destMember);
}
}
}
1 change: 1 addition & 0 deletions ExpressMapper NET40/IMappingServiceProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public interface IMappingServiceProvider
object Map(Type srcType, Type dstType, object src);
object Map(Type srcType, Type dstType, object src, object dest);
IMemberConfiguration<T, TN> Register<T, TN>();
bool MapExists(Type sourceType, Type destinationType);
void RegisterCustom<T, TN, TMapper>() where TMapper : ICustomTypeMapper<T, TN>;
void RegisterCustom<T, TN>(Func<T, TN> mapFunc);
void Reset();
Expand Down
1 change: 1 addition & 0 deletions ExpressMapper NET40/IMemberConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,6 @@ public interface IMemberConfiguration<T, TN>
IMemberConfiguration<T, TN> Value<TNMember>(Expression<Func<TN, TNMember>> dest, TNMember value);
IMemberConfiguration<T, TN> CaseSensitive(bool caseSensitive);
IMemberConfiguration<T, TN> CompileTo(CompilationTypes compilationType);
IMemberConfiguration<T, TN> Flatten();
}
}
4 changes: 4 additions & 0 deletions ExpressMapper NET40/ITypeMapper.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;

namespace ExpressMapper
{
Expand All @@ -22,14 +23,17 @@ public interface ITypeMapper<T, TN> : ITypeMapper
Expression<Func<T, TN>> QueryableExpression { get; }
TN MapTo(T src, TN dest);
void Ignore<TMember>(Expression<Func<TN, TMember>> left);
void Ignore(PropertyInfo left);
void CaseSensetiveMemberMap(bool caseSensitive);
void CompileTo(CompilationTypes compileType);
void MapMember<TMember, TNMember>(Expression<Func<TN, TNMember>> left, Expression<Func<T, TMember>> right);
void MapMemberFlattened(MemberExpression left, Expression right);
void MapFunction<TMember, TNMember>(Expression<Func<TN, TNMember>> left, Func<T, TMember> right);
void InstantiateFunc(Func<T,TN> constructor);
void Instantiate(Expression<Func<T,TN>> constructor);
void BeforeMap(Action<T,TN> beforeMap);
void AfterMap(Action<T,TN> afterMap);
void Flatten();
CompilationTypes MapperType { get; }
}
}
5 changes: 5 additions & 0 deletions ExpressMapper NET40/Mapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ public static IMemberConfiguration<T, TN> Register<T, TN>()
return Instance.Register<T, TN>();
}

public static bool MapExists(Type sourceType, Type destinationType)
{
return Instance.MapExists(sourceType, destinationType);
}

public static void RegisterCustom<T, TN, TMapper>()
where TMapper : ICustomTypeMapper<T, TN>
{
Expand Down
10 changes: 5 additions & 5 deletions ExpressMapper NET40/MappingServiceBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -184,11 +184,11 @@ public Expression GetMemberMappingExpression(Expression left, Expression right,
}
var mapComplexResult = GetDifferentTypeMemberMappingExpression(right, left, newDest);

return nullCheckNestedMemberVisitor.CheckNullExpression != null
? Expression.Condition(nullCheckNestedMemberVisitor.CheckNullExpression,
Expression.Assign(left, Expression.Default(left.Type)),
mapComplexResult)
: mapComplexResult;
// return nullCheckNestedMemberVisitor.CheckNullExpression != null
// ? Expression.Condition(nullCheckNestedMemberVisitor.CheckNullExpression,
// Expression.Assign(left, Expression.Default(left.Type)),
// mapComplexResult)
return mapComplexResult;
}
var binaryExpression = CreateAssignExpression(left,
right,
Expand Down
Loading

0 comments on commit 6a9a06f

Please sign in to comment.