diff --git a/src/AutoMapper.Extensions.ExpressionMapping/AnonymousTypeFactory.cs b/src/AutoMapper.Extensions.ExpressionMapping/AnonymousTypeFactory.cs new file mode 100644 index 0000000..c135a58 --- /dev/null +++ b/src/AutoMapper.Extensions.ExpressionMapping/AnonymousTypeFactory.cs @@ -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 memberDetails) + => CreateAnonymousType(memberDetails.ToDictionary(key => key.Name, element => element.GetMemberType())); + + public static Type CreateAnonymousType(IDictionary 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}"; + } +} diff --git a/src/AutoMapper.Extensions.ExpressionMapping/XpressionMapperVisitor.cs b/src/AutoMapper.Extensions.ExpressionMapping/XpressionMapperVisitor.cs index e169f1c..e2c1a91 100644 --- a/src/AutoMapper.Extensions.ExpressionMapping/XpressionMapperVisitor.cs +++ b/src/AutoMapper.Extensions.ExpressionMapping/XpressionMapperVisitor.cs @@ -161,11 +161,37 @@ protected override Expression VisitLambda(Expression 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 bindingExpressions = new Dictionary(); + + 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)) @@ -193,9 +219,36 @@ protected override Expression VisitMemberInit(MemberInitExpression node) ) ); } + else if (IsAnonymousType(node.Type)) + { + return GetMemberInitExpression + ( + node.Bindings + .OfType() + .ToDictionary + ( + binding => binding.Member.Name, + binding => this.Visit(binding.Expression) + ), + node.Type + ); + } return base.VisitMemberInit(node); + } + private MemberInitExpression GetMemberInitExpression(Dictionary 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) @@ -537,8 +590,10 @@ private bool GenericTypeDefinitionsAreEquivalent(Type typeSource, Type typeDesti protected void FindDestinationFullName(Type typeSource, Type typeDestination, string sourceFullName, List 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) => diff --git a/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/XpressionMapperTests.cs b/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/XpressionMapperTests.cs index 837058d..c803a67 100644 --- a/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/XpressionMapperTests.cs +++ b/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/XpressionMapperTests.cs @@ -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, IEnumerable>> expression = q => q.OrderBy(s => s.Id).Select(u => new { UserId = u.Id, Account = u.AccountModel }); + + //Act + Expression, IEnumerable>> expMapped = (Expression, IEnumerable>>)mapper.MapExpression + ( + expression, + typeof(Expression, IEnumerable>>), + typeof(Expression, IEnumerable>>) + ); + + List 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, IEnumerable>> expression = q => q.OrderBy(s => s.Id).Select(u => new { UserId = u.Id, Branch = u.AccountModel.Branch }); + + //Act + Expression, IEnumerable>> expMapped = (Expression, IEnumerable>>)mapper.MapExpression + ( + expression, + typeof(Expression, IEnumerable>>), + typeof(Expression, IEnumerable>>) + ); + + List 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() {