Skip to content

Commit

Permalink
Merge pull request #103 from AutoMapper/ImprovingAnonymousTypeExpress…
Browse files Browse the repository at this point in the history
…ionMapping

Mapping MemberInit and Expression.New for anonymous types.
  • Loading branch information
BlaiseD authored Feb 5, 2021
2 parents 9f1dc09 + 65dc62f commit 86dd7df
Show file tree
Hide file tree
Showing 3 changed files with 160 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;

namespace AutoMapper.Extensions.ExpressionMapping
{
internal static class AnonymousTypeFactory
{
private static int classCount;

public static Type CreateAnonymousType(IEnumerable<MemberInfo> memberDetails)
=> CreateAnonymousType(memberDetails.ToDictionary(key => key.Name, element => element.GetMemberType()));

public static Type CreateAnonymousType(IDictionary<string, Type> memberDetails)
{
AssemblyName dynamicAssemblyName = new AssemblyName("TempAssembly.AutoMapper.Extensions.ExpressionMapping");
AssemblyBuilder dynamicAssembly = AssemblyBuilder.DefineDynamicAssembly(dynamicAssemblyName, AssemblyBuilderAccess.Run);
ModuleBuilder dynamicModule = dynamicAssembly.DefineDynamicModule("TempAssembly.AutoMapper.Extensions.ExpressionMapping");
TypeBuilder typeBuilder = dynamicModule.DefineType(GetAnonymousTypeName(), TypeAttributes.Public);
MethodAttributes getSetAttr = MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig;

var builders = memberDetails.Select
(
info =>
{
Type memberType = info.Value;
string memberName = info.Key;
return new
{
FieldBuilder = typeBuilder.DefineField(string.Concat("_", memberName), memberType, FieldAttributes.Private),
PropertyBuilder = typeBuilder.DefineProperty(memberName, PropertyAttributes.HasDefault, memberType, null),
GetMethodBuilder = typeBuilder.DefineMethod(string.Concat("get_", memberName), getSetAttr, memberType, Type.EmptyTypes),
SetMethodBuilder = typeBuilder.DefineMethod(string.Concat("set_", memberName), getSetAttr, null, new Type[] { memberType })
};
}
);

builders.ToList().ForEach(builder =>
{
ILGenerator getMethodIL = builder.GetMethodBuilder.GetILGenerator();
getMethodIL.Emit(OpCodes.Ldarg_0);
getMethodIL.Emit(OpCodes.Ldfld, builder.FieldBuilder);
getMethodIL.Emit(OpCodes.Ret);
ILGenerator setMethodIL = builder.SetMethodBuilder.GetILGenerator();
setMethodIL.Emit(OpCodes.Ldarg_0);
setMethodIL.Emit(OpCodes.Ldarg_1);
setMethodIL.Emit(OpCodes.Stfld, builder.FieldBuilder);
setMethodIL.Emit(OpCodes.Ret);
builder.PropertyBuilder.SetGetMethod(builder.GetMethodBuilder);
builder.PropertyBuilder.SetSetMethod(builder.SetMethodBuilder);
});

return typeBuilder.CreateTypeInfo().AsType();
}

private static string GetAnonymousTypeName()
=> $"AnonymousType{++classCount}";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -161,11 +161,37 @@ protected override Expression VisitLambda<T>(Expression<T> node)
protected override Expression VisitNew(NewExpression node)
{
if (this.TypeMappings.TryGetValue(node.Type, out Type newType))
{
return Expression.New(newType);
}
else if (node.Arguments.Count > 0 && IsAnonymousType(node.Type))
{
ParameterInfo[] parameters = node.Type.GetConstructors()[0].GetParameters();
Dictionary<string, Expression> bindingExpressions = new Dictionary<string, Expression>();

for (int i = 0; i < parameters.Length; i++)
bindingExpressions.Add(parameters[i].Name, this.Visit(node.Arguments[i]));

return GetMemberInitExpression(bindingExpressions, node.Type);
}

return base.VisitNew(node);
}

private static bool IsAnonymousType(Type type)
=> type.Name.Contains("AnonymousType")
&&
(
Attribute.IsDefined
(
type,
typeof(System.Runtime.CompilerServices.CompilerGeneratedAttribute),
false
)
||
type.Assembly.IsDynamic
);

protected override Expression VisitMemberInit(MemberInitExpression node)
{
if (this.TypeMappings.TryGetValue(node.Type, out Type newType))
Expand Down Expand Up @@ -193,9 +219,36 @@ protected override Expression VisitMemberInit(MemberInitExpression node)
)
);
}
else if (IsAnonymousType(node.Type))
{
return GetMemberInitExpression
(
node.Bindings
.OfType<MemberAssignment>()
.ToDictionary
(
binding => binding.Member.Name,
binding => this.Visit(binding.Expression)
),
node.Type
);
}

