From 88171611301b62f52ffa86469904cf7f447fb30a Mon Sep 17 00:00:00 2001 From: Blaise Taylor Date: Tue, 28 Jun 2022 06:19:02 -0400 Subject: [PATCH] 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; }