From f86b6e28741064af6fc125c5662559052f4634a7 Mon Sep 17 00:00:00 2001 From: John Zabroski Date: Mon, 25 Apr 2022 06:38:40 -0400 Subject: [PATCH 01/18] Fix typo: sorce -> source (#139) * Fix typo * Fix typo: constent -> constant --- .../ExpressionMapper.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/AutoMapper.Extensions.ExpressionMapping/ExpressionMapper.cs b/src/AutoMapper.Extensions.ExpressionMapping/ExpressionMapper.cs index b60a4de..8f51b56 100644 --- a/src/AutoMapper.Extensions.ExpressionMapping/ExpressionMapper.cs +++ b/src/AutoMapper.Extensions.ExpressionMapping/ExpressionMapper.cs @@ -95,7 +95,7 @@ protected override Expression VisitBinary(BinaryExpression node) var newLeft = Visit(node.Left); var newRight = Visit(node.Right); - // check if the non-string expression is a null constent + // check if the non-string expression is a null constant // as this would lead to a "null.ToString()" and thus an error when executing the expression if (newLeft.Type != newRight.Type && newRight.Type == typeof(string) && !IsNullConstant(newLeft)) newLeft = Call(newLeft, typeof(object).GetDeclaredMethod("ToString")); @@ -206,7 +206,7 @@ protected override Expression VisitMember(MemberExpression node) if (constantVisitor.IsConstant) return node; - SetSorceSubTypes(propertyMap); + SetSourceSubTypes(propertyMap); var replacedExpression = Visit(node.Expression); if (replacedExpression == node.Expression) @@ -282,7 +282,7 @@ private PropertyMap GetExistingPropertyMapFor(MemberInfo destinationProperty, Ty return typeMap.PropertyMaps.FirstOrDefault(pm => pm.DestinationName == destinationProperty.Name); } - private void SetSorceSubTypes(PropertyMap propertyMap) + private void SetSourceSubTypes(PropertyMap propertyMap) { if (propertyMap.SourceMember is PropertyInfo info) _destSubTypes = info.PropertyType.GetTypeInfo().GenericTypeArguments.Concat(new[] { info.PropertyType }).ToList(); From dee8f94e44deceea446980e3e92f56eb0caca067 Mon Sep 17 00:00:00 2001 From: Blaise Taylor Date: Tue, 14 Jun 2022 14:07:25 -0400 Subject: [PATCH 02/18] Updating the list of literals which may need help to resolve "operator is not defined for the types" exception. (#142) --- .../TypeExtensions.cs | 15 +- ...ensions.ExpressionMapping.UnitTests.csproj | 3 +- ...dOperationExceptionForUnmatchedLiterals.cs | 274 ++++++++++++++++++ 3 files changed, 286 insertions(+), 6 deletions(-) create mode 100644 tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/ShouldThrowInvalidOperationExceptionForUnmatchedLiterals.cs diff --git a/src/AutoMapper.Extensions.ExpressionMapping/TypeExtensions.cs b/src/AutoMapper.Extensions.ExpressionMapping/TypeExtensions.cs index f8bbc27..34a6f95 100644 --- a/src/AutoMapper.Extensions.ExpressionMapping/TypeExtensions.cs +++ b/src/AutoMapper.Extensions.ExpressionMapping/TypeExtensions.cs @@ -3,13 +3,9 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; -using System.Reflection.Emit; namespace AutoMapper { -#if NET45 - using System.Reflection.Emit; -#endif internal static class TypeExtensions { @@ -127,14 +123,23 @@ public static bool IsLiteralType(this Type type) if (type.IsNullableType()) type = Nullable.GetUnderlyingType(type); - return LiteralTypes.Contains(type); + return LiteralTypes.Contains(type) || NonNetStandardLiteralTypes.Contains(type.FullName); } private static HashSet LiteralTypes => new HashSet(_literalTypes); + private static readonly HashSet NonNetStandardLiteralTypes = new() + { + "System.DateOnly", + "Microsoft.OData.Edm.Date", + "System.TimeOnly", + "Microsoft.OData.Edm.TimeOfDay" + }; + private static Type[] _literalTypes => new Type[] { typeof(bool), typeof(DateTime), + typeof(DateTimeOffset), typeof(TimeSpan), typeof(Guid), typeof(decimal), diff --git a/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/AutoMapper.Extensions.ExpressionMapping.UnitTests.csproj b/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/AutoMapper.Extensions.ExpressionMapping.UnitTests.csproj index 370fd9e..3ef441b 100644 --- a/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/AutoMapper.Extensions.ExpressionMapping.UnitTests.csproj +++ b/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/AutoMapper.Extensions.ExpressionMapping.UnitTests.csproj @@ -1,13 +1,14 @@  - netcoreapp3.1 + net6.0 false + diff --git a/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/ShouldThrowInvalidOperationExceptionForUnmatchedLiterals.cs b/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/ShouldThrowInvalidOperationExceptionForUnmatchedLiterals.cs new file mode 100644 index 0000000..f2454c6 --- /dev/null +++ b/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/ShouldThrowInvalidOperationExceptionForUnmatchedLiterals.cs @@ -0,0 +1,274 @@ +using Microsoft.OData.Edm; +using System; +using System.Linq.Expressions; +using Xunit; + +namespace AutoMapper.Extensions.ExpressionMapping.UnitTests +{ + public class ShouldThrowInvalidOperationExceptionForUnmatchedLiterals + { + [Theory] + [InlineData(nameof(ProductModel.Bool), typeof(bool?))] + [InlineData(nameof(ProductModel.DateTime), typeof(DateTime?))] + [InlineData(nameof(ProductModel.DateTimeOffset), typeof(DateTimeOffset?))] + [InlineData(nameof(ProductModel.Date), typeof(Date?))] + [InlineData(nameof(ProductModel.DateOnly), typeof(DateOnly?))] + [InlineData(nameof(ProductModel.TimeSpan), typeof(TimeSpan?))] + [InlineData(nameof(ProductModel.TimeOfDay), typeof(TimeOfDay?))] + [InlineData(nameof(ProductModel.TimeOnly), typeof(TimeOnly?))] + [InlineData(nameof(ProductModel.Guid), typeof(Guid?))] + [InlineData(nameof(ProductModel.Decimal), typeof(decimal?))] + [InlineData(nameof(ProductModel.Byte), typeof(byte?))] + [InlineData(nameof(ProductModel.Short), typeof(short?))] + [InlineData(nameof(ProductModel.Int), typeof(int?))] + [InlineData(nameof(ProductModel.Long), typeof(long?))] + [InlineData(nameof(ProductModel.Float), typeof(float?))] + [InlineData(nameof(ProductModel.Double), typeof(double?))] + [InlineData(nameof(ProductModel.Char), typeof(char?))] + [InlineData(nameof(ProductModel.SByte), typeof(sbyte?))] + [InlineData(nameof(ProductModel.UShort), typeof(ushort?))] + [InlineData(nameof(ProductModel.ULong), typeof(ulong?))] + public void ThrowsCreatingBinaryExpressionCombiningNonNullableParameterWithNullableConstant(string memberName, Type constantType) + { + var mapper = GetMapper(); + ParameterExpression productParam = Expression.Parameter(typeof(ProductModel), "x"); + MemberExpression property = Expression.MakeMemberAccess(productParam, AutoMapper.Internal.TypeExtensions.GetFieldOrProperty(typeof(ProductModel), memberName)); + + Assert.Throws + ( + () => Expression.Lambda> + ( + Expression.Equal + ( + property, + Expression.Constant(Activator.CreateInstance(constantType), constantType) + ), + productParam + ) + ); + } + + [Theory] + [InlineData(nameof(Product.Bool), typeof(bool))] + [InlineData(nameof(Product.DateTime), typeof(DateTime))] + [InlineData(nameof(Product.DateTimeOffset), typeof(DateTimeOffset))] + [InlineData(nameof(Product.Date), typeof(Date))] + [InlineData(nameof(Product.DateOnly), typeof(DateOnly))] + [InlineData(nameof(Product.TimeSpan), typeof(TimeSpan))] + [InlineData(nameof(Product.TimeOfDay), typeof(TimeOfDay))] + [InlineData(nameof(Product.TimeOnly), typeof(TimeOnly))] + [InlineData(nameof(Product.Guid), typeof(Guid))] + [InlineData(nameof(Product.Decimal), typeof(decimal))] + [InlineData(nameof(Product.Byte), typeof(byte))] + [InlineData(nameof(Product.Short), typeof(short))] + [InlineData(nameof(Product.Int), typeof(int))] + [InlineData(nameof(Product.Long), typeof(long))] + [InlineData(nameof(Product.Float), typeof(float))] + [InlineData(nameof(Product.Double), typeof(double))] + [InlineData(nameof(Product.Char), typeof(char))] + [InlineData(nameof(Product.SByte), typeof(sbyte))] + [InlineData(nameof(Product.UShort), typeof(ushort))] + [InlineData(nameof(Product.ULong), typeof(ulong))] + public void ThrowsCreatingBinaryExpressionCombiningNullableParameterWithNonNullableConstant(string memberName, Type constantType) + { + var mapper = GetMapper(); + ParameterExpression productParam = Expression.Parameter(typeof(Product), "x"); + MemberExpression property = Expression.MakeMemberAccess(productParam, AutoMapper.Internal.TypeExtensions.GetFieldOrProperty(typeof(Product), memberName)); + + var ex = Assert.Throws + ( + () => Expression.Lambda> + ( + Expression.Equal + ( + property, + Expression.Constant(Activator.CreateInstance(constantType), constantType) + ), + productParam + ) + ); + } + + [Theory] + [InlineData(nameof(ProductModel.Bool), typeof(bool))] + [InlineData(nameof(ProductModel.DateTime), typeof(DateTime))] + [InlineData(nameof(ProductModel.DateTimeOffset), typeof(DateTimeOffset))] + [InlineData(nameof(ProductModel.Date), typeof(Date))] + [InlineData(nameof(ProductModel.DateOnly), typeof(DateOnly))] + [InlineData(nameof(ProductModel.TimeSpan), typeof(TimeSpan))] + [InlineData(nameof(ProductModel.TimeOfDay), typeof(TimeOfDay))] + [InlineData(nameof(ProductModel.TimeOnly), typeof(TimeOnly))] + [InlineData(nameof(ProductModel.Guid), typeof(Guid))] + [InlineData(nameof(ProductModel.Decimal), typeof(decimal))] + [InlineData(nameof(ProductModel.Byte), typeof(byte))] + [InlineData(nameof(ProductModel.Short), typeof(short))] + [InlineData(nameof(ProductModel.Int), typeof(int))] + [InlineData(nameof(ProductModel.Long), typeof(long))] + [InlineData(nameof(ProductModel.Float), typeof(float))] + [InlineData(nameof(ProductModel.Double), typeof(double))] + [InlineData(nameof(ProductModel.Char), typeof(char))] + [InlineData(nameof(ProductModel.SByte), typeof(sbyte))] + [InlineData(nameof(ProductModel.UShort), typeof(ushort))] + [InlineData(nameof(ProductModel.ULong), typeof(ulong))] + public void ThrowsmappingExpressionWithMismatchedOperands(string memberName, Type constantType) + { + var mapper = GetMapper(); + ParameterExpression productParam = Expression.Parameter(typeof(ProductModel), "x"); + MemberExpression property = Expression.MakeMemberAccess(productParam, AutoMapper.Internal.TypeExtensions.GetFieldOrProperty(typeof(ProductModel), memberName)); + + Expression> expression = Expression.Lambda> + ( + Expression.Equal + ( + property, + Expression.Constant(Activator.CreateInstance(constantType), constantType) + ), + productParam + ); + + var exception = Assert.Throws + ( + () => mapper.MapExpression>>(expression) + ); + + Assert.StartsWith + ( + "The source and destination types must be the same for expression mapping between literal types.", + exception.Message + ); + } + + [Theory] + [InlineData(nameof(ProductModel.Bool), typeof(bool))] + [InlineData(nameof(ProductModel.DateTime), typeof(DateTime))] + [InlineData(nameof(ProductModel.DateTimeOffset), typeof(DateTimeOffset))] + [InlineData(nameof(ProductModel.Date), typeof(Date))] + [InlineData(nameof(ProductModel.DateOnly), typeof(DateOnly))] + [InlineData(nameof(ProductModel.TimeSpan), typeof(TimeSpan))] + [InlineData(nameof(ProductModel.TimeOfDay), typeof(TimeOfDay))] + [InlineData(nameof(ProductModel.TimeOnly), typeof(TimeOnly))] + [InlineData(nameof(ProductModel.Guid), typeof(Guid))] + [InlineData(nameof(ProductModel.Decimal), typeof(decimal))] + [InlineData(nameof(ProductModel.Byte), typeof(byte))] + [InlineData(nameof(ProductModel.Short), typeof(short))] + [InlineData(nameof(ProductModel.Int), typeof(int))] + [InlineData(nameof(ProductModel.Long), typeof(long))] + [InlineData(nameof(ProductModel.Float), typeof(float))] + [InlineData(nameof(ProductModel.Double), typeof(double))] + [InlineData(nameof(ProductModel.Char), typeof(char))] + [InlineData(nameof(ProductModel.SByte), typeof(sbyte))] + [InlineData(nameof(ProductModel.UShort), typeof(ushort))] + [InlineData(nameof(ProductModel.ULong), typeof(ulong))] + public void MappingExpressionWorksUsingCustomExpressionToResolveBinaryOperators(string memberName, Type constantType) + { + var mapper = GetMapperWithCustomExpressions(); + ParameterExpression productParam = Expression.Parameter(typeof(ProductModel), "x"); + MemberExpression property = Expression.MakeMemberAccess(productParam, AutoMapper.Internal.TypeExtensions.GetFieldOrProperty(typeof(ProductModel), memberName)); + + Expression> expression = Expression.Lambda> + ( + Expression.Equal + ( + property, + Expression.Constant(Activator.CreateInstance(constantType), constantType) + ), + productParam + ); + + var mappedExpression = mapper.MapExpression>>(expression); + Assert.NotNull(mappedExpression); + } + + private static IMapper GetMapper() + { + var config = new MapperConfiguration(c => + { + c.CreateMap(); + }); + config.AssertConfigurationIsValid(); + return config.CreateMapper(); + } + + private static IMapper GetMapperWithCustomExpressions() + { + var config = new MapperConfiguration(c => + { + c.CreateMap() + .ForMember(d => d.Bool, o => o.MapFrom(s => s.Bool.Value)) + .ForMember(d => d.DateTime, o => o.MapFrom(s => s.DateTime.Value)) + .ForMember(d => d.DateTimeOffset, o => o.MapFrom(s => s.DateTimeOffset.Value)) + .ForMember(d => d.Date, o => o.MapFrom(s => s.Date.Value)) + .ForMember(d => d.DateOnly, o => o.MapFrom(s => s.DateOnly.Value)) + .ForMember(d => d.TimeSpan, o => o.MapFrom(s => s.TimeSpan.Value)) + .ForMember(d => d.TimeOfDay, o => o.MapFrom(s => s.TimeOfDay.Value)) + .ForMember(d => d.TimeOnly, o => o.MapFrom(s => s.TimeOnly.Value)) + .ForMember(d => d.Guid, o => o.MapFrom(s => s.Guid.Value)) + .ForMember(d => d.Decimal, o => o.MapFrom(s => s.Decimal.Value)) + .ForMember(d => d.Byte, o => o.MapFrom(s => s.Byte.Value)) + .ForMember(d => d.Short, o => o.MapFrom(s => s.Short.Value)) + .ForMember(d => d.Int, o => o.MapFrom(s => s.Int.Value)) + .ForMember(d => d.Long, o => o.MapFrom(s => s.Long.Value)) + .ForMember(d => d.Float, o => o.MapFrom(s => s.Float.Value)) + .ForMember(d => d.Double, o => o.MapFrom(s => s.Double.Value)) + .ForMember(d => d.Char, o => o.MapFrom(s => s.Char.Value)) + .ForMember(d => d.SByte, o => o.MapFrom(s => s.SByte.Value)) + .ForMember(d => d.UShort, o => o.MapFrom(s => s.UShort.Value)) + .ForMember(d => d.UInt, o => o.MapFrom(s => s.UInt.Value)) + .ForMember(d => d.ULong, o => o.MapFrom(s => s.ULong.Value)); + }); + + config.AssertConfigurationIsValid(); + return config.CreateMapper(); + } + + class Product + { + public bool? Bool { get; set; } + public DateTimeOffset? DateTimeOffset { get; set; } + public DateTime? DateTime { get; set; } + public Date? Date { get; set; } + public DateOnly? DateOnly { get; set; } + public TimeSpan? TimeSpan { get; set; } + public TimeOfDay? TimeOfDay { get; set; } + public TimeOnly? TimeOnly { get; set; } + public Guid? Guid { get; set; } + public decimal? Decimal { get; set; } + public byte? Byte { get; set; } + public short? Short { get; set; } + public int? Int { get; set; } + public long? Long { get; set; } + public float? Float { get; set; } + public double? Double { get; set; } + public char? Char { get; set; } + public sbyte? SByte { get; set; } + public ushort? UShort { get; set; } + public uint? UInt { get; set; } + public ulong? ULong { get; set; } + } + + class ProductModel + { + public bool Bool { get; set; } + public DateTimeOffset DateTimeOffset { get; set; } + public DateTime DateTime { get; set; } + public Date Date { get; set; } + public DateOnly DateOnly { get; set; } + public TimeSpan TimeSpan { get; set; } + public TimeOfDay TimeOfDay { get; set; } + public TimeOnly TimeOnly { get; set; } + public Guid Guid { get; set; } + public decimal Decimal { get; set; } + public byte Byte { get; set; } + public short Short { get; set; } + public int Int { get; set; } + public long Long { get; set; } + public float Float { get; set; } + public double Double { get; set; } + public char Char { get; set; } + public sbyte SByte { get; set; } + public ushort UShort { get; set; } + public uint UInt { get; set; } + public ulong ULong { get; set; } + } + } +} From 58e905f6b5acb9731ae7a8c550568ae8d31e5b2d Mon Sep 17 00:00:00 2001 From: Blaise Taylor Date: Thu, 16 Jun 2022 07:26:53 -0400 Subject: [PATCH 03/18] Adding PackageIcon --- icon.png | Bin 0 -> 1672 bytes ...utoMapper.Extensions.ExpressionMapping.csproj | 6 +++++- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 icon.png diff --git a/icon.png b/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..56b96cabfea86f6bed8f2392fbe4a3553d108c86 GIT binary patch literal 1672 zcmV;326y?1P)?ZyV}#yTcU9VCW@8K-~f6mOc@sprX^vnT#m-heSt|!~tq_0USYr0CO=$V}eW6#4KiEkcbNo7~9yo!RXev(Z0TS z{G;vbyZte`f{e2?d%gGEb3gZ-bI-kP84S|zC@fM;l8X__0F;6;3vQT}&)o#8GPe(1@cy%}I1-FCP{LL>uJh4(qzrNMErWqTN2>`BDQ7 z3Hf~R)NaG*w@1d+OIuihsdYObK%L!s|{0+I)f z5DTzWu0+=M_W&vA;n5?TWXj`@A?J&FKwxl}S3teoryDkv%!8x(D3>al$AkXW>)~ttD+VwE1kQ`b1;kIz zB*b^J1%uD7#FU51pkD5aWR}^C>|J#{q*q4YGta|!wnLvjS-`MP%Af$Cw4Z^}eugt@ z+prd<;(Iu~tF`aLceW$xFmnsI6`(N}2uXzX!ZYk-lhA zu#AO8RdHoU9W;ZN0R4pK7a0f6c!W+Ebi50MZS5*fSN`c2C|z9>4zm$3{G2psTn`vL zW14p#(jI((8*}ZehLplg6uW?=Koi~*4egoh)^KBdr_W&Q&(@prV$z`Fh1qCKuuq3& z*%EH-wT1(_$5Zv<4M7uamiUtprdGWUW-{sH)vH4oKG6*ACIBOC6o1zz6K3NA(kq@o z*6Noy8=l5P(0sm>!yGRP`mL3?7wM1`(--L%o%wKl@-YXgboby&{T|&D1i-d*2{Ime zI5M6{haAs-IKPPi!Kk1ayr@xhd-yqMz%=JxIGc__oR(t<2chq+8mL}b*JtVJ$lX?p zyAJL{){5mf0E~oMhYWjkqFyA%U|s$c3XUI!G$W7m=1{|acz^qyTUhq1ufUX-2WFB` zuxlqA)m4Dm%-PnA_6=S%>aZ?n5y3zon=hec&dWpkf^uY5zKFE>C5RT!u@mUs@-|4* z^s(afX(;}q1%j3Tpc@z%#NdHO4D4@&GB9As*~EFuhQDl};I0lXgF3o>y7RSJaWw@ys-L5J?Rk=k%5RhA21N+7e@dpw?>374|-2?xnOHfC=k+DW+1r#s} z*M}5D8ykcBgAZ|aeG{(KQ&OzD`U8+8H2k&hwsA0gn-89 zDYIuG_e~dao?gb)-2VmidfO=s9Xte2Q!_MqG^Ls-Iqa~nszmnUg-D-MoYd!f&F=>r zs^6=FM(2m-m7#QZL+S2;zv~i|&`o~4FvmU{77(>r1e+Ds`{%>#bV8h(1tBAY1B~Qo z0PNh1SzZc#|HPlo(5FZUF@ci>F}BCCPf=q*vOt730$<>bHH|O!5ie?n!X2wKeof^1 zjW1o7qTAa9BD6*fB`{zBoXF7^d%6Yx;x9gY>hTT=Nh|Ac6c%ZM&`u1N$qRE-E=K@C zAX$P^A{4v3IsvM_RbEUj%zVF57*Tb(+ubPuz}&uzdo_(*Dv_**!~hsKR3QC=qEDO* zAd&+lkjzMPEg0zC3xNOjzTHt+1O^v?G79}5cDuvO2yFmbMW)o3yWRQ^V*dez9rQ~~ Srxt<$0000true true AutoMapper.Extensions.ExpressionMapping - https://s3.amazonaws.com/automapper/icon.png + icon.png http://automapper.org https://github.com/AutoMapper/AutoMapper.Extensions.ExpressionMapping/blob/master/LICENSE git @@ -23,6 +23,10 @@ true + + + + From d7f1cecabd3151f4e32cb35754b10a92d5c8f3f8 Mon Sep 17 00:00:00 2001 From: Blaise Taylor Date: Thu, 16 Jun 2022 07:37:32 -0400 Subject: [PATCH 04/18] PackagePath --- .../AutoMapper.Extensions.ExpressionMapping.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AutoMapper.Extensions.ExpressionMapping/AutoMapper.Extensions.ExpressionMapping.csproj b/src/AutoMapper.Extensions.ExpressionMapping/AutoMapper.Extensions.ExpressionMapping.csproj index 05770ae..ef53d09 100644 --- a/src/AutoMapper.Extensions.ExpressionMapping/AutoMapper.Extensions.ExpressionMapping.csproj +++ b/src/AutoMapper.Extensions.ExpressionMapping/AutoMapper.Extensions.ExpressionMapping.csproj @@ -24,7 +24,7 @@ - + From 88171611301b62f52ffa86469904cf7f447fb30a Mon Sep 17 00:00:00 2001 From: Blaise Taylor Date: Tue, 28 Jun 2022 06:19:02 -0400 Subject: [PATCH 05/18] Reduce the need for custom expressions. (#144) Reducing the need for custom expressions in literal member maps. --- ...Mapper.Extensions.ExpressionMapping.csproj | 15 + .../FindMemberExpressionsVisitor.cs | 4 +- .../MapperExtensions.cs | 10 +- .../Resources.Designer.cs} | 22 +- .../Properties/Resources.resx | 146 ++++++++++ .../Resource.resx | 161 ----------- .../XpressionMapperVisitor.cs | 45 +-- ...mberExpressionsWithoutCustomExpressions.cs | 269 ++++++++++++++++++ ...erMappingsOfLiteralParentTypesMustMatch.cs | 114 ++++++++ ...dOperationExceptionForUnmatchedLiterals.cs | 134 --------- 10 files changed, 593 insertions(+), 327 deletions(-) rename src/AutoMapper.Extensions.ExpressionMapping/{Resource.Designer.cs => Properties/Resources.Designer.cs} (88%) create mode 100644 src/AutoMapper.Extensions.ExpressionMapping/Properties/Resources.resx delete mode 100644 src/AutoMapper.Extensions.ExpressionMapping/Resource.resx create mode 100644 tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/CanMapMismatchedLiteralMemberExpressionsWithoutCustomExpressions.cs create mode 100644 tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/MemberMappingsOfLiteralParentTypesMustMatch.cs diff --git a/src/AutoMapper.Extensions.ExpressionMapping/AutoMapper.Extensions.ExpressionMapping.csproj b/src/AutoMapper.Extensions.ExpressionMapping/AutoMapper.Extensions.ExpressionMapping.csproj index ef53d09..0ac4660 100644 --- a/src/AutoMapper.Extensions.ExpressionMapping/AutoMapper.Extensions.ExpressionMapping.csproj +++ b/src/AutoMapper.Extensions.ExpressionMapping/AutoMapper.Extensions.ExpressionMapping.csproj @@ -36,4 +36,19 @@ + + + True + True + Resources.resx + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + diff --git a/src/AutoMapper.Extensions.ExpressionMapping/FindMemberExpressionsVisitor.cs b/src/AutoMapper.Extensions.ExpressionMapping/FindMemberExpressionsVisitor.cs index 24234bc..fe1543e 100644 --- a/src/AutoMapper.Extensions.ExpressionMapping/FindMemberExpressionsVisitor.cs +++ b/src/AutoMapper.Extensions.ExpressionMapping/FindMemberExpressionsVisitor.cs @@ -30,7 +30,7 @@ public MemberExpression Result if (string.IsNullOrEmpty(result) || next.Contains(result)) result = next; else throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, - Resource.includeExpressionTooComplex, + Properties.Resources.includeExpressionTooComplex, string.Concat(_newParentExpression.Type.Name, period, result), string.Concat(_newParentExpression.Type.Name, period, next))); @@ -50,7 +50,7 @@ protected override Expression VisitMember(MemberExpression node) if (node.Expression.NodeType == ExpressionType.MemberAccess && node.Type.IsLiteralType()) _memberExpressions.Add((MemberExpression)node.Expression); else if (node.Expression.NodeType == ExpressionType.Parameter && node.Type.IsLiteralType()) - throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Resource.mappedMemberIsChildOfTheParameterFormat, node.GetPropertyFullName(), node.Type.FullName, sType.FullName)); + throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Properties.Resources.mappedMemberIsChildOfTheParameterFormat, node.GetPropertyFullName(), node.Type.FullName, sType.FullName)); else _memberExpressions.Add(node); } diff --git a/src/AutoMapper.Extensions.ExpressionMapping/MapperExtensions.cs b/src/AutoMapper.Extensions.ExpressionMapping/MapperExtensions.cs index c8df785..5051cf8 100644 --- a/src/AutoMapper.Extensions.ExpressionMapping/MapperExtensions.cs +++ b/src/AutoMapper.Extensions.ExpressionMapping/MapperExtensions.cs @@ -109,7 +109,7 @@ TDestDelegate MapBody(Dictionary typeMappings, XpressionMapperVisito TDestDelegate GetLambda(Dictionary typeMappings, XpressionMapperVisitor visitor, Expression mappedBody) { if (mappedBody == null) - throw new InvalidOperationException(Resource.cantRemapExpression); + throw new InvalidOperationException(Properties.Resources.cantRemapExpression); return (TDestDelegate)Lambda ( @@ -255,7 +255,7 @@ public static List GetDestinationParameterExpressions(this /// public static Dictionary AddTypeMapping(this Dictionary typeMappings, IConfigurationProvider configurationProvider) => typeMappings == null - ? throw new ArgumentException(Resource.typeMappingsDictionaryIsNull) + ? throw new ArgumentException(Properties.Resources.typeMappingsDictionaryIsNull) : typeMappings.AddTypeMapping(configurationProvider, typeof(TSource), typeof(TDest)); private static bool HasUnderlyingType(this Type type) @@ -284,7 +284,7 @@ private static void AddUnderlyingTypes(this Dictionary typeMappings, public static Dictionary AddTypeMapping(this Dictionary typeMappings, IConfigurationProvider configurationProvider, Type sourceType, Type destType) { if (typeMappings == null) - throw new ArgumentException(Resource.typeMappingsDictionaryIsNull); + throw new ArgumentException(Properties.Resources.typeMappingsDictionaryIsNull); if (sourceType.GetTypeInfo().IsGenericType && sourceType.GetGenericTypeDefinition() == typeof(Expression<>)) { @@ -357,7 +357,7 @@ public static Type ReplaceType(this Dictionary typeMappings, Type so private static Dictionary AddTypeMappingsFromDelegates(this Dictionary typeMappings, IConfigurationProvider configurationProvider, Type sourceType, Type destType) { if (typeMappings == null) - throw new ArgumentException(Resource.typeMappingsDictionaryIsNull); + throw new ArgumentException(Properties.Resources.typeMappingsDictionaryIsNull); typeMappings.DoAddTypeMappingsFromDelegates ( @@ -372,7 +372,7 @@ private static Dictionary AddTypeMappingsFromDelegates(this Dictiona private static void DoAddTypeMappingsFromDelegates(this Dictionary typeMappings, IConfigurationProvider configurationProvider, List sourceArguments, List destArguments) { if (sourceArguments.Count != destArguments.Count) - throw new ArgumentException(Resource.invalidArgumentCount); + throw new ArgumentException(Properties.Resources.invalidArgumentCount); for (int i = 0; i < sourceArguments.Count; i++) { diff --git a/src/AutoMapper.Extensions.ExpressionMapping/Resource.Designer.cs b/src/AutoMapper.Extensions.ExpressionMapping/Properties/Resources.Designer.cs similarity index 88% rename from src/AutoMapper.Extensions.ExpressionMapping/Resource.Designer.cs rename to src/AutoMapper.Extensions.ExpressionMapping/Properties/Resources.Designer.cs index 8eddfcd..461bc9c 100644 --- a/src/AutoMapper.Extensions.ExpressionMapping/Resource.Designer.cs +++ b/src/AutoMapper.Extensions.ExpressionMapping/Properties/Resources.Designer.cs @@ -8,9 +8,8 @@ // //------------------------------------------------------------------------------ -namespace AutoMapper.Extensions.ExpressionMapping { +namespace AutoMapper.Extensions.ExpressionMapping.Properties { using System; - using System.Reflection; /// @@ -20,17 +19,17 @@ namespace AutoMapper.Extensions.ExpressionMapping { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resource { + internal class Resources { private static global::System.Resources.ResourceManager resourceMan; private static global::System.Globalization.CultureInfo resourceCulture; [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resource() { + internal Resources() { } /// @@ -40,7 +39,7 @@ internal Resource() { internal static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("AutoMapper.Extensions.ExpressionMapping.Resource", typeof(Resource).GetTypeInfo().Assembly); + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("AutoMapper.Extensions.ExpressionMapping.Properties.Resources", typeof(Resources).Assembly); resourceMan = temp; } return resourceMan; @@ -89,7 +88,7 @@ internal static string customResolversNotSupported { } /// - /// Looks up a localized string similar to The source and destination types must be the same for expression mapping between value types. Source Type: {0}, Source Description: {1}, Destination Type: {2}, Destination Property: {3}.. + /// Looks up a localized string similar to The source and destination types must be the same for expression mapping between literal types. Source Type: {0}, Source Description: {1}, Destination Type: {2}, Destination Property: {3}.. /// internal static string expressionMapValueTypeMustMatchFormat { get { @@ -124,6 +123,15 @@ internal static string invalidExpErr { } } + /// + /// Looks up a localized string similar to For members of literal types, use IMappingExpression.ForMember() to make the parent property types an exact match. Parent Source Type: {0}, Parent Destination Type: {1}, Full Member Name "{2}".. + /// + internal static string makeParentTypesMatchForMembersOfLiteralsFormat { + get { + return ResourceManager.GetString("makeParentTypesMatchForMembersOfLiteralsFormat", resourceCulture); + } + } + /// /// Looks up a localized string similar to The mapped member {0} is of type {1} and a child of the parameter type {2}. No reference type (parent of) {0} is available to map as an include.. /// diff --git a/src/AutoMapper.Extensions.ExpressionMapping/Properties/Resources.resx b/src/AutoMapper.Extensions.ExpressionMapping/Properties/Resources.resx new file mode 100644 index 0000000..da39c6c --- /dev/null +++ b/src/AutoMapper.Extensions.ExpressionMapping/Properties/Resources.resx @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Cannot create a binary expression for the following pair. Node: {0}, Type: {1} and Node: {2}, Type: {3}. + 0=leftNode; 1=leftNodeType; 2=rightNode; 3=rightNodeType + + + Can't rempa expression + + + The source and destination types must be the same for expression mapping between literal types. Source Type: {0}, Source Description: {1}, Destination Type: {2}, Destination Property: {3}. + 0=Source Type; 1=SourceDescription; 2=Destination Type; 3=Destination Property. + + + The Include value-type expression uses multiple sibling navigation objects "{0}", "{1}" and is too complex to translate. + 0=FirstNavigationProperty, 1=SecondNavigationProperty + + + Source and destination must have the same number of arguments. + + + Invalid expression type for this operation. + + + Mapper Info dictionary cannot be null. + + + SourceMember cannot be null. Source Type: {0}, Destination Type: {1}, Property: {2}. + 0=SorceType; 1=DestinationType; 2=Name of the source property + + + Type Mappings dictionary cannot be null. + + + Custom resolvers are not supported for expression mapping. + + + Arguments must be expressions. + + + The mapped member {0} is of type {1} and a child of the parameter type {2}. No reference type (parent of) {0} is available to map as an include. + 0=memberName, 1=memberType; 2=parameterType + + + For members of literal types, use IMappingExpression.ForMember() to make the parent property types an exact match. Parent Source Type: {0}, Parent Destination Type: {1}, Full Member Name "{2}". + 0=typeSource, 1=typeDestination; 2=sourceFullName + + \ No newline at end of file diff --git a/src/AutoMapper.Extensions.ExpressionMapping/Resource.resx b/src/AutoMapper.Extensions.ExpressionMapping/Resource.resx deleted file mode 100644 index ddc224b..0000000 --- a/src/AutoMapper.Extensions.ExpressionMapping/Resource.resx +++ /dev/null @@ -1,161 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - Cannot create a binary expression for the following pair. Node: {0}, Type: {1} and Node: {2}, Type: {3}. - 0=leftNode; 1=leftNodeType; 2=rightNode; 3=rightNodeType - - - Can't rempa expression - - - The source and destination types must be the same for expression mapping between literal types. Source Type: {0}, Source Description: {1}, Destination Type: {2}, Destination Property: {3}. - 0=Source Type; 1=SourceDescription; 2=Destination Type; 3=Destination Property. - - - The Include value-type expression uses multiple sibling navigation objects "{0}", "{1}" and is too complex to translate. - 0=FirstNavigationProperty, 1=SecondNavigationProperty - - - Source and destination must have the same number of arguments. - - - Invalid expression type for this operation. - - - Mapper Info dictionary cannot be null. - - - SourceMember cannot be null. Source Type: {0}, Destination Type: {1}, Property: {2}. - 0=SorceType; 1=DestinationType; 2=Name of the source property - - - Type Mappings dictionary cannot be null. - - - Custom resolvers are not supported for expression mapping. - - - Arguments must be expressions. - - - The mapped member {0} is of type {1} and a child of the parameter type {2}. No reference type (parent of) {0} is available to map as an include. - 0=memberName, 1=memberType; 2=parameterType - - \ No newline at end of file diff --git a/src/AutoMapper.Extensions.ExpressionMapping/XpressionMapperVisitor.cs b/src/AutoMapper.Extensions.ExpressionMapping/XpressionMapperVisitor.cs index f1b9201..f8986b1 100644 --- a/src/AutoMapper.Extensions.ExpressionMapping/XpressionMapperVisitor.cs +++ b/src/AutoMapper.Extensions.ExpressionMapping/XpressionMapperVisitor.cs @@ -95,11 +95,17 @@ Expression GetMappedMemberExpression(Expression parentExpression, List propertyMapInfoList) { + if (typeSource.IsLiteralType() + && typeDestination.IsLiteralType() + && typeSource != typeDestination) + { + throw new InvalidOperationException + ( + string.Format + ( + CultureInfo.CurrentCulture, + Properties.Resources.makeParentTypesMatchForMembersOfLiteralsFormat, + typeSource, + typeDestination, + sourceFullName + ) + ); + } + const string period = "."; bool BothTypesAreAnonymous() => IsAnonymousType(typeSource) && IsAnonymousType(typeDestination); @@ -696,25 +719,11 @@ TypeMap GetTypeMap() => BothTypesAreAnonymous() var sourceMemberInfo = typeSource.GetFieldOrProperty(propertyMap.GetDestinationName()); if (propertyMap.ValueResolverConfig != null) { - throw new InvalidOperationException(Resource.customResolversNotSupported); + throw new InvalidOperationException(Properties.Resources.customResolversNotSupported); } if (propertyMap.CustomMapExpression == null && !propertyMap.SourceMembers.Any()) - throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Resource.srcMemberCannotBeNullFormat, typeSource.Name, typeDestination.Name, sourceFullName)); - - CompareSourceAndDestLiterals - ( - propertyMap.CustomMapExpression != null ? propertyMap.CustomMapExpression.ReturnType : propertyMap.SourceMember.GetMemberType(), - propertyMap.CustomMapExpression != null ? propertyMap.CustomMapExpression.ToString() : propertyMap.SourceMember.Name, - sourceMemberInfo.GetMemberType() - ); - - void CompareSourceAndDestLiterals(Type mappedPropertyType, string mappedPropertyDescription, Type sourceMemberType) - { - //switch from IsValueType to IsLiteralType because we do not want to throw an exception for all structs - if ((mappedPropertyType.IsLiteralType() || sourceMemberType.IsLiteralType()) && sourceMemberType != mappedPropertyType) - throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Resource.expressionMapValueTypeMustMatchFormat, mappedPropertyType.Name, mappedPropertyDescription, sourceMemberType.Name, propertyMap.GetDestinationName())); - } + throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Properties.Resources.srcMemberCannotBeNullFormat, typeSource.Name, typeDestination.Name, sourceFullName)); if (propertyMap.IncludedMember?.ProjectToCustomSource != null) propertyMapInfoList.Add(new PropertyMapInfo(propertyMap.IncludedMember.ProjectToCustomSource, new List())); @@ -728,7 +737,7 @@ void CompareSourceAndDestLiterals(Type mappedPropertyType, string mappedProperty var sourceMemberInfo = typeSource.GetFieldOrProperty(propertyMap.GetDestinationName()); 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)); + throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Properties.Resources.srcMemberCannotBeNullFormat, typeSource.Name, typeDestination.Name, propertyName)); if (propertyMap.IncludedMember?.ProjectToCustomSource != null) propertyMapInfoList.Add(new PropertyMapInfo(propertyMap.IncludedMember.ProjectToCustomSource, new List())); diff --git a/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/CanMapMismatchedLiteralMemberExpressionsWithoutCustomExpressions.cs b/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/CanMapMismatchedLiteralMemberExpressionsWithoutCustomExpressions.cs new file mode 100644 index 0000000..cfd5472 --- /dev/null +++ b/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/CanMapMismatchedLiteralMemberExpressionsWithoutCustomExpressions.cs @@ -0,0 +1,269 @@ +using Microsoft.OData.Edm; +using System; +using System.Linq.Expressions; +using Xunit; + +namespace AutoMapper.Extensions.ExpressionMapping.UnitTests +{ + public class CanMapMismatchedLiteralMemberExpressionsWithoutCustomExpressions + { + [Theory] + [InlineData(nameof(ProductModel.Bool), typeof(bool))] + [InlineData(nameof(ProductModel.DateTime), typeof(DateTime))] + [InlineData(nameof(ProductModel.DateTimeOffset), typeof(DateTimeOffset))] + [InlineData(nameof(ProductModel.Date), typeof(Date))] + [InlineData(nameof(ProductModel.DateOnly), typeof(DateOnly))] + [InlineData(nameof(ProductModel.TimeSpan), typeof(TimeSpan))] + [InlineData(nameof(ProductModel.TimeOfDay), typeof(TimeOfDay))] + [InlineData(nameof(ProductModel.TimeOnly), typeof(TimeOnly))] + [InlineData(nameof(ProductModel.Guid), typeof(Guid))] + [InlineData(nameof(ProductModel.Decimal), typeof(decimal))] + [InlineData(nameof(ProductModel.Byte), typeof(byte))] + [InlineData(nameof(ProductModel.Short), typeof(short))] + [InlineData(nameof(ProductModel.Int), typeof(int))] + [InlineData(nameof(ProductModel.Long), typeof(long))] + [InlineData(nameof(ProductModel.Float), typeof(float))] + [InlineData(nameof(ProductModel.Double), typeof(double))] + [InlineData(nameof(ProductModel.Char), typeof(char))] + [InlineData(nameof(ProductModel.SByte), typeof(sbyte))] + [InlineData(nameof(ProductModel.UShort), typeof(ushort))] + [InlineData(nameof(ProductModel.ULong), typeof(ulong))] + public void CanMapNonNullableToNullableWithoutCustomExpression(string memberName, Type constantType) + { + //arrange + var mapper = GetDataToModelMapper(); + ParameterExpression productParam = Expression.Parameter(typeof(ProductModel), "x"); + MemberExpression property = Expression.MakeMemberAccess(productParam, AutoMapper.Internal.TypeExtensions.GetFieldOrProperty(typeof(ProductModel), memberName)); + object constantValue = Activator.CreateInstance(constantType); + Expression> expression = Expression.Lambda> + ( + Expression.Equal + ( + property, + Expression.Constant(constantValue, constantType) + ), + productParam + ); + Product product = new(); + typeof(Product).GetProperty(memberName).SetValue(product, constantValue); + + //act + var mappedExpression = mapper.MapExpression>>(expression); + + //assert + Assert.True(mappedExpression.Compile()(product)); + } + + [Theory] + [InlineData(nameof(Product.Bool), typeof(bool?))] + [InlineData(nameof(Product.DateTime), typeof(DateTime?))] + [InlineData(nameof(Product.DateTimeOffset), typeof(DateTimeOffset?))] + [InlineData(nameof(Product.Date), typeof(Date?))] + [InlineData(nameof(Product.DateOnly), typeof(DateOnly?))] + [InlineData(nameof(Product.TimeSpan), typeof(TimeSpan?))] + [InlineData(nameof(Product.TimeOfDay), typeof(TimeOfDay?))] + [InlineData(nameof(Product.TimeOnly), typeof(TimeOnly?))] + [InlineData(nameof(Product.Guid), typeof(Guid?))] + [InlineData(nameof(Product.Decimal), typeof(decimal?))] + [InlineData(nameof(Product.Byte), typeof(byte?))] + [InlineData(nameof(Product.Short), typeof(short?))] + [InlineData(nameof(Product.Int), typeof(int?))] + [InlineData(nameof(Product.Long), typeof(long?))] + [InlineData(nameof(Product.Float), typeof(float?))] + [InlineData(nameof(Product.Double), typeof(double?))] + [InlineData(nameof(Product.Char), typeof(char?))] + [InlineData(nameof(Product.SByte), typeof(sbyte?))] + [InlineData(nameof(Product.UShort), typeof(ushort?))] + [InlineData(nameof(Product.ULong), typeof(ulong?))] + public void CanMapNullableToNonNullableWithoutCustomExpression(string memberName, Type constantType) + { + //arrange + var mapper = GetModelToDataMapper(); + ParameterExpression productParam = Expression.Parameter(typeof(Product), "x"); + MemberExpression property = Expression.MakeMemberAccess(productParam, AutoMapper.Internal.TypeExtensions.GetFieldOrProperty(typeof(Product), memberName)); + object constantValue = Activator.CreateInstance(Nullable.GetUnderlyingType(constantType)); + Expression> expression = Expression.Lambda> + ( + Expression.Equal + ( + property, + Expression.Constant(constantValue, constantType) + ), + productParam + ); + ProductModel product = new(); + typeof(ProductModel).GetProperty(memberName).SetValue(product, constantValue); + + //act + var mappedExpression = mapper.MapExpression>>(expression); + + //assert + Assert.True(mappedExpression.Compile()(product)); + } + + [Theory] + [InlineData(nameof(ProductModel.Bool), typeof(bool))] + [InlineData(nameof(ProductModel.DateTime), typeof(DateTime))] + [InlineData(nameof(ProductModel.DateTimeOffset), typeof(DateTimeOffset))] + [InlineData(nameof(ProductModel.Date), typeof(Date))] + [InlineData(nameof(ProductModel.DateOnly), typeof(DateOnly))] + [InlineData(nameof(ProductModel.TimeSpan), typeof(TimeSpan))] + [InlineData(nameof(ProductModel.TimeOfDay), typeof(TimeOfDay))] + [InlineData(nameof(ProductModel.TimeOnly), typeof(TimeOnly))] + [InlineData(nameof(ProductModel.Guid), typeof(Guid))] + [InlineData(nameof(ProductModel.Decimal), typeof(decimal))] + [InlineData(nameof(ProductModel.Byte), typeof(byte))] + [InlineData(nameof(ProductModel.Short), typeof(short))] + [InlineData(nameof(ProductModel.Int), typeof(int))] + [InlineData(nameof(ProductModel.Long), typeof(long))] + [InlineData(nameof(ProductModel.Float), typeof(float))] + [InlineData(nameof(ProductModel.Double), typeof(double))] + [InlineData(nameof(ProductModel.Char), typeof(char))] + [InlineData(nameof(ProductModel.SByte), typeof(sbyte))] + [InlineData(nameof(ProductModel.UShort), typeof(ushort))] + [InlineData(nameof(ProductModel.ULong), typeof(ulong))] + public void CanMapNonNullableSelectorToNullableelectorWithoutCustomExpression(string memberName, Type memberType) + { + var mapper = GetDataToModelMapper(); + ParameterExpression productParam = Expression.Parameter(typeof(ProductModel), "x"); + MemberExpression property = Expression.MakeMemberAccess(productParam, AutoMapper.Internal.TypeExtensions.GetFieldOrProperty(typeof(ProductModel), memberName)); + Type sourceType = typeof(Func<,>).MakeGenericType(typeof(ProductModel), memberType); + Type destType = typeof(Func<,>).MakeGenericType(typeof(Product), memberType); + Type sourceExpressionype = typeof(Expression<>).MakeGenericType(sourceType); + Type destExpressionType = typeof(Expression<>).MakeGenericType(destType); + var expression = Expression.Lambda + ( + sourceType, + property, + productParam + ); + object constantValue = Activator.CreateInstance(memberType); + Product product = new(); + typeof(Product).GetProperty(memberName).SetValue(product, constantValue); + + //act + var mappedExpression = mapper.MapExpression(expression, sourceExpressionype, destExpressionType); + + //assert + Assert.Equal(constantValue, mappedExpression.Compile().DynamicInvoke(product)); + } + + [Theory] + [InlineData(nameof(Product.Bool), typeof(bool?))] + [InlineData(nameof(Product.DateTime), typeof(DateTime?))] + [InlineData(nameof(Product.DateTimeOffset), typeof(DateTimeOffset?))] + [InlineData(nameof(Product.Date), typeof(Date?))] + [InlineData(nameof(Product.DateOnly), typeof(DateOnly?))] + [InlineData(nameof(Product.TimeSpan), typeof(TimeSpan?))] + [InlineData(nameof(Product.TimeOfDay), typeof(TimeOfDay?))] + [InlineData(nameof(Product.TimeOnly), typeof(TimeOnly?))] + [InlineData(nameof(Product.Guid), typeof(Guid?))] + [InlineData(nameof(Product.Decimal), typeof(decimal?))] + [InlineData(nameof(Product.Byte), typeof(byte?))] + [InlineData(nameof(Product.Short), typeof(short?))] + [InlineData(nameof(Product.Int), typeof(int?))] + [InlineData(nameof(Product.Long), typeof(long?))] + [InlineData(nameof(Product.Float), typeof(float?))] + [InlineData(nameof(Product.Double), typeof(double?))] + [InlineData(nameof(Product.Char), typeof(char?))] + [InlineData(nameof(Product.SByte), typeof(sbyte?))] + [InlineData(nameof(Product.UShort), typeof(ushort?))] + [InlineData(nameof(Product.ULong), typeof(ulong?))] + public void CanMapNullableSelectorToNonNullableelectorWithoutCustomExpression(string memberName, Type memberType) + { + var mapper = GetModelToDataMapper(); + ParameterExpression productParam = Expression.Parameter(typeof(Product), "x"); + MemberExpression property = Expression.MakeMemberAccess(productParam, AutoMapper.Internal.TypeExtensions.GetFieldOrProperty(typeof(Product), memberName)); + Type sourceType = typeof(Func<,>).MakeGenericType(typeof(Product), memberType); + Type destType = typeof(Func<,>).MakeGenericType(typeof(ProductModel), memberType); + Type sourceExpressionype = typeof(Expression<>).MakeGenericType(sourceType); + Type destExpressionType = typeof(Expression<>).MakeGenericType(destType); + var expression = Expression.Lambda + ( + sourceType, + property, + productParam + ); + + object constantValue = Activator.CreateInstance(Nullable.GetUnderlyingType(memberType)); + ProductModel product = new(); + typeof(ProductModel).GetProperty(memberName).SetValue(product, constantValue); + + //act + var mappedExpression = mapper.MapExpression(expression, sourceExpressionype, destExpressionType); + + //assert + Assert.Equal(constantValue, mappedExpression.Compile().DynamicInvoke(product)); + } + + private static IMapper GetModelToDataMapper() + { + var config = new MapperConfiguration(c => + { + c.CreateMap(); + }); + config.AssertConfigurationIsValid(); + return config.CreateMapper(); + } + + private static IMapper GetDataToModelMapper() + { + var config = new MapperConfiguration(c => + { + c.CreateMap(); + }); + config.AssertConfigurationIsValid(); + return config.CreateMapper(); + } + + class Product + { + public bool? Bool { get; set; } + public DateTimeOffset? DateTimeOffset { get; set; } + public DateTime? DateTime { get; set; } + public Date? Date { get; set; } + public DateOnly? DateOnly { get; set; } + public TimeSpan? TimeSpan { get; set; } + public TimeOfDay? TimeOfDay { get; set; } + public TimeOnly? TimeOnly { get; set; } + public Guid? Guid { get; set; } + public decimal? Decimal { get; set; } + public byte? Byte { get; set; } + public short? Short { get; set; } + public int? Int { get; set; } + public long? Long { get; set; } + public float? Float { get; set; } + public double? Double { get; set; } + public char? Char { get; set; } + public sbyte? SByte { get; set; } + public ushort? UShort { get; set; } + public uint? UInt { get; set; } + public ulong? ULong { get; set; } + } + + class ProductModel + { + public bool Bool { get; set; } + public DateTimeOffset DateTimeOffset { get; set; } + public DateTime DateTime { get; set; } + public Date Date { get; set; } + public DateOnly DateOnly { get; set; } + public TimeSpan TimeSpan { get; set; } + public TimeOfDay TimeOfDay { get; set; } + public TimeOnly TimeOnly { get; set; } + public Guid Guid { get; set; } + public decimal Decimal { get; set; } + public byte Byte { get; set; } + public short Short { get; set; } + public int Int { get; set; } + public long Long { get; set; } + public float Float { get; set; } + public double Double { get; set; } + public char Char { get; set; } + public sbyte SByte { get; set; } + public ushort UShort { get; set; } + public uint UInt { get; set; } + public ulong ULong { get; set; } + } + } +} diff --git a/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/MemberMappingsOfLiteralParentTypesMustMatch.cs b/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/MemberMappingsOfLiteralParentTypesMustMatch.cs new file mode 100644 index 0000000..27b7a4d --- /dev/null +++ b/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/MemberMappingsOfLiteralParentTypesMustMatch.cs @@ -0,0 +1,114 @@ +using System; +using System.Linq.Expressions; +using Xunit; + +namespace AutoMapper.Extensions.ExpressionMapping.UnitTests +{ + public class MemberMappingsOfLiteralParentTypesMustMatch + { + [Fact] + public void MappingMemberOfNullableParentToMemberOfNonNullableParentWithoutCustomExpressionsThrowsException() + { + //arrange + var mapper = GetMapper(); + Expression> expression = x => x.DateTimeOffset.Value.Day.ToString() == "2"; + + //act + var exception = Assert.Throws(() => mapper.MapExpression>>(expression)); + + //assert + Assert.StartsWith + ( + "For members of literal types, use IMappingExpression.ForMember() to make the parent property types an exact match.", + exception.Message + ); + } + + [Fact] + public void MappingMemberOfNonNullableParentToMemberOfNullableParentWithoutCustomExpressionsThrowsException() + { + //arrange + var mapper = GetMapper(); + Expression> expression = x => x.DateTime.Day.ToString() == "2"; + + //act + var exception = Assert.Throws(() => mapper.MapExpression>>(expression)); + + //assert + Assert.StartsWith + ( + "For members of literal types, use IMappingExpression.ForMember() to make the parent property types an exact match.", + exception.Message + ); + } + + [Fact] + public void MappingMemberOfNullableParentToMemberOfNonNullableParentWorksUsingCustomExpressions() + { + //arrange + var mapper = GetMapperWithCustomExpressions(); + Expression> expression = x => x.DateTimeOffset.Value.Day.ToString() == "2"; + + //act + var mappedExpression = mapper.MapExpression>>(expression); + + //assert + Assert.NotNull(mappedExpression); + Func func = mappedExpression.Compile(); + Assert.False(func(new Product { DateTimeOffset = new DateTimeOffset(new DateTime(2000, 3, 3), TimeSpan.Zero) })); + Assert.True(func(new Product { DateTimeOffset = new DateTimeOffset(new DateTime(2000, 2, 2), TimeSpan.Zero) })); + } + + [Fact] + public void MappingMemberOfNonNullableParentToMemberOfNullableParentWorksUsingCustomExpressions() + { + //arrange + var mapper = GetMapperWithCustomExpressions(); + Expression> expression = x => x.DateTime.Day.ToString() == "2"; + + //act + var mappedExpression = mapper.MapExpression>>(expression); + + //assert + Assert.NotNull(mappedExpression); + Func func = mappedExpression.Compile(); + Assert.False(func(new Product { DateTime = new DateTime(2000, 3, 3) })); + Assert.True(func(new Product { DateTime = new DateTime(2000, 2, 2) })); + } + + + private static IMapper GetMapper() + { + var config = new MapperConfiguration(c => + { + c.CreateMap(); + }); + config.AssertConfigurationIsValid(); + return config.CreateMapper(); + } + + private static IMapper GetMapperWithCustomExpressions() + { + var config = new MapperConfiguration(c => + { + c.CreateMap() + .ForMember(d => d.DateTime, o => o.MapFrom(s => s.DateTime.Value)) + .ForMember(d => d.DateTimeOffset, o => o.MapFrom(s => (DateTimeOffset?)s.DateTimeOffset)); + }); + config.AssertConfigurationIsValid(); + return config.CreateMapper(); + } + + class Product + { + public DateTime? DateTime { get; set; } + public DateTimeOffset DateTimeOffset { get; set; } + } + + class ProductModel + { + public DateTime DateTime { get; set; } + public DateTimeOffset? DateTimeOffset { get; set; } + } + } +} diff --git a/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/ShouldThrowInvalidOperationExceptionForUnmatchedLiterals.cs b/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/ShouldThrowInvalidOperationExceptionForUnmatchedLiterals.cs index f2454c6..6fb88f9 100644 --- a/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/ShouldThrowInvalidOperationExceptionForUnmatchedLiterals.cs +++ b/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/ShouldThrowInvalidOperationExceptionForUnmatchedLiterals.cs @@ -30,7 +30,6 @@ public class ShouldThrowInvalidOperationExceptionForUnmatchedLiterals [InlineData(nameof(ProductModel.ULong), typeof(ulong?))] public void ThrowsCreatingBinaryExpressionCombiningNonNullableParameterWithNullableConstant(string memberName, Type constantType) { - var mapper = GetMapper(); ParameterExpression productParam = Expression.Parameter(typeof(ProductModel), "x"); MemberExpression property = Expression.MakeMemberAccess(productParam, AutoMapper.Internal.TypeExtensions.GetFieldOrProperty(typeof(ProductModel), memberName)); @@ -71,7 +70,6 @@ public void ThrowsCreatingBinaryExpressionCombiningNonNullableParameterWithNulla [InlineData(nameof(Product.ULong), typeof(ulong))] public void ThrowsCreatingBinaryExpressionCombiningNullableParameterWithNonNullableConstant(string memberName, Type constantType) { - var mapper = GetMapper(); ParameterExpression productParam = Expression.Parameter(typeof(Product), "x"); MemberExpression property = Expression.MakeMemberAccess(productParam, AutoMapper.Internal.TypeExtensions.GetFieldOrProperty(typeof(Product), memberName)); @@ -89,138 +87,6 @@ public void ThrowsCreatingBinaryExpressionCombiningNullableParameterWithNonNulla ); } - [Theory] - [InlineData(nameof(ProductModel.Bool), typeof(bool))] - [InlineData(nameof(ProductModel.DateTime), typeof(DateTime))] - [InlineData(nameof(ProductModel.DateTimeOffset), typeof(DateTimeOffset))] - [InlineData(nameof(ProductModel.Date), typeof(Date))] - [InlineData(nameof(ProductModel.DateOnly), typeof(DateOnly))] - [InlineData(nameof(ProductModel.TimeSpan), typeof(TimeSpan))] - [InlineData(nameof(ProductModel.TimeOfDay), typeof(TimeOfDay))] - [InlineData(nameof(ProductModel.TimeOnly), typeof(TimeOnly))] - [InlineData(nameof(ProductModel.Guid), typeof(Guid))] - [InlineData(nameof(ProductModel.Decimal), typeof(decimal))] - [InlineData(nameof(ProductModel.Byte), typeof(byte))] - [InlineData(nameof(ProductModel.Short), typeof(short))] - [InlineData(nameof(ProductModel.Int), typeof(int))] - [InlineData(nameof(ProductModel.Long), typeof(long))] - [InlineData(nameof(ProductModel.Float), typeof(float))] - [InlineData(nameof(ProductModel.Double), typeof(double))] - [InlineData(nameof(ProductModel.Char), typeof(char))] - [InlineData(nameof(ProductModel.SByte), typeof(sbyte))] - [InlineData(nameof(ProductModel.UShort), typeof(ushort))] - [InlineData(nameof(ProductModel.ULong), typeof(ulong))] - public void ThrowsmappingExpressionWithMismatchedOperands(string memberName, Type constantType) - { - var mapper = GetMapper(); - ParameterExpression productParam = Expression.Parameter(typeof(ProductModel), "x"); - MemberExpression property = Expression.MakeMemberAccess(productParam, AutoMapper.Internal.TypeExtensions.GetFieldOrProperty(typeof(ProductModel), memberName)); - - Expression> expression = Expression.Lambda> - ( - Expression.Equal - ( - property, - Expression.Constant(Activator.CreateInstance(constantType), constantType) - ), - productParam - ); - - var exception = Assert.Throws - ( - () => mapper.MapExpression>>(expression) - ); - - Assert.StartsWith - ( - "The source and destination types must be the same for expression mapping between literal types.", - exception.Message - ); - } - - [Theory] - [InlineData(nameof(ProductModel.Bool), typeof(bool))] - [InlineData(nameof(ProductModel.DateTime), typeof(DateTime))] - [InlineData(nameof(ProductModel.DateTimeOffset), typeof(DateTimeOffset))] - [InlineData(nameof(ProductModel.Date), typeof(Date))] - [InlineData(nameof(ProductModel.DateOnly), typeof(DateOnly))] - [InlineData(nameof(ProductModel.TimeSpan), typeof(TimeSpan))] - [InlineData(nameof(ProductModel.TimeOfDay), typeof(TimeOfDay))] - [InlineData(nameof(ProductModel.TimeOnly), typeof(TimeOnly))] - [InlineData(nameof(ProductModel.Guid), typeof(Guid))] - [InlineData(nameof(ProductModel.Decimal), typeof(decimal))] - [InlineData(nameof(ProductModel.Byte), typeof(byte))] - [InlineData(nameof(ProductModel.Short), typeof(short))] - [InlineData(nameof(ProductModel.Int), typeof(int))] - [InlineData(nameof(ProductModel.Long), typeof(long))] - [InlineData(nameof(ProductModel.Float), typeof(float))] - [InlineData(nameof(ProductModel.Double), typeof(double))] - [InlineData(nameof(ProductModel.Char), typeof(char))] - [InlineData(nameof(ProductModel.SByte), typeof(sbyte))] - [InlineData(nameof(ProductModel.UShort), typeof(ushort))] - [InlineData(nameof(ProductModel.ULong), typeof(ulong))] - public void MappingExpressionWorksUsingCustomExpressionToResolveBinaryOperators(string memberName, Type constantType) - { - var mapper = GetMapperWithCustomExpressions(); - ParameterExpression productParam = Expression.Parameter(typeof(ProductModel), "x"); - MemberExpression property = Expression.MakeMemberAccess(productParam, AutoMapper.Internal.TypeExtensions.GetFieldOrProperty(typeof(ProductModel), memberName)); - - Expression> expression = Expression.Lambda> - ( - Expression.Equal - ( - property, - Expression.Constant(Activator.CreateInstance(constantType), constantType) - ), - productParam - ); - - var mappedExpression = mapper.MapExpression>>(expression); - Assert.NotNull(mappedExpression); - } - - private static IMapper GetMapper() - { - var config = new MapperConfiguration(c => - { - c.CreateMap(); - }); - config.AssertConfigurationIsValid(); - return config.CreateMapper(); - } - - private static IMapper GetMapperWithCustomExpressions() - { - var config = new MapperConfiguration(c => - { - c.CreateMap() - .ForMember(d => d.Bool, o => o.MapFrom(s => s.Bool.Value)) - .ForMember(d => d.DateTime, o => o.MapFrom(s => s.DateTime.Value)) - .ForMember(d => d.DateTimeOffset, o => o.MapFrom(s => s.DateTimeOffset.Value)) - .ForMember(d => d.Date, o => o.MapFrom(s => s.Date.Value)) - .ForMember(d => d.DateOnly, o => o.MapFrom(s => s.DateOnly.Value)) - .ForMember(d => d.TimeSpan, o => o.MapFrom(s => s.TimeSpan.Value)) - .ForMember(d => d.TimeOfDay, o => o.MapFrom(s => s.TimeOfDay.Value)) - .ForMember(d => d.TimeOnly, o => o.MapFrom(s => s.TimeOnly.Value)) - .ForMember(d => d.Guid, o => o.MapFrom(s => s.Guid.Value)) - .ForMember(d => d.Decimal, o => o.MapFrom(s => s.Decimal.Value)) - .ForMember(d => d.Byte, o => o.MapFrom(s => s.Byte.Value)) - .ForMember(d => d.Short, o => o.MapFrom(s => s.Short.Value)) - .ForMember(d => d.Int, o => o.MapFrom(s => s.Int.Value)) - .ForMember(d => d.Long, o => o.MapFrom(s => s.Long.Value)) - .ForMember(d => d.Float, o => o.MapFrom(s => s.Float.Value)) - .ForMember(d => d.Double, o => o.MapFrom(s => s.Double.Value)) - .ForMember(d => d.Char, o => o.MapFrom(s => s.Char.Value)) - .ForMember(d => d.SByte, o => o.MapFrom(s => s.SByte.Value)) - .ForMember(d => d.UShort, o => o.MapFrom(s => s.UShort.Value)) - .ForMember(d => d.UInt, o => o.MapFrom(s => s.UInt.Value)) - .ForMember(d => d.ULong, o => o.MapFrom(s => s.ULong.Value)); - }); - - config.AssertConfigurationIsValid(); - return config.CreateMapper(); - } - class Product { public bool? Bool { get; set; } From 5309cfa0faea91b5a967b4af52458be3a954dcc3 Mon Sep 17 00:00:00 2001 From: Blaise Taylor Date: Sat, 1 Oct 2022 06:50:26 -0400 Subject: [PATCH 06/18] Supporting AutoMapper v12. (#147) --- .../AutoMapper.Extensions.ExpressionMapping.csproj | 2 +- .../MapperExtensions.cs | 2 +- .../ReflectionExtensions.cs | 3 --- .../XpressionMapperVisitor.cs | 8 ++------ 4 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/AutoMapper.Extensions.ExpressionMapping/AutoMapper.Extensions.ExpressionMapping.csproj b/src/AutoMapper.Extensions.ExpressionMapping/AutoMapper.Extensions.ExpressionMapping.csproj index 0ac4660..933d7d6 100644 --- a/src/AutoMapper.Extensions.ExpressionMapping/AutoMapper.Extensions.ExpressionMapping.csproj +++ b/src/AutoMapper.Extensions.ExpressionMapping/AutoMapper.Extensions.ExpressionMapping.csproj @@ -28,7 +28,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/AutoMapper.Extensions.ExpressionMapping/MapperExtensions.cs b/src/AutoMapper.Extensions.ExpressionMapping/MapperExtensions.cs index 5051cf8..acf9c5a 100644 --- a/src/AutoMapper.Extensions.ExpressionMapping/MapperExtensions.cs +++ b/src/AutoMapper.Extensions.ExpressionMapping/MapperExtensions.cs @@ -396,7 +396,7 @@ private static void DoAddTypeMappings(this Dictionary typeMappings, private static Type GetSourceMemberType(this PropertyMap propertyMap) => propertyMap.CustomMapExpression != null ? propertyMap.CustomMapExpression.ReturnType - : propertyMap.SourceMember.GetMemberType(); + : propertyMap.SourceMembers.Last().GetMemberType(); private static void FindChildPropertyTypeMaps(this Dictionary typeMappings, IConfigurationProvider ConfigurationProvider, Type source, Type dest) { diff --git a/src/AutoMapper.Extensions.ExpressionMapping/ReflectionExtensions.cs b/src/AutoMapper.Extensions.ExpressionMapping/ReflectionExtensions.cs index 0e99879..22a3c02 100644 --- a/src/AutoMapper.Extensions.ExpressionMapping/ReflectionExtensions.cs +++ b/src/AutoMapper.Extensions.ExpressionMapping/ReflectionExtensions.cs @@ -11,9 +11,6 @@ namespace AutoMapper.Extensions.ExpressionMapping internal static class ReflectionExtensions { - public static object GetDefaultValue(this ParameterInfo parameter) - => ReflectionHelper.GetDefaultValue(parameter); - public static object MapMember(this ResolutionContext context, MemberInfo member, object value, object destination = null) => ReflectionHelper.MapMember(context, member, value, destination); diff --git a/src/AutoMapper.Extensions.ExpressionMapping/XpressionMapperVisitor.cs b/src/AutoMapper.Extensions.ExpressionMapping/XpressionMapperVisitor.cs index f8986b1..ecf33ef 100644 --- a/src/AutoMapper.Extensions.ExpressionMapping/XpressionMapperVisitor.cs +++ b/src/AutoMapper.Extensions.ExpressionMapping/XpressionMapperVisitor.cs @@ -388,7 +388,7 @@ private MemberBinding DoBind(MemberInfo sourceMember, Expression initial, Expres private MemberInfo GetSourceMember(PropertyMap propertyMap) => propertyMap.CustomMapExpression != null ? propertyMap.CustomMapExpression.GetMemberExpression()?.Member - : propertyMap.SourceMember; + : propertyMap.SourceMembers.Last(); private MemberInfo GetParentMember(PropertyMap propertyMap) => propertyMap.IncludedMember?.ProjectToCustomSource != null @@ -717,10 +717,6 @@ TypeMap GetTypeMap() => BothTypesAreAnonymous() { var propertyMap = typeMap.GetMemberMapByDestinationProperty(sourceFullName); var sourceMemberInfo = typeSource.GetFieldOrProperty(propertyMap.GetDestinationName()); - if (propertyMap.ValueResolverConfig != null) - { - throw new InvalidOperationException(Properties.Resources.customResolversNotSupported); - } if (propertyMap.CustomMapExpression == null && !propertyMap.SourceMembers.Any()) throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Properties.Resources.srcMemberCannotBeNullFormat, typeSource.Name, typeDestination.Name, sourceFullName)); @@ -746,7 +742,7 @@ TypeMap GetTypeMap() => BothTypesAreAnonymous() var childFullName = sourceFullName.Substring(sourceFullName.IndexOf(period, StringComparison.OrdinalIgnoreCase) + 1); FindDestinationFullName(sourceMemberInfo.GetMemberType(), propertyMap.CustomMapExpression == null - ? propertyMap.SourceMember.GetMemberType() + ? propertyMap.SourceMembers.Last().GetMemberType() : propertyMap.CustomMapExpression.ReturnType, childFullName, propertyMapInfoList); } } From 22a1a469e0b01b57b5793e2a6c1f52b7b85768bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Mitermite?= Date: Sat, 29 Oct 2022 12:23:03 +0200 Subject: [PATCH 07/18] Fixes #149 (#148) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Stéphane Mitermite --- .../MapIncludesVisitor.cs | 8 +++- .../XpressionMapperTests.cs | 37 ++++++++++++++++--- 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/src/AutoMapper.Extensions.ExpressionMapping/MapIncludesVisitor.cs b/src/AutoMapper.Extensions.ExpressionMapping/MapIncludesVisitor.cs index a224880..85898e3 100644 --- a/src/AutoMapper.Extensions.ExpressionMapping/MapIncludesVisitor.cs +++ b/src/AutoMapper.Extensions.ExpressionMapping/MapIncludesVisitor.cs @@ -2,8 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; -using System.Reflection; -using AutoMapper.Internal; using AutoMapper.Extensions.ExpressionMapping.Extensions; using AutoMapper.Extensions.ExpressionMapping.Structures; @@ -18,6 +16,9 @@ public MapIncludesVisitor(IMapper mapper, IConfigurationProvider configurationPr protected override Expression VisitLambda(Expression node) { + if (!node.Body.Type.IsLiteralType()) + return base.VisitLambda(node); + var ex = this.Visit(node.Body); var mapped = Expression.Lambda(ex, node.GetDestinationParameterExpressions(this.InfoDictionary, this.TypeMappings)); @@ -27,6 +28,9 @@ protected override Expression VisitLambda(Expression node) protected override Expression VisitMember(MemberExpression node) { + if (!node.Type.IsLiteralType()) + return base.VisitMember(node); + var parameterExpression = node.GetParameterExpression(); if (parameterExpression == null) return base.VisitMember(node); diff --git a/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/XpressionMapperTests.cs b/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/XpressionMapperTests.cs index b923e47..0346583 100644 --- a/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/XpressionMapperTests.cs +++ b/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/XpressionMapperTests.cs @@ -147,6 +147,20 @@ public void Map_includes_trim_string_nested_in_select_using_explicit_types() Assert.True(cars.Count == 4); } + private static bool IncludeTest(IQueryable model, Expression> navigationPropertyPath) + { + return !model.Equals(navigationPropertyPath); + } + + [Fact] + public void Map_includes_with_include_method_call() + { + Expression, bool>> selection = s => IncludeTest(s, s => s.ValidLines); + var expression = mapper.MapExpressionAsInclude, bool>>>(selection); + + Assert.True(expression.Compile()(new List() { new PurchaseOrder() }.AsQueryable())); + } + [Fact] public void Map_object_type_change() { @@ -625,8 +639,8 @@ public void Map_orderBy_thenBy_To_Dictionary_Select_expression_without_generic_t //Act Expression, IEnumerable>> expMapped = (Expression, IEnumerable>>)mapper.MapExpression ( - grouped, - typeof(Expression, IEnumerable>>), + grouped, + typeof(Expression, IEnumerable>>), typeof(Expression, IEnumerable>>) ); @@ -1019,7 +1033,7 @@ public void Can_map_expression_with_multiple_destination_parameters_of_the_same_ Assert.Equal (//parameters ch and c are of the same type - "c => ((c.Finished == null) AndAlso Not(c.Part.History.Any(ch => (ch.Started > c.Started))))", + "c => ((c.Finished == null) AndAlso Not(c.Part.History.Any(ch => (ch.Started > c.Started))))", mappedExpression.ToString() ); } @@ -1094,7 +1108,7 @@ private static void SetupQueryableCollection() new Thing { Bar = "Bar4", Car = new Car { Color = "White", Year = 2015 } } }, Type = "Business", - Users = new User[] + Users = new User[] { new User { @@ -1265,6 +1279,16 @@ public interface IUserModel : IIdentifiable bool IsActive { get; set; } } + public class PurchaseOrder + { + public IList Lines { get; set; } + } + + public class PurchaseOrderDTO + { + public IEnumerable ValidLines { get; set; } + } + public class OrderLine { public int Id { get; set; } @@ -1415,7 +1439,7 @@ class ItemEntity public string Name { get; set; } } - class ListParentExtension + class ListParentExtension { public ListExtension List { get; set; } } @@ -1588,6 +1612,9 @@ public OrganizationProfile() CreateMap() .ForMember(dest => dest.CreateDate, opts => opts.MapFrom(x => x.Date)); + CreateMap() + .ForMember(d => d.ValidLines, opt => opt.MapFrom(s => s.Lines.Where(l => l.Quantity > 0))); + CreateMap() .ForMember(dto => dto.Name, conf => conf.MapFrom(src => src.ItemName)); CreateMap() From 2e3c1f3477a1377cd68792000aedf7412a615d99 Mon Sep 17 00:00:00 2001 From: Blaise Taylor Date: Fri, 25 Nov 2022 09:18:40 -0500 Subject: [PATCH 08/18] Fixes Issue #152. EF.Property not mapped correctly. (#153) --- .../PrependParentNameVisitor.cs | 12 +++++ ...omChildReferenceWithoutMemberExpression.cs | 53 +++++++++++++++++++ ...MapParameterBodyWithoutMemberExpression.cs | 46 ++++++++++++++++ .../EF.cs | 12 +++++ 4 files changed, 123 insertions(+) create mode 100644 tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/CanMapParameterBodyFromChildReferenceWithoutMemberExpression.cs create mode 100644 tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/CanMapParameterBodyWithoutMemberExpression.cs create mode 100644 tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/EF.cs diff --git a/src/AutoMapper.Extensions.ExpressionMapping/PrependParentNameVisitor.cs b/src/AutoMapper.Extensions.ExpressionMapping/PrependParentNameVisitor.cs index ae40e80..7ffdfbc 100644 --- a/src/AutoMapper.Extensions.ExpressionMapping/PrependParentNameVisitor.cs +++ b/src/AutoMapper.Extensions.ExpressionMapping/PrependParentNameVisitor.cs @@ -18,6 +18,18 @@ public PrependParentNameVisitor(ParameterExpression currentParameter, string par public string ParentFullName { get; } public Expression NewParameter { get; } + protected override Expression VisitParameter(ParameterExpression node) + { + if (object.ReferenceEquals(CurrentParameter, node)) + { + return string.IsNullOrEmpty(ParentFullName) + ? NewParameter + : ExpressionHelpers.MemberAccesses(ParentFullName, NewParameter); + } + + return base.VisitParameter(node); + } + protected override Expression VisitTypeBinary(TypeBinaryExpression node) { if (!(node.Expression is ParameterExpression)) diff --git a/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/CanMapParameterBodyFromChildReferenceWithoutMemberExpression.cs b/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/CanMapParameterBodyFromChildReferenceWithoutMemberExpression.cs new file mode 100644 index 0000000..ecda1f7 --- /dev/null +++ b/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/CanMapParameterBodyFromChildReferenceWithoutMemberExpression.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using Xunit; + +namespace AutoMapper.Extensions.ExpressionMapping.UnitTests +{ + public class CanMapParameterBodyFromChildReferenceWithoutMemberExpression + { + [Fact] + public void Can_map_parameter_body_from_child_reference_without_member_expression() + { + // Arrange + var config = new MapperConfiguration(c => + { + c.CreateMap() + .ForMember(p => p.Brand, c => c.MapFrom(p => EF.Property(p, "BrandId"))); ; + + c.CreateMap() + .IncludeMembers(p => p.Category); + }); + + config.AssertConfigurationIsValid(); + var mapper = config.CreateMapper(); + + var products = new List() { + new TestProduct { } + }.AsQueryable(); + + //Act + Expression> expr = x => x.Brand == 2; + var mappedExpression = mapper.MapExpression>>(expr); + + //Assert + Assert.Equal("x => (Convert(Property(x.Category, \"BrandId\"), Int32) == 2)", mappedExpression.ToString()); + } + + public class TestCategory + { + // Has FK BrandId + } + public class TestProduct + { + public TestCategory? Category { get; set; } + } + + public class TestProductDTO + { + public int Brand { get; set; } + } + } +} diff --git a/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/CanMapParameterBodyWithoutMemberExpression.cs b/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/CanMapParameterBodyWithoutMemberExpression.cs new file mode 100644 index 0000000..e3c0f25 --- /dev/null +++ b/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/CanMapParameterBodyWithoutMemberExpression.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using Xunit; + +namespace AutoMapper.Extensions.ExpressionMapping.UnitTests +{ + public class CanMapParameterBodyWithoutMemberExpression + { + [Fact] + public void Can_map_parameter_body_without_member_expression() + { + // Arrange + var config = new MapperConfiguration(c => + { + c.CreateMap() + .ForMember(p => p.Brand, c => c.MapFrom(p => EF.Property(p, "BrandId"))); + }); + + config.AssertConfigurationIsValid(); + var mapper = config.CreateMapper(); + + var products = new List() { + new TestProduct { } + }.AsQueryable(); + + //Act + Expression> expr = x => x.Brand == 2; + var mappedExpression = mapper.MapExpression>>(expr); + + //Assert + Assert.Equal("x => (Convert(Property(x, \"BrandId\"), Int32) == 2)", mappedExpression.ToString()); + } + + public class TestProduct + { + // Empty, has shadow key named BrandId + } + + public class TestProductDTO + { + public int Brand { get; set; } + } + } +} diff --git a/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/EF.cs b/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/EF.cs new file mode 100644 index 0000000..8587ba3 --- /dev/null +++ b/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/EF.cs @@ -0,0 +1,12 @@ +using System; + +namespace AutoMapper.Extensions.ExpressionMapping.UnitTests +{ + internal class EF + { + internal static object Property(object p, string v) + { + throw new NotImplementedException(); + } + } +} From edb717fca357f832ae251f4e3cb8e55a9c111467 Mon Sep 17 00:00:00 2001 From: Blaise Taylor Date: Fri, 25 Nov 2022 15:07:28 -0500 Subject: [PATCH 09/18] Simpler to just visit the parameter. (#154) --- .../PrependParentNameVisitor.cs | 112 +----------------- ...omChildReferenceWithoutMemberExpression.cs | 2 +- ...MapParameterBodyWithoutMemberExpression.cs | 2 +- .../EF.cs | 8 +- 4 files changed, 6 insertions(+), 118 deletions(-) diff --git a/src/AutoMapper.Extensions.ExpressionMapping/PrependParentNameVisitor.cs b/src/AutoMapper.Extensions.ExpressionMapping/PrependParentNameVisitor.cs index 7ffdfbc..5397f18 100644 --- a/src/AutoMapper.Extensions.ExpressionMapping/PrependParentNameVisitor.cs +++ b/src/AutoMapper.Extensions.ExpressionMapping/PrependParentNameVisitor.cs @@ -1,7 +1,4 @@ -using AutoMapper.Extensions.ExpressionMapping.Extensions; -using System.Linq; -using System.Linq.Expressions; -using System.Runtime.CompilerServices; +using System.Linq.Expressions; namespace AutoMapper.Extensions.ExpressionMapping { @@ -29,112 +26,5 @@ protected override Expression VisitParameter(ParameterExpression node) return base.VisitParameter(node); } - - protected override Expression VisitTypeBinary(TypeBinaryExpression node) - { - if (!(node.Expression is ParameterExpression)) - return base.VisitTypeBinary(node); - - if (!object.ReferenceEquals(CurrentParameter, node.GetParameterExpression())) - return base.VisitTypeBinary(node); - - return Expression.TypeIs - ( - string.IsNullOrEmpty(ParentFullName) - ? NewParameter - : ExpressionHelpers.MemberAccesses(ParentFullName, NewParameter), - node.TypeOperand - ); - } - - protected override Expression VisitMember(MemberExpression node) - { - if (node.NodeType == ExpressionType.Constant) - return base.VisitMember(node); - - if (!object.ReferenceEquals(CurrentParameter, node.GetParameterExpression()) || !node.IsMemberExpression()) - return base.VisitMember(node); - - return ExpressionHelpers.MemberAccesses - ( - string.IsNullOrEmpty(ParentFullName) - ? node.GetPropertyFullName() - : $"{ParentFullName}.{node.GetPropertyFullName()}", - NewParameter - ); - } - - protected override Expression VisitMethodCall(MethodCallExpression node) - { - if (!IsParentParameterExpression()) - return base.VisitMethodCall(node); - - if (!object.ReferenceEquals(CurrentParameter, node.GetParameterExpression())) - return base.VisitMethodCall(node); - - if (node.Method.IsStatic) - { - if (!IsExtentionMethod()) - return base.VisitMethodCall(node); - - if (node.Method.IsGenericMethod) - return Expression.Call - ( - node.Method.DeclaringType, - node.Method.Name, - node.Method.GetGenericArguments(), - GetNewArgumentsForExtensionMethod() - ); - else - return Expression.Call(node.Method, GetNewArgumentsForExtensionMethod()); - } - - //instance method - if (node.Method.IsGenericMethod) - { - return Expression.Call - ( - GetNewParent(), - node.Method.Name, - node.Method.GetGenericArguments(), - node.Arguments.ToArray() - ); - } - else - { - return Expression.Call - ( - GetNewParent(), - node.Method, - node.Arguments - ); - } - - Expression[] GetNewArgumentsForExtensionMethod() - { - Expression[] arguments = node.Arguments.ToArray(); - arguments[0] = GetNewParent(); - return arguments.ToArray(); - } - - Expression GetNewParent() - => string.IsNullOrEmpty(ParentFullName) - ? NewParameter - : ExpressionHelpers.MemberAccesses(ParentFullName, NewParameter); - - bool IsParentParameterExpression() - { - if (node.Method.IsStatic) - return node.Arguments[0] is ParameterExpression; - - if (!node.Method.IsStatic) - return node.Object is ParameterExpression; - - return false; - } - - bool IsExtentionMethod() - => node.Method.IsDefined(typeof(ExtensionAttribute), true); - } } } diff --git a/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/CanMapParameterBodyFromChildReferenceWithoutMemberExpression.cs b/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/CanMapParameterBodyFromChildReferenceWithoutMemberExpression.cs index ecda1f7..0c26713 100644 --- a/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/CanMapParameterBodyFromChildReferenceWithoutMemberExpression.cs +++ b/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/CanMapParameterBodyFromChildReferenceWithoutMemberExpression.cs @@ -33,7 +33,7 @@ public void Can_map_parameter_body_from_child_reference_without_member_expressio var mappedExpression = mapper.MapExpression>>(expr); //Assert - Assert.Equal("x => (Convert(Property(x.Category, \"BrandId\"), Int32) == 2)", mappedExpression.ToString()); + Assert.Equal("x => (Property(x.Category, \"BrandId\") == 2)", mappedExpression.ToString()); } public class TestCategory diff --git a/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/CanMapParameterBodyWithoutMemberExpression.cs b/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/CanMapParameterBodyWithoutMemberExpression.cs index e3c0f25..f0e8db3 100644 --- a/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/CanMapParameterBodyWithoutMemberExpression.cs +++ b/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/CanMapParameterBodyWithoutMemberExpression.cs @@ -30,7 +30,7 @@ public void Can_map_parameter_body_without_member_expression() var mappedExpression = mapper.MapExpression>>(expr); //Assert - Assert.Equal("x => (Convert(Property(x, \"BrandId\"), Int32) == 2)", mappedExpression.ToString()); + Assert.Equal("x => (Property(x, \"BrandId\") == 2)", mappedExpression.ToString()); } public class TestProduct diff --git a/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/EF.cs b/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/EF.cs index 8587ba3..6836154 100644 --- a/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/EF.cs +++ b/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/EF.cs @@ -1,12 +1,10 @@ -using System; - -namespace AutoMapper.Extensions.ExpressionMapping.UnitTests +namespace AutoMapper.Extensions.ExpressionMapping.UnitTests { internal class EF { - internal static object Property(object p, string v) + internal static T Property(object p, string v) { - throw new NotImplementedException(); + return default; } } } From 6524c154d852bcee5017cf1664f45259ac7f9b46 Mon Sep 17 00:00:00 2001 From: Lucian Bargaoanu Date: Thu, 19 Jan 2023 13:16:17 +0200 Subject: [PATCH 10/18] Only dotnet test is needed (#157) * onmy dotnet test is needed * only dotnet test is needed --- .github/workflows/ci.yml | 8 +------- .github/workflows/release.yml | 8 +------- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aed9fb2..9fbc332 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,14 +19,8 @@ jobs: - name: Add AutoMapper Myget Source run: dotnet nuget add source https://www.myget.org/F/automapperdev/api/v3/index.json -n automappermyget - - name: Restore - run: dotnet restore - - - name: Build - run: dotnet build --configuration Release --no-restore - - name: Test - run: dotnet test --no-restore --verbosity normal + run: dotnet test --configuration Release --verbosity normal - name: Pack and push env: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1695ca2..52c0df8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,14 +14,8 @@ jobs: with: fetch-depth: 0 - - name: Restore - run: dotnet restore - - - name: Build - run: dotnet build --configuration Release --no-restore - - name: Test - run: dotnet test --no-restore --verbosity normal + run: dotnet test --configuration Release --verbosity normal - name: Pack and push env: From 2768e95d10d5b1ac770c70ae8cff13fe65d72bcc Mon Sep 17 00:00:00 2001 From: Blaise Taylor Date: Sun, 22 Jan 2023 08:02:25 -0500 Subject: [PATCH 11/18] Fixes issue #158 - incorrect arguments passed to Expression.Call. (#159) * Fixes issue #158 - incorrect arguments passed to Expression.Call. * Mapping array constant. --- .../MapperExtensions.cs | 25 ++- .../TypeMapHelper.cs | 46 +++++ .../XpressionMapperVisitor.cs | 7 +- .../CanMapExpressionWithListConstants.cs | 193 ++++++++++++++++++ 4 files changed, 264 insertions(+), 7 deletions(-) create mode 100644 tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/CanMapExpressionWithListConstants.cs diff --git a/src/AutoMapper.Extensions.ExpressionMapping/MapperExtensions.cs b/src/AutoMapper.Extensions.ExpressionMapping/MapperExtensions.cs index acf9c5a..72ba3d0 100644 --- a/src/AutoMapper.Extensions.ExpressionMapping/MapperExtensions.cs +++ b/src/AutoMapper.Extensions.ExpressionMapping/MapperExtensions.cs @@ -335,22 +335,39 @@ void AddTypeMaps(TypeMap typeMap) /// public static Type ReplaceType(this Dictionary typeMappings, Type sourceType) { - if (!sourceType.IsGenericType) + if (sourceType.IsArray) { - return typeMappings.TryGetValue(sourceType, out Type destType) ? destType : sourceType; + if (typeMappings.TryGetValue(sourceType, out Type destType)) + return destType; + + if (typeMappings.TryGetValue(sourceType.GetElementType(), out Type destElementType)) + { + int rank = sourceType.GetArrayRank(); + return rank == 1 + ? destElementType.MakeArrayType() + : destElementType.MakeArrayType(rank); + } + + return sourceType; } - else + else if (sourceType.IsGenericType) { if (typeMappings.TryGetValue(sourceType, out Type destType)) return destType; else + { return sourceType.GetGenericTypeDefinition().MakeGenericType ( sourceType .GetGenericArguments() - .Select(type => typeMappings.ReplaceType(type)) + .Select(typeMappings.ReplaceType) .ToArray() ); + } + } + else + { + return typeMappings.TryGetValue(sourceType, out Type destType) ? destType : sourceType; } } diff --git a/src/AutoMapper.Extensions.ExpressionMapping/TypeMapHelper.cs b/src/AutoMapper.Extensions.ExpressionMapping/TypeMapHelper.cs index d559096..65df684 100644 --- a/src/AutoMapper.Extensions.ExpressionMapping/TypeMapHelper.cs +++ b/src/AutoMapper.Extensions.ExpressionMapping/TypeMapHelper.cs @@ -6,6 +6,52 @@ namespace AutoMapper.Extensions.ExpressionMapping { internal static class TypeMapHelper { + public static bool CanMapConstant(this IConfigurationProvider config, Type sourceType, Type destType) + { + if (sourceType == destType) + return false; + + if (BothTypesAreDictionary()) + { + Type[] sourceGenericTypes = sourceType.GetGenericArguments(); + Type[] destGenericTypes = destType.GetGenericArguments(); + if (sourceGenericTypes.SequenceEqual(destGenericTypes)) + return false; + else if (sourceGenericTypes[0] == destGenericTypes[0]) + return config.CanMapConstant(sourceGenericTypes[1], destGenericTypes[1]); + else if (sourceGenericTypes[1] == destGenericTypes[1]) + return config.CanMapConstant(sourceGenericTypes[0], destGenericTypes[0]); + else + return config.CanMapConstant(sourceGenericTypes[0], destGenericTypes[0]) && config.CanMapConstant(sourceGenericTypes[1], destGenericTypes[1]); + } + else if (sourceType.IsArray && destType.IsArray) + return config.CanMapConstant(sourceType.GetElementType(), destType.GetElementType()); + else if (BothTypesAreEnumerable()) + return config.CanMapConstant(sourceType.GetGenericArguments()[0], destType.GetGenericArguments()[0]); + else + return config.Internal().ResolveTypeMap(sourceType, destType) != null; + + bool BothTypesAreEnumerable() + { + Type enumerableType = typeof(System.Collections.IEnumerable); + return sourceType.IsGenericType + && destType.IsGenericType + && enumerableType.IsAssignableFrom(sourceType) + && enumerableType.IsAssignableFrom(destType); + } + + bool BothTypesAreDictionary() + { + Type dictionaryType = typeof(System.Collections.IDictionary); + return sourceType.IsGenericType + && destType.IsGenericType + && dictionaryType.IsAssignableFrom(sourceType) + && dictionaryType.IsAssignableFrom(destType) + && sourceType.GetGenericArguments().Length == 2 + && destType.GetGenericArguments().Length == 2; + } + } + public static MemberMap GetMemberMapByDestinationProperty(this TypeMap typeMap, string destinationPropertyName) { var propertyMap = typeMap.PropertyMaps.SingleOrDefault(item => item.DestinationName == destinationPropertyName); diff --git a/src/AutoMapper.Extensions.ExpressionMapping/XpressionMapperVisitor.cs b/src/AutoMapper.Extensions.ExpressionMapping/XpressionMapperVisitor.cs index ecf33ef..ff354d7 100644 --- a/src/AutoMapper.Extensions.ExpressionMapping/XpressionMapperVisitor.cs +++ b/src/AutoMapper.Extensions.ExpressionMapping/XpressionMapperVisitor.cs @@ -511,12 +511,13 @@ Expression DoVisitUnary(Expression updated) protected override Expression VisitConstant(ConstantExpression node) { - if (this.TypeMappings.TryGetValue(node.Type, out Type newType)) + Type newType = this.TypeMappings.ReplaceType(node.Type); + if (newType != node.Type) { if (node.Value == null) return base.VisitConstant(Expression.Constant(null, newType)); - if (ConfigurationProvider.Internal().ResolveTypeMap(node.Type, newType) != null) + if (ConfigurationProvider.CanMapConstant(node.Type, newType)) return base.VisitConstant(Expression.Constant(Mapper.MapObject(node.Value, node.Type, newType), newType)); //Issue 3455 (Non-Generic Mapper.Map failing for structs in v10) //return base.VisitConstant(Expression.Constant(Mapper.Map(node.Value, node.Type, newType), newType)); @@ -553,7 +554,7 @@ protected override Expression VisitMethodCall(MethodCallExpression node) MethodCallExpression GetInstanceExpression(Expression instance) => node.Method.IsGenericMethod ? Expression.Call(instance, node.Method.Name, typeArgsForNewMethod.ToArray(), listOfArgumentsForNewMethod.ToArray()) - : Expression.Call(instance, node.Method, listOfArgumentsForNewMethod.ToArray()); + : Expression.Call(instance, instance.Type.GetMethod(node.Method.Name, listOfArgumentsForNewMethod.Select(a => a.Type).ToArray()), listOfArgumentsForNewMethod.ToArray()); MethodCallExpression GetStaticExpression() => node.Method.IsGenericMethod diff --git a/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/CanMapExpressionWithListConstants.cs b/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/CanMapExpressionWithListConstants.cs new file mode 100644 index 0000000..8573476 --- /dev/null +++ b/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/CanMapExpressionWithListConstants.cs @@ -0,0 +1,193 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using Xunit; + +namespace AutoMapper.Extensions.ExpressionMapping.UnitTests +{ + public class CanMapExpressionWithListConstants + { + [Fact] + public void Map_expression_with_constant_array() + { + //Arrange + var config = new MapperConfiguration + ( + cfg => + { + cfg.CreateMap(); + cfg.CreateMap(); + } + ); + config.AssertConfigurationIsValid(); + var mapper = config.CreateMapper(); + List source1 = new() { + new EntityModel { SimpleEnum = SimpleEnumModel.Value3 } + }; + List source2 = new() { + new EntityModel { SimpleEnum = SimpleEnumModel.Value1 } + }; + Entity[] entities = new Entity[] { new Entity { SimpleEnum = SimpleEnum.Value1 }, new Entity { SimpleEnum = SimpleEnum.Value2 } }; + Expression> filter = e => entities.Any(en => e.SimpleEnum == en.SimpleEnum); + + //act + Expression> mappedFilter = mapper.MapExpression>>(filter); + + //assert + Assert.False(source1.AsQueryable().Any(mappedFilter)); + Assert.True(source2.AsQueryable().Any(mappedFilter)); + } + + [Fact] + public void Map_expression_with_constant_list_using_generic_list_dot_contains() + { + //Arrange + var config = new MapperConfiguration + ( + cfg => + { + cfg.CreateMap(); + cfg.CreateMap(); + } + ); + config.AssertConfigurationIsValid(); + var mapper = config.CreateMapper(); + List source1 = new() { + new EntityModel { SimpleEnum = SimpleEnumModel.Value3 } + }; + List source2 = new() { + new EntityModel { SimpleEnum = SimpleEnumModel.Value1 } + }; + List enums = new() { SimpleEnum.Value1, SimpleEnum.Value2 }; + Expression> filter = e => enums.Contains(e.SimpleEnum); + + //act + Expression> mappedFilter = mapper.MapExpression>>(filter); + + //assert + Assert.False(source1.AsQueryable().Any(mappedFilter)); + Assert.True(source2.AsQueryable().Any(mappedFilter)); + } + + [Fact] + public void Map_expression_with_constant_list_using_generic_enumerable_dot_contains() + { + //Arrange + var config = new MapperConfiguration + ( + cfg => + { + cfg.CreateMap(); + cfg.CreateMap(); + } + ); + config.AssertConfigurationIsValid(); + var mapper = config.CreateMapper(); + List source1 = new() { + new EntityModel { SimpleEnum = SimpleEnumModel.Value3 } + }; + List source2 = new() { + new EntityModel { SimpleEnum = SimpleEnumModel.Value1 } + }; + List enums = new() { SimpleEnum.Value1, SimpleEnum.Value2 }; + Expression> filter = e => Enumerable.Contains(enums, e.SimpleEnum); + + //act + Expression> mappedFilter = mapper.MapExpression>>(filter); + + //assert + Assert.False(source1.AsQueryable().Any(mappedFilter)); + Assert.True(source2.AsQueryable().Any(mappedFilter)); + } + + [Fact] + public void Map_expression_with_constant_dictionary() + { + //Arrange + var config = new MapperConfiguration + ( + cfg => + { + cfg.CreateMap(); + cfg.CreateMap(); + } + ); + config.AssertConfigurationIsValid(); + var mapper = config.CreateMapper(); + List source1 = new() { + new EntityModel { SimpleEnum = SimpleEnumModel.Value3 } + }; + List source2 = new() { + new EntityModel { SimpleEnum = SimpleEnumModel.Value1 } + }; + Dictionary enumDictionary = new() { ["A"] = SimpleEnum.Value1, ["B"] = SimpleEnum.Value2 }; + Expression> filter = e => enumDictionary.Any(i => i.Value == e.SimpleEnum); + + //act + Expression> mappedFilter = mapper.MapExpression>>(filter); + + //assert + Assert.False(source1.AsQueryable().Any(mappedFilter)); + Assert.True(source2.AsQueryable().Any(mappedFilter)); + } + + [Fact] + public void Map_expression_with_constant_dictionary_mapping_both_Key_and_value() + { + //Arrange + var config = new MapperConfiguration + ( + cfg => + { + cfg.CreateMap(); + cfg.CreateMap(); + cfg.CreateMap(); + } + ); + config.AssertConfigurationIsValid(); + var mapper = config.CreateMapper(); + List source1 = new() { + new EntityModel { SimpleEnum = SimpleEnumModel.Value3 } + }; + List source2 = new() { + new EntityModel { SimpleEnum = SimpleEnumModel.Value1 } + }; + Dictionary enumDictionary = new() { [SimpleEnum.Value1] = new Entity { SimpleEnum = SimpleEnum.Value1 }, [SimpleEnum.Value2] = new Entity { SimpleEnum = SimpleEnum.Value2 } }; + Expression> filter = e => enumDictionary.Any(i => i.Key == e.SimpleEnum && i.Value.SimpleEnum == e.SimpleEnum); + + //act + Expression> mappedFilter = mapper.MapExpression>>(filter); + + //assert + Assert.False(source1.AsQueryable().Any(mappedFilter)); + Assert.True(source2.AsQueryable().Any(mappedFilter)); + } + + public enum SimpleEnum + { + Value1, + Value2, + Value3 + } + + public record Entity + { + public int Id { get; init; } + public SimpleEnum SimpleEnum { get; init; } + } + + public enum SimpleEnumModel + { + Value1, + Value2, + Value3 + } + + public record EntityModel + { + public int Id { get; init; } + public SimpleEnumModel SimpleEnum { get; init; } + } + } +} From c5c1a1df65ccfdbdb0c9922a78da85103533420c Mon Sep 17 00:00:00 2001 From: Blaise Taylor Date: Wed, 25 Jan 2023 06:22:28 -0500 Subject: [PATCH 12/18] Enums do not require type maps. (#160) --- src/AutoMapper.Extensions.ExpressionMapping/TypeMapHelper.cs | 5 +++++ .../CanMapExpressionWithListConstants.cs | 4 ---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/AutoMapper.Extensions.ExpressionMapping/TypeMapHelper.cs b/src/AutoMapper.Extensions.ExpressionMapping/TypeMapHelper.cs index 65df684..de99e10 100644 --- a/src/AutoMapper.Extensions.ExpressionMapping/TypeMapHelper.cs +++ b/src/AutoMapper.Extensions.ExpressionMapping/TypeMapHelper.cs @@ -28,9 +28,14 @@ public static bool CanMapConstant(this IConfigurationProvider config, Type sourc return config.CanMapConstant(sourceType.GetElementType(), destType.GetElementType()); else if (BothTypesAreEnumerable()) return config.CanMapConstant(sourceType.GetGenericArguments()[0], destType.GetGenericArguments()[0]); + else if (BothTypesAreEnums()) + return true; else return config.Internal().ResolveTypeMap(sourceType, destType) != null; + bool BothTypesAreEnums() + => sourceType.IsEnum && destType.IsEnum; + bool BothTypesAreEnumerable() { Type enumerableType = typeof(System.Collections.IEnumerable); diff --git a/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/CanMapExpressionWithListConstants.cs b/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/CanMapExpressionWithListConstants.cs index 8573476..c1ab754 100644 --- a/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/CanMapExpressionWithListConstants.cs +++ b/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/CanMapExpressionWithListConstants.cs @@ -48,7 +48,6 @@ public void Map_expression_with_constant_list_using_generic_list_dot_contains() cfg => { cfg.CreateMap(); - cfg.CreateMap(); } ); config.AssertConfigurationIsValid(); @@ -79,7 +78,6 @@ public void Map_expression_with_constant_list_using_generic_enumerable_dot_conta cfg => { cfg.CreateMap(); - cfg.CreateMap(); } ); config.AssertConfigurationIsValid(); @@ -110,7 +108,6 @@ public void Map_expression_with_constant_dictionary() cfg => { cfg.CreateMap(); - cfg.CreateMap(); } ); config.AssertConfigurationIsValid(); @@ -142,7 +139,6 @@ public void Map_expression_with_constant_dictionary_mapping_both_Key_and_value() { cfg.CreateMap(); cfg.CreateMap(); - cfg.CreateMap(); } ); config.AssertConfigurationIsValid(); From 826680034397364df9b37d5b50ad8220834a41b2 Mon Sep 17 00:00:00 2001 From: ErikGjers <124898951+ErikGjers@users.noreply.github.com> Date: Mon, 13 Mar 2023 20:56:25 +0100 Subject: [PATCH 13/18] Added enum to int conversion for unmapped side of binary expressions (#163) * Added enum to int conversion for unmapped side of binary expressions * Enum to int conversion logic from VisitBinary to VisitConstant. * new test start * Made tests using Theory * Logic already in VisitMember solves the same problem - should be safer to reuse. --------- Co-authored-by: Erik Gjers Co-authored-by: Erik Gjers Co-authored-by: Blaise Taylor --- .../Extensions/VisitorExtensions.cs | 9 + .../XpressionMapperVisitor.cs | 21 +- .../ExpressionMappingEnumToNumericOrString.cs | 269 ++++++++++++++++++ 3 files changed, 297 insertions(+), 2 deletions(-) create mode 100644 tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/ExpressionMappingEnumToNumericOrString.cs diff --git a/src/AutoMapper.Extensions.ExpressionMapping/Extensions/VisitorExtensions.cs b/src/AutoMapper.Extensions.ExpressionMapping/Extensions/VisitorExtensions.cs index decffed..f876a97 100644 --- a/src/AutoMapper.Extensions.ExpressionMapping/Extensions/VisitorExtensions.cs +++ b/src/AutoMapper.Extensions.ExpressionMapping/Extensions/VisitorExtensions.cs @@ -5,6 +5,7 @@ using System.Reflection; using System.Runtime.CompilerServices; using AutoMapper.Extensions.ExpressionMapping.Structures; +using AutoMapper.Internal; namespace AutoMapper.Extensions.ExpressionMapping.Extensions { @@ -202,5 +203,13 @@ public static List GetUnderlyingGenericTypes(this Type type) => type == null || !type.GetTypeInfo().IsGenericType ? new List() : type.GetGenericArguments().ToList(); + + public static bool IsEnumType(this Type type) + { + if (type.IsNullableType()) + type = Nullable.GetUnderlyingType(type); + + return type.IsEnum(); + } } } diff --git a/src/AutoMapper.Extensions.ExpressionMapping/XpressionMapperVisitor.cs b/src/AutoMapper.Extensions.ExpressionMapping/XpressionMapperVisitor.cs index ff354d7..1f9fa78 100644 --- a/src/AutoMapper.Extensions.ExpressionMapping/XpressionMapperVisitor.cs +++ b/src/AutoMapper.Extensions.ExpressionMapping/XpressionMapperVisitor.cs @@ -95,7 +95,7 @@ Expression GetMappedMemberExpression(Expression parentExpression, List propertyMapInfoList, PropertyMapInfo lastWithCustExpression, Expression mappedParentExpr) => GetMemberExpressionFromCustomExpression ( diff --git a/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/ExpressionMappingEnumToNumericOrString.cs b/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/ExpressionMappingEnumToNumericOrString.cs new file mode 100644 index 0000000..d0200f1 --- /dev/null +++ b/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/ExpressionMappingEnumToNumericOrString.cs @@ -0,0 +1,269 @@ + +using System; +using System.Linq.Expressions; +using Xunit; + +namespace AutoMapper.Extensions.ExpressionMapping.UnitTests +{ + public class ExpressionMappingEnumToNumericOrString : AutoMapperSpecBase + { + public enum SimpleEnumByte : byte + { + Value1 = 1, + Value2 = 2, + Value3 = 3 + } + public enum SimpleEnumSByte : sbyte + { + Value1 = 1, + Value2 = 2, + Value3 = 3 + } + public enum SimpleEnumShort : short + { + Value1 = 1, + Value2 = 2, + Value3 = 3 + } + public enum SimpleEnumUShort : ushort + { + Value1 = 1, + Value2 = 2, + Value3 = 3 + } + public enum SimpleEnumInt : int + { + Value1 = 1, + Value2 = 2, + Value3 = 3 + } + public enum SimpleEnumUInt : uint + { + Value1 = 1, + Value2 = 2, + Value3 = 3 + } + public enum SimpleEnumLong : long + { + Value1 = 1, + Value2 = 2, + Value3 = long.MaxValue + } + public enum SimpleEnumULong : ulong + { + Value1 = 1, + Value2 = 2, + Value3 = long.MaxValue + } + private class EntityDto + where TEnum : Enum + { + internal TEnum Value { get; set; } + } + private class Entity + { + internal T Value { get; set; } + } + + protected override MapperConfiguration Configuration + { + get + { + return new MapperConfiguration(config => + { + config.AddExpressionMapping(); + config.CreateMap, EntityDto>() + .ForMember(dest => dest.Value, config => config.MapFrom(src => src.Value)) + .ReverseMap(); + + config.CreateMap, EntityDto>() + .ForMember(dest => dest.Value, config => config.MapFrom(src => src.Value)) + .ReverseMap(); + config.CreateMap, EntityDto>() + .ForMember(dest => dest.Value, config => config.MapFrom(src => src.Value)) + .ReverseMap(); + config.CreateMap, EntityDto>() + .ForMember(dest => dest.Value, config => config.MapFrom(src => src.Value)) + .ReverseMap(); + config.CreateMap, EntityDto>() + .ForMember(dest => dest.Value, config => config.MapFrom(src => src.Value)) + .ReverseMap(); + config.CreateMap, EntityDto>() + .ForMember(dest => dest.Value, config => config.MapFrom(src => src.Value)) + .ReverseMap(); + config.CreateMap, EntityDto>() + .ForMember(dest => dest.Value, config => config.MapFrom(src => src.Value)) + .ReverseMap(); + config.CreateMap, EntityDto>() + .ForMember(dest => dest.Value, config => config.MapFrom(src => src.Value)) + .ReverseMap(); + config.CreateMap, EntityDto>() + .ForMember(dest => dest.Value, config => config.MapFrom(src => src.Value)) + .ReverseMap(); + config.CreateMap, EntityDto>() + .ForMember(dest => dest.Value, config => config.MapFrom(src => src.Value)) + .ReverseMap(); + config.CreateMap, EntityDto>() + .ForMember(dest => dest.Value, config => config.MapFrom(src => src.Value)) + .ReverseMap(); + config.CreateMap, EntityDto>() + .ForMember(dest => dest.Value, config => config.MapFrom(src => src.Value)) + .ReverseMap(); + config.CreateMap, EntityDto>() + .ForMember(dest => dest.Value, config => config.MapFrom(src => src.Value)) + .ReverseMap(); + config.CreateMap, EntityDto>() + .ForMember(dest => dest.Value, config => config.MapFrom(src => src.Value)) + .ReverseMap(); + config.CreateMap, EntityDto>() + .ForMember(dest => dest.Value, config => config.MapFrom(src => src.Value)) + .ReverseMap(); + config.CreateMap, EntityDto>() + .ForMember(dest => dest.Value, config => config.MapFrom(src => src.Value)) + .ReverseMap(); + + config.CreateMap().ConvertUsing(e => e.ToString()); + config.CreateMap().ConvertUsing(e => e.ToString()); + config.CreateMap().ConvertUsing(e => e.ToString()); + config.CreateMap().ConvertUsing(e => e.ToString()); + config.CreateMap().ConvertUsing(e => e.ToString()); + config.CreateMap().ConvertUsing(e => e.ToString()); + config.CreateMap().ConvertUsing(e => e.ToString()); + config.CreateMap().ConvertUsing(e => e.ToString()); + + config.CreateMap() +.ForMember(dest => dest.intToEnum, config => config.MapFrom(src => src.intToEnum)) +.ForMember(dest => dest.enumToEnum, config => config.MapFrom(src => src.enumToEnum)) +.ForMember(dest => dest.enumToInt, config => config.MapFrom(src => src.enumToInt)) +.ForMember(dest => dest.intToInt, config => config.MapFrom(src => src.intToInt)) +.ReverseMap(); + }); + } + } + + [Theory] + [InlineData(SimpleEnumByte.Value2, (byte)2)] + [InlineData(SimpleEnumSByte.Value2, (sbyte)2)] + [InlineData(SimpleEnumShort.Value2, (short)2)] + [InlineData(SimpleEnumUShort.Value2, (ushort)2)] + [InlineData(SimpleEnumInt.Value2, 2)] + [InlineData(SimpleEnumUInt.Value2, 2U)] + [InlineData(SimpleEnumLong.Value2, 2L)] + [InlineData(SimpleEnumULong.Value2, 2UL)] + [InlineData(SimpleEnumSByte.Value2, (sbyte)1)] + [InlineData(SimpleEnumByte.Value2, (byte)1)] + [InlineData(SimpleEnumShort.Value2, (short)1)] + [InlineData(SimpleEnumUShort.Value2, (ushort)1)] + [InlineData(SimpleEnumInt.Value2, 1)] + [InlineData(SimpleEnumUInt.Value2, 1U)] + [InlineData(SimpleEnumLong.Value2, 1L)] + [InlineData(SimpleEnumULong.Value2, 1UL)] + [InlineData(SimpleEnumSByte.Value3, (sbyte)3)] + [InlineData(SimpleEnumByte.Value3, (byte)1)] + [InlineData(SimpleEnumShort.Value3, (short)1)] + [InlineData(SimpleEnumUShort.Value3, (ushort)1)] + [InlineData(SimpleEnumInt.Value3, 1)] + [InlineData(SimpleEnumUInt.Value3, 1U)] + [InlineData(SimpleEnumLong.Value3, 1L)] + [InlineData(SimpleEnumULong.Value3, 1UL)] + [InlineData(SimpleEnumSByte.Value3, (sbyte)3)] + [InlineData(SimpleEnumByte.Value3, (byte)3)] + [InlineData(SimpleEnumShort.Value3, (short)3)] + [InlineData(SimpleEnumUShort.Value3, (ushort)3)] + [InlineData(SimpleEnumInt.Value3, 3)] + [InlineData(SimpleEnumUInt.Value3, 3U)] + [InlineData(SimpleEnumLong.Value3, 3L)] + [InlineData(SimpleEnumULong.Value3, 3UL)] + [InlineData(SimpleEnumLong.Value3, long.MaxValue)] + [InlineData(SimpleEnumULong.Value3, (ulong)long.MaxValue)] + public void BinaryExpressionEquals(TEnum enumConstant, TNumeric numericConstant) + where TEnum : Enum + { + var correctResult = ((TNumeric)(object)enumConstant).Equals(numericConstant); + Expression, bool>> mappedExpression; + { + var param = Expression.Parameter(typeof(EntityDto), "x"); + var property = Expression.Property(param, nameof(EntityDto.Value)); + var constantExp = Expression.Constant(enumConstant, typeof(TEnum)); + var binaryExpression = Expression.Equal(property, constantExp); + var lambdaExpression = Expression.Lambda(binaryExpression, param); + mappedExpression = Mapper.Map, bool>>>(lambdaExpression); + } + + var mappedExpressionDelegate = mappedExpression.Compile(); + + var entity = new Entity { Value = numericConstant }; + var result = mappedExpressionDelegate(entity); + + Assert.Equal(result, correctResult); + } + + private class ComplexEntity + { + public int intToEnum { get; set; } + public SimpleEnumInt enumToInt { get; set; } + public SimpleEnumInt enumToEnum { get; set; } + public int intToInt { get; set; } + } + + private class ComplexEntityDto + { + public SimpleEnumInt intToEnum { get; set; } + public int enumToInt { get; set; } + + public SimpleEnumInt enumToEnum { get; set; } + public int intToInt { get; set; } + } + + [Fact] + public void BinaryExpressionPartialTranslation() + { + Expression> mappedExpression; + { + var param = Expression.Parameter(typeof(ComplexEntity), "x"); + var property1 = Expression.Property(param, nameof(ComplexEntity.intToEnum)); + var property2 = Expression.Property(param, nameof(ComplexEntity.intToInt)); + var property5 = Expression.Property(param, nameof(ComplexEntity.enumToEnum)); + var property6 = Expression.Property(param, nameof(ComplexEntity.enumToInt)); + + var constant1 = Expression.Constant(2, typeof(int)); + var constant2 = Expression.Constant(1, typeof(int)); + var constant5 = Expression.Constant(SimpleEnumInt.Value3, typeof(SimpleEnumInt)); + var constant6 = Expression.Constant(SimpleEnumInt.Value2, typeof(SimpleEnumInt)); + + Expression[] equals = new Expression[]{ + Expression.Equal(property1, constant1), + Expression.Equal(property2, constant2), + Expression.Equal(property5, constant5), + Expression.Equal(property6, constant6), + }; + + Expression andExpression = equals[0]; + for (int i = 1; i < equals.Length; i++) + { + andExpression = Expression.And(andExpression, equals[i]); + } + var lambdaExpression = Expression.Lambda(andExpression, param); + mappedExpression = Mapper.Map>>(lambdaExpression); + } + + Expression> translatedExpression = + translatedExpression = x => + x.intToEnum == SimpleEnumInt.Value2 + && x.intToInt == (int)SimpleEnumInt.Value1 + && x.enumToEnum == SimpleEnumInt.Value3 + && x.enumToInt == (int)SimpleEnumInt.Value2 + ; + + var mappedExpressionDelegate = mappedExpression.Compile(); + var translatedExpressionDelegate = translatedExpression.Compile(); + + var entity = new ComplexEntityDto { intToEnum = SimpleEnumInt.Value2, intToInt = 1, enumToEnum = SimpleEnumInt.Value3, enumToInt = 2 }; + var mappedResult = mappedExpressionDelegate(entity); + var translatedResult = translatedExpressionDelegate(entity); + + Assert.True(translatedResult); + Assert.Equal(mappedResult, translatedResult); + } + } +} From 18c37f10da7e47908ba363c680e5786a3deee5ac Mon Sep 17 00:00:00 2001 From: Blaise Taylor Date: Mon, 20 Mar 2023 10:12:46 -0400 Subject: [PATCH 14/18] Error in #161 fix. (#167) * Error in #161 fix. * Failing test --- .../XpressionMapperVisitor.cs | 2 +- .../ExpressionMappingEnumToNumericOrString.cs | 26 +++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/AutoMapper.Extensions.ExpressionMapping/XpressionMapperVisitor.cs b/src/AutoMapper.Extensions.ExpressionMapping/XpressionMapperVisitor.cs index 1f9fa78..88ad595 100644 --- a/src/AutoMapper.Extensions.ExpressionMapping/XpressionMapperVisitor.cs +++ b/src/AutoMapper.Extensions.ExpressionMapping/XpressionMapperVisitor.cs @@ -152,7 +152,7 @@ private bool ShouldConvertMemberExpression(Type initialType, Type mappedType) initialType = Nullable.GetUnderlyingType(initialType); if (mappedType.IsNullableType()) - initialType = Nullable.GetUnderlyingType(mappedType); + mappedType = Nullable.GetUnderlyingType(mappedType); return mappedType == Enum.GetUnderlyingType(initialType); } diff --git a/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/ExpressionMappingEnumToNumericOrString.cs b/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/ExpressionMappingEnumToNumericOrString.cs index d0200f1..bda7b52 100644 --- a/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/ExpressionMappingEnumToNumericOrString.cs +++ b/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/ExpressionMappingEnumToNumericOrString.cs @@ -87,6 +87,9 @@ protected override MapperConfiguration Configuration .ReverseMap(); config.CreateMap, EntityDto>() .ForMember(dest => dest.Value, config => config.MapFrom(src => src.Value)) + .ReverseMap(); + config.CreateMap, EntityDto>() + .ForMember(dest => dest.Value, config => config.MapFrom(src => src.Value)) .ReverseMap(); config.CreateMap, EntityDto>() .ForMember(dest => dest.Value, config => config.MapFrom(src => src.Value)) @@ -198,6 +201,29 @@ public void BinaryExpressionEquals(TEnum enumConstant, TNumeric Assert.Equal(result, correctResult); } + [Fact] + public void BinaryExpressionEqualsWithNullable() + { + SimpleEnumInt enumConstant = SimpleEnumInt.Value2; + int? numericConstant = 2; + Expression, bool>> mappedExpression; + { + var param = Expression.Parameter(typeof(EntityDto), "x"); + var property = Expression.Property(param, nameof(EntityDto.Value)); + var constantExp = Expression.Constant(enumConstant, typeof(SimpleEnumInt)); + var binaryExpression = Expression.Equal(property, constantExp); + var lambdaExpression = Expression.Lambda(binaryExpression, param); + mappedExpression = Mapper.Map, bool>>>(lambdaExpression); + } + + var mappedExpressionDelegate = mappedExpression.Compile(); + + var entity = new Entity { Value = numericConstant }; + var result = mappedExpressionDelegate(entity); + + Assert.True(result); + } + private class ComplexEntity { public int intToEnum { get; set; } From fd47b8413a37c76e9bc35bc28f920ca1596421ed Mon Sep 17 00:00:00 2001 From: Blaise Taylor Date: Sun, 2 Apr 2023 07:32:32 -0400 Subject: [PATCH 15/18] Replacing PackageLicenseUrl with PackageLicenseExpression. (#168) --- .../AutoMapper.Extensions.ExpressionMapping.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AutoMapper.Extensions.ExpressionMapping/AutoMapper.Extensions.ExpressionMapping.csproj b/src/AutoMapper.Extensions.ExpressionMapping/AutoMapper.Extensions.ExpressionMapping.csproj index 933d7d6..dcb9b1c 100644 --- a/src/AutoMapper.Extensions.ExpressionMapping/AutoMapper.Extensions.ExpressionMapping.csproj +++ b/src/AutoMapper.Extensions.ExpressionMapping/AutoMapper.Extensions.ExpressionMapping.csproj @@ -11,7 +11,7 @@ AutoMapper.Extensions.ExpressionMapping icon.png http://automapper.org - https://github.com/AutoMapper/AutoMapper.Extensions.ExpressionMapping/blob/master/LICENSE + MIT git https://github.com/AutoMapper/AutoMapper.Extensions.ExpressionMapping v From 69e5e93fbf51b8a12e4e71d4d175455e970b5bcc Mon Sep 17 00:00:00 2001 From: Lionel Vallet <67357826+LionelVallet@users.noreply.github.com> Date: Wed, 7 Feb 2024 12:00:31 +0100 Subject: [PATCH 16/18] Targeting .NET 6 and AutoMapper v13. (#177) Co-authored-by: Lionel Vallet <12908586-LionelVallet@users.noreply.gitlab.com> --- .../AutoMapper.Extensions.ExpressionMapping.csproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/AutoMapper.Extensions.ExpressionMapping/AutoMapper.Extensions.ExpressionMapping.csproj b/src/AutoMapper.Extensions.ExpressionMapping/AutoMapper.Extensions.ExpressionMapping.csproj index dcb9b1c..0bbfaa3 100644 --- a/src/AutoMapper.Extensions.ExpressionMapping/AutoMapper.Extensions.ExpressionMapping.csproj +++ b/src/AutoMapper.Extensions.ExpressionMapping/AutoMapper.Extensions.ExpressionMapping.csproj @@ -3,14 +3,14 @@ Expression mapping (OData) extensions for AutoMapper Expression mapping (OData) extensions for AutoMapper - netstandard2.1 + net6.0 true ..\..\AutoMapper.snk true true AutoMapper.Extensions.ExpressionMapping icon.png - http://automapper.org + https://automapper.org MIT git https://github.com/AutoMapper/AutoMapper.Extensions.ExpressionMapping @@ -28,7 +28,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive From 9f77f08a63e6bf28efa41a6d12fd0375b0ee3a95 Mon Sep 17 00:00:00 2001 From: Blaise Taylor Date: Mon, 13 May 2024 05:52:23 -0400 Subject: [PATCH 17/18] Always create instance methods using the declaring type. Fixes Issue #179. (#180) --- .../XpressionMapperVisitor.cs | 16 +++- ...dUseDeclaringTypeForInstanceMethodCalls.cs | 83 +++++++++++++++++++ 2 files changed, 96 insertions(+), 3 deletions(-) create mode 100644 tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/ShouldUseDeclaringTypeForInstanceMethodCalls.cs diff --git a/src/AutoMapper.Extensions.ExpressionMapping/XpressionMapperVisitor.cs b/src/AutoMapper.Extensions.ExpressionMapping/XpressionMapperVisitor.cs index 88ad595..0a92b62 100644 --- a/src/AutoMapper.Extensions.ExpressionMapping/XpressionMapperVisitor.cs +++ b/src/AutoMapper.Extensions.ExpressionMapping/XpressionMapperVisitor.cs @@ -569,9 +569,19 @@ protected override Expression VisitMethodCall(MethodCallExpression node) : GetInstanceExpression(this.Visit(node.Object)); MethodCallExpression GetInstanceExpression(Expression instance) - => node.Method.IsGenericMethod - ? Expression.Call(instance, node.Method.Name, typeArgsForNewMethod.ToArray(), listOfArgumentsForNewMethod.ToArray()) - : Expression.Call(instance, instance.Type.GetMethod(node.Method.Name, listOfArgumentsForNewMethod.Select(a => a.Type).ToArray()), listOfArgumentsForNewMethod.ToArray()); + { + return node.Method.IsGenericMethod + ? Expression.Call(instance, node.Method.Name, typeArgsForNewMethod.ToArray(), listOfArgumentsForNewMethod.ToArray()) + : Expression.Call(instance, GetMethodInfoForNonGeneric(), listOfArgumentsForNewMethod.ToArray()); + + MethodInfo GetMethodInfoForNonGeneric() + { + MethodInfo methodInfo = instance.Type.GetMethod(node.Method.Name, listOfArgumentsForNewMethod.Select(a => a.Type).ToArray()); + if (methodInfo.DeclaringType != instance.Type) + methodInfo = methodInfo.DeclaringType.GetMethod(node.Method.Name, listOfArgumentsForNewMethod.Select(a => a.Type).ToArray()); + return methodInfo; + } + } MethodCallExpression GetStaticExpression() => node.Method.IsGenericMethod diff --git a/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/ShouldUseDeclaringTypeForInstanceMethodCalls.cs b/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/ShouldUseDeclaringTypeForInstanceMethodCalls.cs new file mode 100644 index 0000000..6d6ecaf --- /dev/null +++ b/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/ShouldUseDeclaringTypeForInstanceMethodCalls.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using Xunit; + +namespace AutoMapper.Extensions.ExpressionMapping.UnitTests +{ + public class ShouldUseDeclaringTypeForInstanceMethodCalls + { + [Fact] + public void MethodInfoShouldRetainDeclaringTypeInMappedExpression() + { + //Arrange + var config = new MapperConfiguration + ( + cfg => + { + cfg.CreateMap(); + cfg.CreateMap(); + } + ); + config.AssertConfigurationIsValid(); + var mapper = config.CreateMapper(); + Expression> filter = e => e.SimpleEnum.HasFlag(SimpleEnum.Value3); + EntityModel entityModel1 = new() { SimpleEnum = SimpleEnumModel.Value3 }; + EntityModel entityModel2 = new() { SimpleEnum = SimpleEnumModel.Value2 }; + + //act + Expression> mappedFilter = mapper.MapExpression>>(filter); + + //assert + Assert.Equal(typeof(Enum), HasFlagVisitor.GetasFlagReflectedType(mappedFilter)); + Assert.Single(new List { entityModel1 }.AsQueryable().Where(mappedFilter)); + Assert.Empty(new List { entityModel2 }.AsQueryable().Where(mappedFilter)); + } + + public enum SimpleEnum + { + Value1, + Value2, + Value3 + } + + public record Entity + { + public int Id { get; init; } + public SimpleEnum SimpleEnum { get; init; } + } + + public enum SimpleEnumModel + { + Value1, + Value2, + Value3 + } + + public record EntityModel + { + public int Id { get; init; } + public SimpleEnumModel SimpleEnum { get; init; } + } + + public class HasFlagVisitor : ExpressionVisitor + { + public static Type GetasFlagReflectedType(Expression expression) + { + HasFlagVisitor hasFlagVisitor = new(); + hasFlagVisitor.Visit(expression); + return hasFlagVisitor.HasFlagReflectedType; + } + protected override Expression VisitMethodCall(MethodCallExpression node) + { + if (node.Method.Name == "HasFlag") + HasFlagReflectedType = node.Method.ReflectedType; + + return base.VisitMethodCall(node); + } + + public Type HasFlagReflectedType { get; private set; } + } + } +} From 2e4ea0ca373971765966364e7f58ada6efbe8c9d Mon Sep 17 00:00:00 2001 From: Blaise Taylor Date: Sat, 14 Sep 2024 08:42:20 -0400 Subject: [PATCH 18/18] Handling local constant expression. (#181) --- .../XpressionMapperVisitor.cs | 3 ++ ...apExpressionWithLocalExpressionConstant.cs | 49 +++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/CanMapExpressionWithLocalExpressionConstant.cs diff --git a/src/AutoMapper.Extensions.ExpressionMapping/XpressionMapperVisitor.cs b/src/AutoMapper.Extensions.ExpressionMapping/XpressionMapperVisitor.cs index 0a92b62..15cafb7 100644 --- a/src/AutoMapper.Extensions.ExpressionMapping/XpressionMapperVisitor.cs +++ b/src/AutoMapper.Extensions.ExpressionMapping/XpressionMapperVisitor.cs @@ -538,6 +538,9 @@ protected override Expression VisitConstant(ConstantExpression node) return base.VisitConstant(Expression.Constant(Mapper.MapObject(node.Value, node.Type, newType), newType)); //Issue 3455 (Non-Generic Mapper.Map failing for structs in v10) //return base.VisitConstant(Expression.Constant(Mapper.Map(node.Value, node.Type, newType), newType)); + + if (typeof(Expression).IsAssignableFrom(node.Type)) + return Expression.Constant(this.Visit((Expression)node.Value), newType); } return base.VisitConstant(node); } diff --git a/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/CanMapExpressionWithLocalExpressionConstant.cs b/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/CanMapExpressionWithLocalExpressionConstant.cs new file mode 100644 index 0000000..d83f867 --- /dev/null +++ b/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/CanMapExpressionWithLocalExpressionConstant.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using Xunit; + +namespace AutoMapper.Extensions.ExpressionMapping.UnitTests +{ + public class CanMapExpressionWithLocalExpressionConstant + { + [Fact] + public void Map_expression_wchich_includes_local_constant() + { + //Arrange + var config = new MapperConfiguration + ( + cfg => + { + cfg.CreateMap(); + cfg.CreateMap(); + } + ); + config.AssertConfigurationIsValid(); + var mapper = config.CreateMapper(); + List source = [ + new Entity { Id = 1 }, + new Entity { Id = 3 } + ]; + + //act + Expression> filter = f => f.Id > 2; + Expression, IQueryable>> queryableExpression = q => q.Where(filter); + Expression, IQueryable>> queryableExpressionMapped = mapper.MapExpression, IQueryable>>>(queryableExpression); + + //assert + Assert.Equal(1, queryableExpressionMapped.Compile()(source.AsQueryable()).Count()); + } + + public record Entity + { + public int Id { get; init; } + } + + public record EntityModel + { + public int Id { get; init; } + } + } +}