diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 94ed54d..21ac189 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -23,14 +23,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 336a1e5..479395a 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:
diff --git a/icon.png b/icon.png
new file mode 100644
index 0000000..56b96ca
Binary files /dev/null and b/icon.png differ
diff --git a/src/AutoMapper.Extensions.ExpressionMapping/AutoMapper.Extensions.ExpressionMapping.csproj b/src/AutoMapper.Extensions.ExpressionMapping/AutoMapper.Extensions.ExpressionMapping.csproj
index db7e5ff..dcf0e95 100644
--- a/src/AutoMapper.Extensions.ExpressionMapping/AutoMapper.Extensions.ExpressionMapping.csproj
+++ b/src/AutoMapper.Extensions.ExpressionMapping/AutoMapper.Extensions.ExpressionMapping.csproj
@@ -3,15 +3,15 @@
Expression mapping (OData) extensions for AutoMapper
Expression mapping (OData) extensions for AutoMapper
- netstandard2.1
+ net6.0
true
..\..\AutoMapper.snk
true
true
AutoMapper.Extensions.ExpressionMapping
- https://s3.amazonaws.com/automapper/icon.png
- http://automapper.org
- https://github.com/AutoMapper/AutoMapper.Extensions.ExpressionMapping/blob/master/LICENSE
+ icon.png
+ https://automapper.org
+ MIT
git
https://github.com/Amtote/AutoMapper.Extensions.ExpressionMapping
amtote
@@ -24,8 +24,12 @@
true
+
+
+
+
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
@@ -33,4 +37,19 @@
+
+
+ True
+ True
+ Resources.resx
+
+
+
+
+
+ ResXFileCodeGenerator
+ Resources.Designer.cs
+
+
+
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();
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/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/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/src/AutoMapper.Extensions.ExpressionMapping/MapperExtensions.cs b/src/AutoMapper.Extensions.ExpressionMapping/MapperExtensions.cs
index c8df785..72ba3d0 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<>))
{
@@ -335,29 +335,46 @@ 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;
}
}
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 +389,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++)
{
@@ -396,7 +413,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/PrependParentNameVisitor.cs b/src/AutoMapper.Extensions.ExpressionMapping/PrependParentNameVisitor.cs
index ae40e80..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
{
@@ -18,111 +15,16 @@ public PrependParentNameVisitor(ParameterExpression currentParameter, string par
public string ParentFullName { get; }
public Expression NewParameter { get; }
- protected override Expression VisitTypeBinary(TypeBinaryExpression node)
+ protected override Expression VisitParameter(ParameterExpression 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 (object.ReferenceEquals(CurrentParameter, node))
{
- 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;
+ return string.IsNullOrEmpty(ParentFullName)
+ ? NewParameter
+ : ExpressionHelpers.MemberAccesses(ParentFullName, NewParameter);
}
- bool IsExtentionMethod()
- => node.Method.IsDefined(typeof(ExtensionAttribute), true);
+ return base.VisitParameter(node);
}
}
}
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/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/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/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/src/AutoMapper.Extensions.ExpressionMapping/TypeMapHelper.cs b/src/AutoMapper.Extensions.ExpressionMapping/TypeMapHelper.cs
index d559096..de99e10 100644
--- a/src/AutoMapper.Extensions.ExpressionMapping/TypeMapHelper.cs
+++ b/src/AutoMapper.Extensions.ExpressionMapping/TypeMapHelper.cs
@@ -6,6 +6,57 @@ 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 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);
+ 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 a0fa6f5..15cafb7 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, PropertyMapInfo lastWithCustExpression, Expression mappedParentExpr)
=> GetMemberExpressionFromCustomExpression
(
@@ -382,7 +405,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
@@ -505,15 +528,19 @@ 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));
+
+ if (typeof(Expression).IsAssignableFrom(node.Type))
+ return Expression.Constant(this.Visit((Expression)node.Value), newType);
}
return base.VisitConstant(node);
}
@@ -545,9 +572,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, node.Method, 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
@@ -619,6 +656,23 @@ private bool GenericTypeDefinitionsAreEquivalent(Type typeSource, Type typeDesti
protected void FindDestinationFullName(Type typeSource, Type typeDestination, string sourceFullName, 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);
@@ -694,27 +748,9 @@ TypeMap GetTypeMap() => BothTypesAreAnonymous()
{
var propertyMap = typeMap.GetMemberMapByDestinationProperty(sourceFullName);
var sourceMemberInfo = typeSource.GetFieldOrProperty(propertyMap.GetDestinationName());
- if (propertyMap.ValueResolverConfig != null)
- {
- throw new InvalidOperationException(Resource.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 +764,7 @@ TypeMap GetTypeMap() => BothTypesAreAnonymous()
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()));
@@ -737,7 +773,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);
}
}
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/CanMapExpressionWithListConstants.cs b/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/CanMapExpressionWithListConstants.cs
new file mode 100644
index 0000000..c1ab754
--- /dev/null
+++ b/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/CanMapExpressionWithListConstants.cs
@@ -0,0 +1,189 @@
+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();
+ }
+ );
+ 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();
+ }
+ );
+ 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();
+ }
+ );
+ 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();
+ }
+ );
+ 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; }
+ }
+ }
+}
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; }
+ }
+ }
+}
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/CanMapParameterBodyFromChildReferenceWithoutMemberExpression.cs b/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/CanMapParameterBodyFromChildReferenceWithoutMemberExpression.cs
new file mode 100644
index 0000000..0c26713
--- /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 => (Property(x.Category, \"BrandId\") == 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..f0e8db3
--- /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 => (Property(x, \"BrandId\") == 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..6836154
--- /dev/null
+++ b/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/EF.cs
@@ -0,0 +1,10 @@
+namespace AutoMapper.Extensions.ExpressionMapping.UnitTests
+{
+ internal class EF
+ {
+ internal static T Property(object p, string v)
+ {
+ return default;
+ }
+ }
+}
diff --git a/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/ExpressionMappingEnumToNumericOrString.cs b/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/ExpressionMappingEnumToNumericOrString.cs
new file mode 100644
index 0000000..bda7b52
--- /dev/null
+++ b/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/ExpressionMappingEnumToNumericOrString.cs
@@ -0,0 +1,295 @@
+
+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, 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);
+ }
+
+ [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; }
+ 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);
+ }
+ }
+}
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
new file mode 100644
index 0000000..6fb88f9
--- /dev/null
+++ b/tests/AutoMapper.Extensions.ExpressionMapping.UnitTests/ShouldThrowInvalidOperationExceptionForUnmatchedLiterals.cs
@@ -0,0 +1,140 @@
+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)
+ {
+ 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)
+ {
+ 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
+ )
+ );
+ }
+
+ 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/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; }
+ }
+ }
+}
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