-
Notifications
You must be signed in to change notification settings - Fork 65
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #89 from JonPSmith/master
Request to include flattening feature into ExpressMapper
- Loading branch information
Showing
60 changed files
with
1,806 additions
and
12 deletions.
There are no files selected for viewing
This file contains 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
This file contains 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
This file contains 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
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); | ||
} | ||
|
||
} | ||
} |
This file contains 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
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(); | ||
|
||
} | ||
} | ||
} |
This file contains 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
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); | ||
} | ||
} | ||
} |
This file contains 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
This file contains 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
This file contains 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
This file contains 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
This file contains 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
Oops, something went wrong.