return base.VisitMemberInit(node);
}

private MemberInitExpression GetMemberInitExpression(Dictionary<string, Expression> bindingExpressions, Type oldType)
{
Type newAnonymousType = AnonymousTypeFactory.CreateAnonymousType(bindingExpressions.ToDictionary(a => a.Key, a => a.Value.Type));
TypeMappings.AddTypeMapping(ConfigurationProvider, oldType, newAnonymousType);

return Expression.MemberInit
(
Expression.New(newAnonymousType),
bindingExpressions
.ToDictionary(be => be.Key, be => newAnonymousType.GetMember(be.Key)[0])
.Select(member => Expression.Bind(member.Value, bindingExpressions[member.Key]))
);
}

private MemberInitExpression GetMemberInit(MemberBindingGroup memberBindingGroup)
Expand Down Expand Up @@ -537,8 +590,10 @@ private bool GenericTypeDefinitionsAreEquivalent(Type typeSource, Type typeDesti
protected void FindDestinationFullName(Type typeSource, Type typeDestination, string sourceFullName, List<PropertyMapInfo> propertyMapInfoList)
{
const string period = ".";

if (typeSource == typeDestination)
bool BothTypesAreAnonymous()
=> IsAnonymousType(typeSource) && IsAnonymousType(typeDestination);

if (typeSource == typeDestination || BothTypesAreAnonymous())
{
var sourceFullNameArray = sourceFullName.Split(new[] { period[0] }, StringSplitOptions.RemoveEmptyEntries);
sourceFullNameArray.Aggregate(propertyMapInfoList, (list, next) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -635,6 +635,46 @@ public void Map_orderBy_thenBy_To_Dictionary_Select_expression_without_generic_t
Assert.True(users[0].Id == 11);
}

[Fact]
public void Map_to_anonymous_type_when_init_member_is_not_a_literal()
{
//Arrange
Expression<Func<IQueryable<UserModel>, IEnumerable<object>>> expression = q => q.OrderBy(s => s.Id).Select(u => new { UserId = u.Id, Account = u.AccountModel });

//Act
Expression<Func<IQueryable<User>, IEnumerable<object>>> expMapped = (Expression<Func<IQueryable<User>, IEnumerable<object>>>)mapper.MapExpression
(
expression,
typeof(Expression<Func<IQueryable<UserModel>, IEnumerable<object>>>),
typeof(Expression<Func<IQueryable<User>, IEnumerable<object>>>)
);

List<dynamic> users = expMapped.Compile().Invoke(Users).ToList();

Assert.True(users[0].UserId == 11);
Assert.True(users[0].Account.Balance == 150000);
}

[Fact]
public void Map_to_anonymous_type_when_init_member_is_not_a_literal_with_navigation_property()
{
//Arrange
Expression<Func<IQueryable<UserModel>, IEnumerable<object>>> expression = q => q.OrderBy(s => s.Id).Select(u => new { UserId = u.Id, Branch = u.AccountModel.Branch });

//Act
Expression<Func<IQueryable<User>, IEnumerable<object>>> expMapped = (Expression<Func<IQueryable<User>, IEnumerable<object>>>)mapper.MapExpression
(
expression,
typeof(Expression<Func<IQueryable<UserModel>, IEnumerable<object>>>),
typeof(Expression<Func<IQueryable<User>, IEnumerable<object>>>)
);

List<dynamic> users = expMapped.Compile().Invoke(Users).ToList();

Assert.True(users[0].UserId == 11);
Assert.True(users[0].Branch.Name == "Head Row");
}

[Fact]
public void Map_dynamic_return_type()
{
Expand Down

0 comments on commit 86dd7df

Please sign in to comment.