Skip to content

Commit

Permalink
Merge pull request #100 from AutoMapper/SupportIncludeMembers
Browse files Browse the repository at this point in the history
Supporting IncludeMembers.
  • Loading branch information
BlaiseD authored Nov 20, 2020
2 parents db88e0d + 442eeb5 commit d49d54f
Show file tree
Hide file tree
Showing 8 changed files with 572 additions and 26 deletions.
2 changes: 1 addition & 1 deletion Pack_Push.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ $NUGET_PACKAGE_PATH = ".\artifacts\$($Env:PROJECT_NAME).*.nupkg"
Write-Host "Project Path ${PROJECT_PATH}"
Write-Host "Package Path ${NUGET_PACKAGE_PATH}"

if ($Env:REPO_OWNER -ne "AutoMapper") {
if ([string]::IsNullOrEmpty($Env:DEPLOY_PACKAGE_API_KEY)) {
Write-Host "${scriptName}: Only creates packages on AutoMapper repositories."
} else {
dotnet pack $PROJECT_PATH -c Release -o .\artifacts --no-build
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="AutoMapper" Version="[10.0.0,11.0.0)" />
<PackageReference Include="AutoMapper" Version="[10.1.1,11.0.0)" />
<PackageReference Include="MinVer" Version="2.3.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ public static Expression ConvertTypeIfNecessary(this Expression expression, Type
public static MemberExpression GetMemberExpression(this LambdaExpression expr)
=> expr.Body.GetUnconvertedMemberExpression() as MemberExpression;

public static MemberExpression GetMemberExpression(this Expression expr)
=> expr.GetUnconvertedMemberExpression() as MemberExpression;

/// <summary>
/// Returns the ParameterExpression for the LINQ parameter.
/// </summary>
Expand Down Expand Up @@ -177,7 +180,8 @@ public static string GetMemberFullName(this LambdaExpression expr)
{
case ExpressionType.Convert:
case ExpressionType.ConvertChecked:
me = (expr.Body as UnaryExpression)?.Operand as MemberExpression;
case ExpressionType.TypeAs:
me = expr.Body.GetUnconvertedMemberExpression() as MemberExpression;
break;
default:
me = expr.Body as MemberExpression;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System;
using System.Reflection;

namespace AutoMapper.Extensions.ExpressionMapping.Structures
{
internal class DeclaringMemberKey : IEquatable<DeclaringMemberKey>
{
public DeclaringMemberKey(MemberInfo declaringMemberInfo, string declaringMemberFullName)
{
DeclaringMemberInfo = declaringMemberInfo;
DeclaringMemberFullName = declaringMemberFullName;
}

public MemberInfo DeclaringMemberInfo { get; set; }
public string DeclaringMemberFullName { get; set; }

public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;

DeclaringMemberKey key = obj as DeclaringMemberKey;
if (key == null) return false;

return Equals(key);
}

public bool Equals(DeclaringMemberKey other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;

return this.DeclaringMemberInfo.Equals(other.DeclaringMemberInfo)
&& this.DeclaringMemberFullName == other.DeclaringMemberFullName;
}

public override int GetHashCode() => this.DeclaringMemberInfo.GetHashCode();

public override string ToString() => this.DeclaringMemberFullName;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Text;

namespace AutoMapper.Extensions.ExpressionMapping.Structures
{
internal class MemberAssignmentInfo
{
public MemberAssignmentInfo(PropertyMap propertyMap, MemberAssignment memberAssignment)
{
PropertyMap = propertyMap;
MemberAssignment = memberAssignment;
}

/// <summary>
/// Used to get the source member to be bound with the mapped binding expression.
/// </summary>
public PropertyMap PropertyMap { get; set; }

/// <summary>
/// Initial member assignment who's binding expression will be mapped and assigned to the source menber of the new type
/// </summary>
public MemberAssignment MemberAssignment { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using System;
using System.Collections.Generic;

namespace AutoMapper.Extensions.ExpressionMapping.Structures
{
/// <summary>
/// Defines the type to be initialized and a list of source bindings.
/// The new bound members will be matched using MemberAssignmentInfos.PropertyMap and
/// assigned to the mapped expression (mapped from MemberAssignmentInfos.MemberAssignment.Expression).
/// </summary>
internal class MemberBindingGroup
{
public MemberBindingGroup(DeclaringMemberKey declaringMemberKey, bool isRootMemberAssignment, Type newType, List<MemberAssignmentInfo> memberAssignmentInfos)
{
DeclaringMemberKey = declaringMemberKey;
IsRootMemberAssignment = isRootMemberAssignment;
NewType = newType;
MemberAssignmentInfos = memberAssignmentInfos;
}

/// <summary>
/// DeclaringMemberKey will be null when the member assignment is a member binding of OldType on the initial (root) TypeMap (OldType -> NewType)
/// </summary>
public DeclaringMemberKey DeclaringMemberKey { get; set; }

/// <summary>
/// MemberAssignment is true if it is a member binding of OldType on the initial (root) TypeMap (OldType -> NewType)
/// </summary>
public bool IsRootMemberAssignment { get; set; }

/// <summary>
/// Destination type of the member assignment. If IsRootMemberAssignment == true then this is the destination type of initial (root) TypeMap (OldType -> NewType)
/// Otherwise it is the PropertyType/FieldType of DeclaringMemberInfo
/// </summary>
public Type NewType { get; set; }

/// <summary>
/// List of members to be mapped and bound to the new type
/// </summary>
public List<MemberAssignmentInfo> MemberAssignmentInfos { get; set; }
}
}
169 changes: 146 additions & 23 deletions src/AutoMapper.Extensions.ExpressionMapping/XpressionMapperVisitor.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
using System;
using AutoMapper.Extensions.ExpressionMapping.Extensions;
using AutoMapper.Extensions.ExpressionMapping.Structures;
using AutoMapper.Internal;
using AutoMapper.QueryableExtensions.Impl;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using AutoMapper.Extensions.ExpressionMapping.ArgumentMappers;
using AutoMapper.Extensions.ExpressionMapping.Extensions;
using AutoMapper.Extensions.ExpressionMapping.Structures;
using AutoMapper.Internal;
using AutoMapper.QueryableExtensions.Impl;

namespace AutoMapper.Extensions.ExpressionMapping
{
Expand Down Expand Up @@ -175,34 +174,119 @@ protected override Expression VisitMemberInit(MemberInitExpression node)
//The destination becomes the source because to map a source expression to a destination expression,
//we need the expressions used to create the source from the destination

IEnumerable<MemberBinding> bindings = node.Bindings.Aggregate(new List<MemberBinding>(), (list, binding) =>
{
var propertyMap = typeMap.PropertyMaps.SingleOrDefault(item => item.DestinationName == binding.Member.Name);
if (propertyMap == null)
return list;
return GetMemberInit
(
new MemberBindingGroup
(
declaringMemberKey: null,
isRootMemberAssignment: true,
newType: newType,
memberAssignmentInfos: node.Bindings.OfType<MemberAssignment>().Aggregate(new List<MemberAssignmentInfo>(), (list, binding) =>
{
var propertyMap = typeMap.PropertyMaps.SingleOrDefault(item => item.DestinationName == binding.Member.Name);
if (propertyMap == null)
return list;
list.Add(new MemberAssignmentInfo(propertyMap, binding));
return list;
})
)
);
}

return base.VisitMemberInit(node);

}

var sourceMember = GetSourceMember(propertyMap);
if (sourceMember == null)
return list;
private MemberInitExpression GetMemberInit(MemberBindingGroup memberBindingGroup)
{
Dictionary<DeclaringMemberKey, List<MemberAssignmentInfo>> includedMembers = new Dictionary<DeclaringMemberKey, List<MemberAssignmentInfo>>();

List<MemberBinding> bindings = memberBindingGroup.MemberAssignmentInfos.Aggregate(new List<MemberBinding>(), (list, next) =>
{
var propertyMap = next.PropertyMap;
var binding = next.MemberAssignment;
var sourceMember = GetSourceMember(propertyMap);//does the corresponding member mapping exist
if (sourceMember == null)
return list;
DeclaringMemberKey declaringMemberKey = new DeclaringMemberKey
(
GetParentMember(propertyMap),
BuildParentFullName(propertyMap)
);
Expression bindingExpression = ((MemberAssignment)binding).Expression;
list.Add
if (ShouldBindPropertyMap(next))
{
list.Add//adding bindings for property maps
(
DoBind
(
sourceMember,
bindingExpression,
this.Visit(bindingExpression)
binding.Expression,
this.Visit(binding.Expression)
)
);
}
else
{
if (declaringMemberKey.DeclaringMemberInfo == null)
throw new ArgumentNullException(nameof(declaringMemberKey.DeclaringMemberInfo));
return list;
});
if (!includedMembers.TryGetValue(declaringMemberKey, out List<MemberAssignmentInfo> assignments))
{
includedMembers.Add
(
declaringMemberKey,
new List<MemberAssignmentInfo>
{
new MemberAssignmentInfo
(
propertyMap,
binding
)
}
);
}
else
{
assignments.Add(new MemberAssignmentInfo(propertyMap, binding));
}
}
return Expression.MemberInit(Expression.New(newType), bindings);
}
return list;
return base.VisitMemberInit(node);
bool ShouldBindPropertyMap(MemberAssignmentInfo memberAssignmentInfo)
=> (memberBindingGroup.IsRootMemberAssignment && sourceMember.ReflectedType == memberBindingGroup.NewType)
|| (!memberBindingGroup.IsRootMemberAssignment && declaringMemberKey.Equals(memberBindingGroup.DeclaringMemberKey));
});

includedMembers.Select
(
kvp => new MemberBindingGroup
(
declaringMemberKey: kvp.Key,
isRootMemberAssignment: false,
newType: kvp.Key.DeclaringMemberInfo.GetMemberType(),
memberAssignmentInfos: includedMembers.Values.SelectMany(m => m).ToList()
)
)
.ToList()
.ForEach(group =>
{
if (ShouldBindChildReference(group))
bindings.Add(Expression.Bind(group.DeclaringMemberKey.DeclaringMemberInfo, GetMemberInit(group)));
});

bool ShouldBindChildReference(MemberBindingGroup group)
=> (memberBindingGroup.IsRootMemberAssignment
&& group.DeclaringMemberKey.DeclaringMemberInfo.ReflectedType == memberBindingGroup.NewType)
|| (!memberBindingGroup.IsRootMemberAssignment
&& group.DeclaringMemberKey.DeclaringMemberInfo.ReflectedType == memberBindingGroup.NewType
&& group.DeclaringMemberKey.DeclaringMemberFullName.StartsWith(memberBindingGroup.DeclaringMemberKey.DeclaringMemberFullName));

return Expression.MemberInit(Expression.New(memberBindingGroup.NewType), bindings);
}

private MemberBinding DoBind(MemberInfo sourceMember, Expression initial, Expression mapped)
Expand All @@ -217,6 +301,39 @@ private MemberInfo GetSourceMember(PropertyMap propertyMap)
? propertyMap.CustomMapExpression.GetMemberExpression()?.Member
: propertyMap.SourceMember;

private MemberInfo GetParentMember(PropertyMap propertyMap)
=> propertyMap.IncludedMember != null
? propertyMap.ProjectToCustomSource.GetMemberExpression().Member
: GetSourceParentMember(propertyMap);

private MemberInfo GetSourceParentMember(PropertyMap propertyMap)
{
if (propertyMap.CustomMapExpression != null)
return propertyMap.CustomMapExpression.GetMemberExpression()?.Expression.GetMemberExpression()?.Member;

if (propertyMap.SourceMembers.Count > 1)
return new List<MemberInfo>(propertyMap.SourceMembers)[propertyMap.SourceMembers.Count - 2];

return null;
}

private string BuildParentFullName(PropertyMap propertyMap)
{
List<PropertyMapInfo> propertyMapInfos = new List<PropertyMapInfo>();
if (propertyMap.IncludedMember != null)
propertyMapInfos.Add(new PropertyMapInfo(propertyMap.ProjectToCustomSource, new List<MemberInfo>()));

propertyMapInfos.Add(new PropertyMapInfo(propertyMap.CustomMapExpression, propertyMap.SourceMembers.ToList()));

List<string> fullNameArray = BuildFullName(propertyMapInfos)
.Split(new char[] { '.' })
.ToList();

fullNameArray.Remove(fullNameArray.Last());

return string.Join(".", fullNameArray);
}

protected override Expression VisitBinary(BinaryExpression node)
{
return DoVisitBinary(this.Visit(node.Left), this.Visit(node.Right), this.Visit(node.Conversion));
Expand Down Expand Up @@ -509,6 +626,9 @@ void CompareSourceAndDestLiterals(Type mappedPropertyType, string mappedProperty
throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Resource.expressionMapValueTypeMustMatchFormat, mappedPropertyType.Name, mappedPropertyDescription, sourceMemberType.Name, propertyMap.DestinationMember.Name));
}

if (propertyMap.ProjectToCustomSource != null)
propertyMapInfoList.Add(new PropertyMapInfo(propertyMap.ProjectToCustomSource, new List<MemberInfo>()));

propertyMapInfoList.Add(new PropertyMapInfo(propertyMap.CustomMapExpression, propertyMap.SourceMembers.ToList()));
}
else
Expand All @@ -520,6 +640,9 @@ void CompareSourceAndDestLiterals(Type mappedPropertyType, string mappedProperty
if (propertyMap.CustomMapExpression == null && !propertyMap.SourceMembers.Any())//If sourceFullName has a period then the SourceMember cannot be null. The SourceMember is required to find the ProertyMap of its child object.
throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Resource.srcMemberCannotBeNullFormat, typeSource.Name, typeDestination.Name, propertyName));

if (propertyMap.ProjectToCustomSource != null)
propertyMapInfoList.Add(new PropertyMapInfo(propertyMap.ProjectToCustomSource, new List<MemberInfo>()));

propertyMapInfoList.Add(new PropertyMapInfo(propertyMap.CustomMapExpression, propertyMap.SourceMembers.ToList()));
var childFullName = sourceFullName.Substring(sourceFullName.IndexOf(period, StringComparison.OrdinalIgnoreCase) + 1);

Expand Down
Loading

0 comments on commit d49d54f

Please sign in to comment.