From 9754feaabb5bd1902783820ce2c3e5f9a720db7c Mon Sep 17 00:00:00 2001 From: Dingmeng Xue Date: Thu, 17 Nov 2022 17:00:08 +0800 Subject: [PATCH] Add AutoMapper 6.2.2 (#364) * Update assembly infor * update license and version * Add automapper 6.2.2 --- .../Azure-PowerShell-Common-Publish.yml | 43 +- tools/AutoMapper/AdvancedConfiguration.cs | 35 + tools/AutoMapper/AssemblyInfo.cs | 13 + tools/AutoMapper/AutoMapper.csproj | 32 + .../AutoMapperConfigurationException.cs | 124 ++++ .../AutoMapper/AutoMapperMappingException.cs | 88 +++ .../Conventions/AllMemberInfo.cs | 25 + .../Conventions/CaseInsensitiveName.cs | 16 + .../Conventions/CaseSensitiveName.cs | 22 + .../Conventions/DefaultMember.cs | 24 + .../Configuration/Conventions/DefaultName.cs | 6 + .../Conventions/IChildMemberConfiguration.cs | 11 + .../Conventions/IGetTypeInfoMembers.cs | 12 + .../Conventions/IMemberConfiguration.cs | 19 + .../IParentSourceToDestinationNameMapper.cs | 13 + .../ISourceToDestinationNameMapper.cs | 10 + .../Conventions/MapToAttribute.cs | 17 + .../Conventions/MemberConfiguration.cs | 61 ++ .../Conventions/MemberNameReplacer.cs | 14 + .../Conventions/NameSplitMember.cs | 65 ++ .../ParentSourceToDestinationNameMapper.cs | 26 + .../Conventions/PrePostfixName.cs | 64 ++ .../Configuration/Conventions/ReplaceName.cs | 44 ++ .../SourceToDestinationMapperAttribute.cs | 10 + ...ToDestinationNameMapperAttributesMember.cs | 35 + .../Configuration/IConfiguration.cs | 11 + .../Configuration/IProfileConfiguration.cs | 50 ++ .../IPropertyMapConfiguration.cs | 11 + .../Configuration/ITypeMapConfiguration.cs | 14 + .../Configuration/Internal/PrimitiveHelper.cs | 77 +++ .../MapperConfigurationExpression.cs | 84 +++ .../Configuration/MappingExpression.cs | 651 ++++++++++++++++++ .../MemberConfigurationExpression.cs | 305 ++++++++ tools/AutoMapper/Configuration/MemberPath.cs | 45 ++ .../PathConfigurationExpression.cs | 94 +++ .../Configuration/PrimitiveExtensions.cs | 64 ++ .../Configuration/ResolutionExpression.cs | 44 ++ .../Configuration/SourceMappingExpression.cs | 26 + .../Configuration/SourceMemberConfig.cs | 20 + tools/AutoMapper/ConfigurationValidator.cs | 118 ++++ tools/AutoMapper/ConstructorMap.cs | 58 ++ tools/AutoMapper/ConstructorParameterMap.cs | 30 + .../DuplicateTypeMapConfigurationException.cs | 40 ++ tools/AutoMapper/Execution/DelegateFactory.cs | 99 +++ .../AutoMapper/Execution/ExpressionBuilder.cs | 125 ++++ tools/AutoMapper/Execution/PropertyEmitter.cs | 67 ++ tools/AutoMapper/Execution/ProxyBase.cs | 12 + tools/AutoMapper/Execution/ProxyGenerator.cs | 244 +++++++ .../Execution/TypeMapPlanBuilder.cs | 580 ++++++++++++++++ tools/AutoMapper/ExpressionExtensions.cs | 59 ++ tools/AutoMapper/IConfigurationProvider.cs | 170 +++++ .../ICtorParamConfigurationExpression.cs | 75 ++ tools/AutoMapper/IMapper.cs | 126 ++++ .../IMapperConfigurationExpression.cs | 91 +++ tools/AutoMapper/IMappingAction.cs | 17 + tools/AutoMapper/IMappingExpression.cs | 471 +++++++++++++ tools/AutoMapper/IMappingOperationOptions.cs | 66 ++ .../IMemberConfigurationExpression.cs | 238 +++++++ tools/AutoMapper/INamingConvention.cs | 19 + tools/AutoMapper/IObjectMapper.cs | 70 ++ tools/AutoMapper/IObjectMapperInfo.cs | 7 + .../IPathConfigurationExpression.cs | 47 ++ tools/AutoMapper/IProfileExpression.cs | 168 +++++ tools/AutoMapper/IResolutionExpression.cs | 30 + .../IResolverConfigurationExpression.cs | 26 + .../ISourceMemberConfigurationExpression.cs | 13 + tools/AutoMapper/ITypeConverter.cs | 19 + tools/AutoMapper/IValueResolver.cs | 35 + tools/AutoMapper/IgnoreMapAttribute.cs | 12 + .../AutoMapper/Internal/ExpressionFactory.cs | 242 +++++++ tools/AutoMapper/Internal/MemberVisitor.cs | 25 + tools/AutoMapper/Internal/ReflectionHelper.cs | 190 +++++ .../AutoMapper/LockingConcurrentDictionary.cs | 42 ++ .../LowerUnderscoreNamingConvention.cs | 13 + tools/AutoMapper/Mapper.cs | 375 ++++++++++ tools/AutoMapper/MapperConfiguration.cs | 470 +++++++++++++ .../MapperConfigurationExpressionValidator.cs | 34 + tools/AutoMapper/Mappers/ArrayMapper.cs | 57 ++ tools/AutoMapper/Mappers/AssignableMapper.cs | 12 + tools/AutoMapper/Mappers/CollectionMapper.cs | 17 + tools/AutoMapper/Mappers/ConvertMapper.cs | 50 ++ .../Mappers/CreateMapBasedOnCriteriaMapper.cs | 34 + tools/AutoMapper/Mappers/DictionaryMapper.cs | 17 + tools/AutoMapper/Mappers/EnumToEnumMapper.cs | 49 ++ .../AutoMapper/Mappers/EnumToStringMapper.cs | 41 ++ .../Mappers/EnumToUnderlyingTypeMapper.cs | 34 + tools/AutoMapper/Mappers/EnumerableMapper.cs | 30 + .../Mappers/EnumerableMapperBase.cs | 18 + .../Mappers/EnumerableToDictionaryMapper.cs | 21 + .../ExplicitConversionOperatorMapper.cs | 35 + tools/AutoMapper/Mappers/ExpressionMapper.cs | 281 ++++++++ tools/AutoMapper/Mappers/FlagsEnumMapper.cs | 40 ++ tools/AutoMapper/Mappers/FromDynamicMapper.cs | 62 ++ .../Mappers/FromStringDictionaryMapper.cs | 48 ++ tools/AutoMapper/Mappers/HashSetMapper.cs | 21 + .../ImplicitConversionOperatorMapper.cs | 34 + .../CollectionMapperExpressionFactory.cs | 121 ++++ .../Mappers/Internal/ElementTypeHelper.cs | 66 ++ tools/AutoMapper/Mappers/MapperRegistry.cs | 43 ++ .../Mappers/MultidimensionalArrayMapper.cs | 98 +++ .../Mappers/NameValueCollectionMapper.cs | 29 + .../Mappers/NullableDestinationMapper.cs | 28 + .../Mappers/NullableSourceMapper.cs | 30 + .../Mappers/ReadOnlyCollectionMapper.cs | 34 + tools/AutoMapper/Mappers/StringMapper.cs | 16 + .../AutoMapper/Mappers/StringToEnumMapper.cs | 53 ++ tools/AutoMapper/Mappers/ToDynamicMapper.cs | 64 ++ .../Mappers/ToStringDictionaryMapper.cs | 31 + .../AutoMapper/Mappers/TypeConverterMapper.cs | 51 ++ .../Mappers/UnderlyingTypeToEnumMapper.cs | 32 + tools/AutoMapper/MappingOperationOptions.cs | 62 ++ tools/AutoMapper/MemberList.cs | 23 + .../AutoMapper/PascalCaseNamingConvention.cs | 12 + tools/AutoMapper/PathMap.cs | 28 + tools/AutoMapper/Profile.cs | 199 ++++++ tools/AutoMapper/ProfileMap.cs | 259 +++++++ tools/AutoMapper/PropertyMap.cs | 151 ++++ .../QueryableExtensions/ExpressionBuilder.cs | 565 +++++++++++++++ .../QueryableExtensions/ExpressionRequest.cs | 67 ++ .../ExpressionResolutionResult.cs | 17 + .../QueryableExtensions/Extensions.cs | 126 ++++ .../QueryableExtensions/IExpressionBinder.cs | 12 + .../IExpressionResultConverter.cs | 10 + .../IProjectionExpression.cs | 65 ++ .../Impl/AssignableExpressionBinder.cs | 17 + .../Impl/CustomProjectionExpressionBinder.cs | 17 + .../Impl/EnumerableExpressionBinder.cs | 45 ++ .../Impl/ISourceInjectedQueryable.cs | 21 + .../Impl/MappedTypeExpressionBinder.cs | 34 + .../Impl/MemberAccessQueryMapperVisitor.cs | 31 + .../MemberGetterExpressionResultConverter.cs | 34 + ...MemberResolverExpressionResultConverter.cs | 36 + .../NullableDestinationExpressionBinder.cs | 48 ++ .../Impl/NullableSourceExpressionBinder.cs | 21 + .../Impl/QueryDataSourceInjection.cs | 164 +++++ .../Impl/QueryMapperHelper.cs | 48 ++ .../Impl/QueryMapperVisitor.cs | 165 +++++ .../Impl/SourceInjectedQuery.cs | 80 +++ .../Impl/SourceInjectedQueryInspector.cs | 19 + .../Impl/SourceInjectedQueryProvider.cs | 416 +++++++++++ .../Impl/StringExpressionBinder.cs | 17 + .../ProjectionExpression.cs | 92 +++ tools/AutoMapper/ReflectionExtensions.cs | 333 +++++++++ tools/AutoMapper/ResolutionContext.cs | 145 ++++ tools/AutoMapper/SemanticModel.cd | 163 +++++ tools/AutoMapper/TypeDetails.cs | 231 +++++++ tools/AutoMapper/TypeExtensions.cs | 155 +++++ tools/AutoMapper/TypeMap.cs | 399 +++++++++++ tools/AutoMapper/TypeMapFactory.cs | 72 ++ tools/AutoMapper/TypeMapRegistry.cs | 16 + tools/AutoMapper/TypePair.cs | 178 +++++ .../AutoMapper/ValueResolverConfiguration.cs | 26 + .../ValueTransformerConfiguration.cs | 40 ++ .../ArgumentMappers/ArgumentMapper.cs | 30 + .../ArgumentMappers/DefaultArgumentMapper.cs | 14 + .../ArgumentMappers/LambdaArgumentMapper.cs | 25 + .../ArgumentMappers/QuoteArgumentMapper.cs | 25 + .../Extensions/MapperExtensions.cs | 247 +++++++ .../Extensions/VisitorExtensions.cs | 185 +++++ .../FindMemberExpressionsVisitor.cs | 62 ++ .../XpressionMapper/MapIncludesVisitor.cs | 110 +++ .../XpressionMapper/MapperInfoDictionary.cs | 25 + .../ParameterExpressionEqualityComparer.cs | 12 + .../PrependParentNameVisitor.cs | 48 ++ .../XpressionMapper/Resource.Designer.cs | 172 +++++ .../AutoMapper/XpressionMapper/Resource.resx | 161 +++++ .../XpressionMapper/Structures/MapperInfo.cs | 24 + .../Structures/PropertyMapInfo.cs | 18 + .../XpressionMapper/XpressionMapperVisitor.cs | 267 +++++++ tools/AutoMapper/licenses/LICENSE.txt | 9 + 170 files changed, 14528 insertions(+), 10 deletions(-) create mode 100644 tools/AutoMapper/AdvancedConfiguration.cs create mode 100644 tools/AutoMapper/AssemblyInfo.cs create mode 100644 tools/AutoMapper/AutoMapper.csproj create mode 100644 tools/AutoMapper/AutoMapperConfigurationException.cs create mode 100644 tools/AutoMapper/AutoMapperMappingException.cs create mode 100644 tools/AutoMapper/Configuration/Conventions/AllMemberInfo.cs create mode 100644 tools/AutoMapper/Configuration/Conventions/CaseInsensitiveName.cs create mode 100644 tools/AutoMapper/Configuration/Conventions/CaseSensitiveName.cs create mode 100644 tools/AutoMapper/Configuration/Conventions/DefaultMember.cs create mode 100644 tools/AutoMapper/Configuration/Conventions/DefaultName.cs create mode 100644 tools/AutoMapper/Configuration/Conventions/IChildMemberConfiguration.cs create mode 100644 tools/AutoMapper/Configuration/Conventions/IGetTypeInfoMembers.cs create mode 100644 tools/AutoMapper/Configuration/Conventions/IMemberConfiguration.cs create mode 100644 tools/AutoMapper/Configuration/Conventions/IParentSourceToDestinationNameMapper.cs create mode 100644 tools/AutoMapper/Configuration/Conventions/ISourceToDestinationNameMapper.cs create mode 100644 tools/AutoMapper/Configuration/Conventions/MapToAttribute.cs create mode 100644 tools/AutoMapper/Configuration/Conventions/MemberConfiguration.cs create mode 100644 tools/AutoMapper/Configuration/Conventions/MemberNameReplacer.cs create mode 100644 tools/AutoMapper/Configuration/Conventions/NameSplitMember.cs create mode 100644 tools/AutoMapper/Configuration/Conventions/ParentSourceToDestinationNameMapper.cs create mode 100644 tools/AutoMapper/Configuration/Conventions/PrePostfixName.cs create mode 100644 tools/AutoMapper/Configuration/Conventions/ReplaceName.cs create mode 100644 tools/AutoMapper/Configuration/Conventions/SourceToDestinationMapperAttribute.cs create mode 100644 tools/AutoMapper/Configuration/Conventions/SourceToDestinationNameMapperAttributesMember.cs create mode 100644 tools/AutoMapper/Configuration/IConfiguration.cs create mode 100644 tools/AutoMapper/Configuration/IProfileConfiguration.cs create mode 100644 tools/AutoMapper/Configuration/IPropertyMapConfiguration.cs create mode 100644 tools/AutoMapper/Configuration/ITypeMapConfiguration.cs create mode 100644 tools/AutoMapper/Configuration/Internal/PrimitiveHelper.cs create mode 100644 tools/AutoMapper/Configuration/MapperConfigurationExpression.cs create mode 100644 tools/AutoMapper/Configuration/MappingExpression.cs create mode 100644 tools/AutoMapper/Configuration/MemberConfigurationExpression.cs create mode 100644 tools/AutoMapper/Configuration/MemberPath.cs create mode 100644 tools/AutoMapper/Configuration/PathConfigurationExpression.cs create mode 100644 tools/AutoMapper/Configuration/PrimitiveExtensions.cs create mode 100644 tools/AutoMapper/Configuration/ResolutionExpression.cs create mode 100644 tools/AutoMapper/Configuration/SourceMappingExpression.cs create mode 100644 tools/AutoMapper/Configuration/SourceMemberConfig.cs create mode 100644 tools/AutoMapper/ConfigurationValidator.cs create mode 100644 tools/AutoMapper/ConstructorMap.cs create mode 100644 tools/AutoMapper/ConstructorParameterMap.cs create mode 100644 tools/AutoMapper/DuplicateTypeMapConfigurationException.cs create mode 100644 tools/AutoMapper/Execution/DelegateFactory.cs create mode 100644 tools/AutoMapper/Execution/ExpressionBuilder.cs create mode 100644 tools/AutoMapper/Execution/PropertyEmitter.cs create mode 100644 tools/AutoMapper/Execution/ProxyBase.cs create mode 100644 tools/AutoMapper/Execution/ProxyGenerator.cs create mode 100644 tools/AutoMapper/Execution/TypeMapPlanBuilder.cs create mode 100644 tools/AutoMapper/ExpressionExtensions.cs create mode 100644 tools/AutoMapper/IConfigurationProvider.cs create mode 100644 tools/AutoMapper/ICtorParamConfigurationExpression.cs create mode 100644 tools/AutoMapper/IMapper.cs create mode 100644 tools/AutoMapper/IMapperConfigurationExpression.cs create mode 100644 tools/AutoMapper/IMappingAction.cs create mode 100644 tools/AutoMapper/IMappingExpression.cs create mode 100644 tools/AutoMapper/IMappingOperationOptions.cs create mode 100644 tools/AutoMapper/IMemberConfigurationExpression.cs create mode 100644 tools/AutoMapper/INamingConvention.cs create mode 100644 tools/AutoMapper/IObjectMapper.cs create mode 100644 tools/AutoMapper/IObjectMapperInfo.cs create mode 100644 tools/AutoMapper/IPathConfigurationExpression.cs create mode 100644 tools/AutoMapper/IProfileExpression.cs create mode 100644 tools/AutoMapper/IResolutionExpression.cs create mode 100644 tools/AutoMapper/IResolverConfigurationExpression.cs create mode 100644 tools/AutoMapper/ISourceMemberConfigurationExpression.cs create mode 100644 tools/AutoMapper/ITypeConverter.cs create mode 100644 tools/AutoMapper/IValueResolver.cs create mode 100644 tools/AutoMapper/IgnoreMapAttribute.cs create mode 100644 tools/AutoMapper/Internal/ExpressionFactory.cs create mode 100644 tools/AutoMapper/Internal/MemberVisitor.cs create mode 100644 tools/AutoMapper/Internal/ReflectionHelper.cs create mode 100644 tools/AutoMapper/LockingConcurrentDictionary.cs create mode 100644 tools/AutoMapper/LowerUnderscoreNamingConvention.cs create mode 100644 tools/AutoMapper/Mapper.cs create mode 100644 tools/AutoMapper/MapperConfiguration.cs create mode 100644 tools/AutoMapper/MapperConfigurationExpressionValidator.cs create mode 100644 tools/AutoMapper/Mappers/ArrayMapper.cs create mode 100644 tools/AutoMapper/Mappers/AssignableMapper.cs create mode 100644 tools/AutoMapper/Mappers/CollectionMapper.cs create mode 100644 tools/AutoMapper/Mappers/ConvertMapper.cs create mode 100644 tools/AutoMapper/Mappers/CreateMapBasedOnCriteriaMapper.cs create mode 100644 tools/AutoMapper/Mappers/DictionaryMapper.cs create mode 100644 tools/AutoMapper/Mappers/EnumToEnumMapper.cs create mode 100644 tools/AutoMapper/Mappers/EnumToStringMapper.cs create mode 100644 tools/AutoMapper/Mappers/EnumToUnderlyingTypeMapper.cs create mode 100644 tools/AutoMapper/Mappers/EnumerableMapper.cs create mode 100644 tools/AutoMapper/Mappers/EnumerableMapperBase.cs create mode 100644 tools/AutoMapper/Mappers/EnumerableToDictionaryMapper.cs create mode 100644 tools/AutoMapper/Mappers/ExplicitConversionOperatorMapper.cs create mode 100644 tools/AutoMapper/Mappers/ExpressionMapper.cs create mode 100644 tools/AutoMapper/Mappers/FlagsEnumMapper.cs create mode 100644 tools/AutoMapper/Mappers/FromDynamicMapper.cs create mode 100644 tools/AutoMapper/Mappers/FromStringDictionaryMapper.cs create mode 100644 tools/AutoMapper/Mappers/HashSetMapper.cs create mode 100644 tools/AutoMapper/Mappers/ImplicitConversionOperatorMapper.cs create mode 100644 tools/AutoMapper/Mappers/Internal/CollectionMapperExpressionFactory.cs create mode 100644 tools/AutoMapper/Mappers/Internal/ElementTypeHelper.cs create mode 100644 tools/AutoMapper/Mappers/MapperRegistry.cs create mode 100644 tools/AutoMapper/Mappers/MultidimensionalArrayMapper.cs create mode 100644 tools/AutoMapper/Mappers/NameValueCollectionMapper.cs create mode 100644 tools/AutoMapper/Mappers/NullableDestinationMapper.cs create mode 100644 tools/AutoMapper/Mappers/NullableSourceMapper.cs create mode 100644 tools/AutoMapper/Mappers/ReadOnlyCollectionMapper.cs create mode 100644 tools/AutoMapper/Mappers/StringMapper.cs create mode 100644 tools/AutoMapper/Mappers/StringToEnumMapper.cs create mode 100644 tools/AutoMapper/Mappers/ToDynamicMapper.cs create mode 100644 tools/AutoMapper/Mappers/ToStringDictionaryMapper.cs create mode 100644 tools/AutoMapper/Mappers/TypeConverterMapper.cs create mode 100644 tools/AutoMapper/Mappers/UnderlyingTypeToEnumMapper.cs create mode 100644 tools/AutoMapper/MappingOperationOptions.cs create mode 100644 tools/AutoMapper/MemberList.cs create mode 100644 tools/AutoMapper/PascalCaseNamingConvention.cs create mode 100644 tools/AutoMapper/PathMap.cs create mode 100644 tools/AutoMapper/Profile.cs create mode 100644 tools/AutoMapper/ProfileMap.cs create mode 100644 tools/AutoMapper/PropertyMap.cs create mode 100644 tools/AutoMapper/QueryableExtensions/ExpressionBuilder.cs create mode 100644 tools/AutoMapper/QueryableExtensions/ExpressionRequest.cs create mode 100644 tools/AutoMapper/QueryableExtensions/ExpressionResolutionResult.cs create mode 100644 tools/AutoMapper/QueryableExtensions/Extensions.cs create mode 100644 tools/AutoMapper/QueryableExtensions/IExpressionBinder.cs create mode 100644 tools/AutoMapper/QueryableExtensions/IExpressionResultConverter.cs create mode 100644 tools/AutoMapper/QueryableExtensions/IProjectionExpression.cs create mode 100644 tools/AutoMapper/QueryableExtensions/Impl/AssignableExpressionBinder.cs create mode 100644 tools/AutoMapper/QueryableExtensions/Impl/CustomProjectionExpressionBinder.cs create mode 100644 tools/AutoMapper/QueryableExtensions/Impl/EnumerableExpressionBinder.cs create mode 100644 tools/AutoMapper/QueryableExtensions/Impl/ISourceInjectedQueryable.cs create mode 100644 tools/AutoMapper/QueryableExtensions/Impl/MappedTypeExpressionBinder.cs create mode 100644 tools/AutoMapper/QueryableExtensions/Impl/MemberAccessQueryMapperVisitor.cs create mode 100644 tools/AutoMapper/QueryableExtensions/Impl/MemberGetterExpressionResultConverter.cs create mode 100644 tools/AutoMapper/QueryableExtensions/Impl/MemberResolverExpressionResultConverter.cs create mode 100644 tools/AutoMapper/QueryableExtensions/Impl/NullableDestinationExpressionBinder.cs create mode 100644 tools/AutoMapper/QueryableExtensions/Impl/NullableSourceExpressionBinder.cs create mode 100644 tools/AutoMapper/QueryableExtensions/Impl/QueryDataSourceInjection.cs create mode 100644 tools/AutoMapper/QueryableExtensions/Impl/QueryMapperHelper.cs create mode 100644 tools/AutoMapper/QueryableExtensions/Impl/QueryMapperVisitor.cs create mode 100644 tools/AutoMapper/QueryableExtensions/Impl/SourceInjectedQuery.cs create mode 100644 tools/AutoMapper/QueryableExtensions/Impl/SourceInjectedQueryInspector.cs create mode 100644 tools/AutoMapper/QueryableExtensions/Impl/SourceInjectedQueryProvider.cs create mode 100644 tools/AutoMapper/QueryableExtensions/Impl/StringExpressionBinder.cs create mode 100644 tools/AutoMapper/QueryableExtensions/ProjectionExpression.cs create mode 100644 tools/AutoMapper/ReflectionExtensions.cs create mode 100644 tools/AutoMapper/ResolutionContext.cs create mode 100644 tools/AutoMapper/SemanticModel.cd create mode 100644 tools/AutoMapper/TypeDetails.cs create mode 100644 tools/AutoMapper/TypeExtensions.cs create mode 100644 tools/AutoMapper/TypeMap.cs create mode 100644 tools/AutoMapper/TypeMapFactory.cs create mode 100644 tools/AutoMapper/TypeMapRegistry.cs create mode 100644 tools/AutoMapper/TypePair.cs create mode 100644 tools/AutoMapper/ValueResolverConfiguration.cs create mode 100644 tools/AutoMapper/ValueTransformerConfiguration.cs create mode 100644 tools/AutoMapper/XpressionMapper/ArgumentMappers/ArgumentMapper.cs create mode 100644 tools/AutoMapper/XpressionMapper/ArgumentMappers/DefaultArgumentMapper.cs create mode 100644 tools/AutoMapper/XpressionMapper/ArgumentMappers/LambdaArgumentMapper.cs create mode 100644 tools/AutoMapper/XpressionMapper/ArgumentMappers/QuoteArgumentMapper.cs create mode 100644 tools/AutoMapper/XpressionMapper/Extensions/MapperExtensions.cs create mode 100644 tools/AutoMapper/XpressionMapper/Extensions/VisitorExtensions.cs create mode 100644 tools/AutoMapper/XpressionMapper/FindMemberExpressionsVisitor.cs create mode 100644 tools/AutoMapper/XpressionMapper/MapIncludesVisitor.cs create mode 100644 tools/AutoMapper/XpressionMapper/MapperInfoDictionary.cs create mode 100644 tools/AutoMapper/XpressionMapper/ParameterExpressionEqualityComparer.cs create mode 100644 tools/AutoMapper/XpressionMapper/PrependParentNameVisitor.cs create mode 100644 tools/AutoMapper/XpressionMapper/Resource.Designer.cs create mode 100644 tools/AutoMapper/XpressionMapper/Resource.resx create mode 100644 tools/AutoMapper/XpressionMapper/Structures/MapperInfo.cs create mode 100644 tools/AutoMapper/XpressionMapper/Structures/PropertyMapInfo.cs create mode 100644 tools/AutoMapper/XpressionMapper/XpressionMapperVisitor.cs create mode 100644 tools/AutoMapper/licenses/LICENSE.txt diff --git a/.azure-pipelines/Azure-PowerShell-Common-Publish.yml b/.azure-pipelines/Azure-PowerShell-Common-Publish.yml index b4b5f5573f..c61a21c27e 100644 --- a/.azure-pipelines/Azure-PowerShell-Common-Publish.yml +++ b/.azure-pipelines/Azure-PowerShell-Common-Publish.yml @@ -19,25 +19,29 @@ steps: { throw "The value of Version $env:VERSION must be format of X.X.X" } - displayName: 'Check Version' - task: UseDotNet@2 - displayName: 'Use dotnet sdk 2.1.302' + displayName: 'Install .NET SDK' inputs: - version: 2.1.302 + packageType: 'sdk' + version: '3.x' - powershell: '$(Agent.ToolsDirectory)/dotnet/dotnet msbuild build.proj /t:Build /p:Configuration=Release /p:FileVersion=$env:VERSION /NoLogo' - displayName: build + displayName: build common libs + +- powershell: '$(Agent.ToolsDirectory)/dotnet/dotnet build tools/AutoMapper/AutoMapper.csproj -c Release' + displayName: build AutoMapper - task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@1 displayName: 'dll Signing' inputs: ConnectedServiceName: 'ESRP Signing Service' - FolderPath: artifacts + FolderPath: . Pattern: | - Release\netstandard2.0\Microsoft.Azure.PowerShell*.dll - !Release\netstandard2.0\Microsoft.Azure.PowerShell*.Test.dll + artifacts\Release\netstandard2.0\Microsoft.Azure.PowerShell*.dll + tools\AutoMapper\bin\Release\netstandard2.0\Microsoft.Azure.PowerShell*.dll + !artifacts\Release\netstandard2.0\Microsoft.Azure.PowerShell*.Test.dll UseMinimatch: true signConfigType: inlineSignParams inlineOperation: | @@ -78,8 +82,12 @@ steps: } ] -- powershell: | - $(Agent.ToolsDirectory)/dotnet/dotnet msbuild build.proj /t:Pack /p:Configuration=Release /p:PackageVersion=$env:VERSION /NoLogo +- task: PowerShell@2 + inputs: + targetType: 'inline' + script: | + $(Agent.ToolsDirectory)/dotnet/dotnet msbuild build.proj /t:Pack /p:Configuration=Release /p:PackageVersion=$env:VERSION /NoLogo + $(Agent.ToolsDirectory)/dotnet/dotnet pack tools/AutoMapper/AutoMapper.csproj -c Release --no-build displayName: pack - task: NuGetAuthenticate@0 @@ -91,7 +99,7 @@ steps: condition: and(succeeded(), ne(variables['publish'], 'false')) inputs: command: push - packagesToPush: 'artifacts/Package/Release/Microsoft.Azure.PowerShell.*.nupkg;!artifacts/Package/Release/Microsoft.Azure.PowerShell.*.symbols.nupkg' # .nupkg and .snupkg + packagesToPush: 'artifacts/Package/Release/Microsoft.Azure.PowerShell.*.nupkg;!artifacts/Package/Release/Microsoft.Azure.PowerShell.*.symbols.nupkg;' # .nupkg and .snupkg publishVstsFeed: public/azure-powershell includeSymbols: false @@ -106,3 +114,18 @@ steps: PathtoPublish: artifacts/Package/Release/ ArtifactName: artifacts condition: succeededOrFailed() + +- task: PublishBuildArtifacts@1 + displayName: 'Publish Artifact' + inputs: + PathtoPublish: tools/AutoMapper/bin/Release + ArtifactName: AutoMapper + condition: succeededOrFailed() + +- task: PublishBuildArtifacts@1 + displayName: 'Publish All Artifact For Debug' + inputs: + PathtoPublish: . + ArtifactName: all + condition: and(succeededOrFailed(), eq(variables['System.debug'], 'true')) + diff --git a/tools/AutoMapper/AdvancedConfiguration.cs b/tools/AutoMapper/AdvancedConfiguration.cs new file mode 100644 index 0000000000..816dcc5f9c --- /dev/null +++ b/tools/AutoMapper/AdvancedConfiguration.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; + +namespace AutoMapper +{ + using Validator = Action; + + public class AdvancedConfiguration + { + private readonly List _validators = new List(); + private readonly IList> _beforeSealActions = new List>(); + public IEnumerable> BeforeSealActions => _beforeSealActions; + + /// + /// Add Action called against the IConfigurationProvider before it gets sealed + /// + public void BeforeSeal(Action action) => _beforeSealActions.Add(action); + + /// + /// Add an action to be called when validating the configuration. + /// + /// the validation callback + public void Validator(Validator validator) => _validators.Add(validator); + + public bool AllowAdditiveTypeMapCreation { get; set; } + + /// + /// How many levels deep should AutoMapper try to inline the execution plan for child classes. + /// See the wiki for details. + /// + public int MaxExecutionPlanDepth { get; set; } = 1; + + internal Validator[] GetValidators() => _validators.ToArray(); + } +} \ No newline at end of file diff --git a/tools/AutoMapper/AssemblyInfo.cs b/tools/AutoMapper/AssemblyInfo.cs new file mode 100644 index 0000000000..1e6db0cd8d --- /dev/null +++ b/tools/AutoMapper/AssemblyInfo.cs @@ -0,0 +1,13 @@ +using System; +using System.Reflection; +using System.Resources; +using System.Runtime.InteropServices; +using System.Security; + +[assembly: CLSCompliant(true)] +[assembly: AllowPartiallyTrustedCallers] +[assembly: ComVisible(false)] +[assembly: NeutralResourcesLanguage("en")] +[assembly: Guid("b38fd93e-7dc6-43d3-9081-b2f907994b74")] +[assembly: AssemblyVersion("1.0.0")] +[assembly: AssemblyFileVersion("6.2.2")] \ No newline at end of file diff --git a/tools/AutoMapper/AutoMapper.csproj b/tools/AutoMapper/AutoMapper.csproj new file mode 100644 index 0000000000..6109bc718c --- /dev/null +++ b/tools/AutoMapper/AutoMapper.csproj @@ -0,0 +1,32 @@ + + + + A convention-based object-object mapper. + A convention-based object-object mapper. + netstandard2.0 + Microsoft.Azure.PowerShell.AutoMapper + ..\..\src\MSSharedLibKey.snk + true + 6.2.2 + true + true + Microsoft.Azure.PowerShell.AutoMapper + https://automapper.org + LICENSE.txt + git + https://github.com/AutoMapper/AutoMapper + true + $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb + $(NoWarn);1591 + false + + + + + + + + + + + diff --git a/tools/AutoMapper/AutoMapperConfigurationException.cs b/tools/AutoMapper/AutoMapperConfigurationException.cs new file mode 100644 index 0000000000..27b4aab0cb --- /dev/null +++ b/tools/AutoMapper/AutoMapperConfigurationException.cs @@ -0,0 +1,124 @@ +using System; +using System.Linq; +using System.Text; + +namespace AutoMapper +{ + public class AutoMapperConfigurationException : Exception + { + public TypeMapConfigErrors[] Errors { get; } + public TypePair? Types { get; } + public PropertyMap PropertyMap { get; set; } + + public class TypeMapConfigErrors + { + public TypeMap TypeMap { get; } + public string[] UnmappedPropertyNames { get; } + public bool CanConstruct { get; } + + public TypeMapConfigErrors(TypeMap typeMap, string[] unmappedPropertyNames, bool canConstruct) + { + TypeMap = typeMap; + UnmappedPropertyNames = unmappedPropertyNames; + CanConstruct = canConstruct; + } + } + + public AutoMapperConfigurationException(string message) + : base(message) + { + } + + protected AutoMapperConfigurationException(string message, Exception inner) + : base(message, inner) + { + } + + public AutoMapperConfigurationException(TypeMapConfigErrors[] errors) => Errors = errors; + + public AutoMapperConfigurationException(TypePair types) => Types = types; + + public override string Message + { + get + { + if (Types != null) + { + var message = + string.Format( + "The following property on {0} cannot be mapped: \n\t{2} \nAdd a custom mapping expression, ignore, add a custom resolver, or modify the destination type {1}.", + Types?.DestinationType.FullName, Types?.DestinationType.FullName, + PropertyMap?.DestinationProperty.Name); + + message += "\nContext:"; + + Exception exToUse = this; + while (exToUse != null) + { + if (exToUse is AutoMapperConfigurationException configExc) + { + message += configExc.PropertyMap == null + ? $"\n\tMapping from type {configExc.Types?.SourceType.FullName} to {configExc.Types?.DestinationType.FullName}" + : $"\n\tMapping to property {configExc.PropertyMap.DestinationProperty.Name} from {configExc.Types?.SourceType.FullName} to {configExc.Types?.DestinationType.FullName}"; + } + + exToUse = exToUse.InnerException; + } + + return message + "\n" + base.Message; + } + if (Errors != null) + { + var message = + new StringBuilder( + "\nUnmapped members were found. Review the types and members below.\nAdd a custom mapping expression, ignore, add a custom resolver, or modify the source/destination type\nFor no matching constructor, add a no-arg ctor, add optional arguments, or map all of the constructor parameters\n"); + + foreach (var error in Errors) + { + var len = error.TypeMap.SourceType.FullName.Length + + error.TypeMap.DestinationType.FullName.Length + 5; + + message.AppendLine(new string('=', len)); + message.AppendLine(error.TypeMap.SourceType.Name + " -> " + error.TypeMap.DestinationType.Name + + " (" + + error.TypeMap.ConfiguredMemberList + " member list)"); + message.AppendLine(error.TypeMap.SourceType.FullName + " -> " + + error.TypeMap.DestinationType.FullName + " (" + + error.TypeMap.ConfiguredMemberList + " member list)"); + message.AppendLine(); + + if (error.UnmappedPropertyNames.Any()) + { + message.AppendLine("Unmapped properties:"); + foreach (var name in error.UnmappedPropertyNames) + { + message.AppendLine(name); + } + } + if (!error.CanConstruct) + { + message.AppendLine("No available constructor."); + } + } + return message.ToString(); + } + return base.Message; + } + } + + public override string StackTrace + { + get + { + if (Errors != null) + return string.Join(Environment.NewLine, + base.StackTrace + .Split(new[] {Environment.NewLine}, StringSplitOptions.None) + .Where(str => !str.TrimStart().StartsWith("at AutoMapper.")) + .ToArray()); + + return base.StackTrace; + } + } + } +} \ No newline at end of file diff --git a/tools/AutoMapper/AutoMapperMappingException.cs b/tools/AutoMapper/AutoMapperMappingException.cs new file mode 100644 index 0000000000..f402218c87 --- /dev/null +++ b/tools/AutoMapper/AutoMapperMappingException.cs @@ -0,0 +1,88 @@ +using System; +#if !DEBUG +using System.Linq; +#endif + +namespace AutoMapper +{ + public class AutoMapperMappingException : Exception + { + private readonly string _message; + + // + // For guidelines regarding the creation of new exception types, see + // http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpgenref/html/cpconerrorraisinghandlingguidelines.asp + // and + // http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dncscol/html/csharp07192001.asp + // + + public AutoMapperMappingException() + { + } + + public AutoMapperMappingException(string message) + : base(message) => _message = message; + + public AutoMapperMappingException(string message, Exception innerException) + : base(message, innerException) => _message = message; + + public AutoMapperMappingException(string message, Exception innerException, TypePair types) + : this(message, innerException) => Types = types; + + public AutoMapperMappingException(string message, Exception innerException, TypePair types, TypeMap typeMap) + : this(message, innerException, types) => TypeMap = typeMap; + + public AutoMapperMappingException(string message, Exception innerException, TypePair types, TypeMap typeMap, PropertyMap propertyMap) + : this(message, innerException, types, typeMap) => PropertyMap = propertyMap; + + public TypePair? Types { get; set; } + public TypeMap TypeMap { get; set; } + public PropertyMap PropertyMap { get; set; } + + public override string Message + { + get + { + var message = _message; + var newLine = Environment.NewLine; + if (Types?.SourceType != null && Types?.DestinationType != null) + { + message = message + newLine + newLine + "Mapping types:"; + message += newLine + + $"{Types?.SourceType.Name} -> {Types?.DestinationType.Name}"; + message += newLine + + $"{Types?.SourceType.FullName} -> {Types?.DestinationType.FullName}"; + } + if (TypeMap != null) + { + message = message + newLine + newLine + "Type Map configuration:"; + message += newLine + + $"{TypeMap.SourceType.Name} -> {TypeMap.DestinationType.Name}"; + message += newLine + + $"{TypeMap.SourceType.FullName} -> {TypeMap.DestinationType.FullName}"; + } + if (PropertyMap != null) + { + message = message + newLine + newLine + "Property:"; + message += newLine + + $"{PropertyMap.DestinationProperty.Name}"; + } + + return message; + } + } + +#if !DEBUG + public override string StackTrace + { + get + { + return string.Join(Environment.NewLine, + base.StackTrace + .Split(new[] {Environment.NewLine}, StringSplitOptions.None) + .Where(str => !str.TrimStart().StartsWith("at AutoMapper."))); + } + } +#endif + } +} diff --git a/tools/AutoMapper/Configuration/Conventions/AllMemberInfo.cs b/tools/AutoMapper/Configuration/Conventions/AllMemberInfo.cs new file mode 100644 index 0000000000..df66103475 --- /dev/null +++ b/tools/AutoMapper/Configuration/Conventions/AllMemberInfo.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace AutoMapper.Configuration.Conventions +{ + public class AllMemberInfo : IGetTypeInfoMembers + { + private readonly IList> _predicates = new List>(); + + public IEnumerable GetMemberInfos(TypeDetails typeInfo) + { + return !_predicates.Any() + ? typeInfo.AllMembers + : typeInfo.AllMembers.Where(m => _predicates.All(p => p(m))).ToList(); + } + + public IGetTypeInfoMembers AddCondition(Func predicate) + { + _predicates.Add(predicate); + return this; + } + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Configuration/Conventions/CaseInsensitiveName.cs b/tools/AutoMapper/Configuration/Conventions/CaseInsensitiveName.cs new file mode 100644 index 0000000000..5e4fc1d069 --- /dev/null +++ b/tools/AutoMapper/Configuration/Conventions/CaseInsensitiveName.cs @@ -0,0 +1,16 @@ +using System; +using System.Linq; +using System.Reflection; + +namespace AutoMapper.Configuration.Conventions +{ + public class CaseInsensitiveName : ISourceToDestinationNameMapper + { + public MemberInfo GetMatchingMemberInfo(IGetTypeInfoMembers getTypeInfoMembers, TypeDetails typeInfo, Type destType, Type destMemberType, string nameToSearch) + { + return + getTypeInfoMembers.GetMemberInfos(typeInfo) + .FirstOrDefault(mi => string.Compare(mi.Name, nameToSearch, StringComparison.OrdinalIgnoreCase) == 0); + } + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Configuration/Conventions/CaseSensitiveName.cs b/tools/AutoMapper/Configuration/Conventions/CaseSensitiveName.cs new file mode 100644 index 0000000000..ab1c449d79 --- /dev/null +++ b/tools/AutoMapper/Configuration/Conventions/CaseSensitiveName.cs @@ -0,0 +1,22 @@ +using System; +using System.Linq; +using System.Reflection; + +namespace AutoMapper.Configuration.Conventions +{ + public class CaseSensitiveName : ISourceToDestinationNameMapper + { + public bool MethodCaseSensitive { get; set; } + + public MemberInfo GetMatchingMemberInfo(IGetTypeInfoMembers getTypeInfoMembers, TypeDetails typeInfo, Type destType, Type destMemberType, string nameToSearch) + { + return + getTypeInfoMembers.GetMemberInfos(typeInfo) + .FirstOrDefault( + mi => + typeof (ParameterInfo).IsAssignableFrom(destType) || !MethodCaseSensitive + ? string.Compare(mi.Name, nameToSearch, StringComparison.OrdinalIgnoreCase) == 0 + : string.CompareOrdinal(mi.Name, nameToSearch) == 0); + } + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Configuration/Conventions/DefaultMember.cs b/tools/AutoMapper/Configuration/Conventions/DefaultMember.cs new file mode 100644 index 0000000000..e1b9089747 --- /dev/null +++ b/tools/AutoMapper/Configuration/Conventions/DefaultMember.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace AutoMapper.Configuration.Conventions +{ + // Source Destination Mapper + + public class DefaultMember : IChildMemberConfiguration + { + public IParentSourceToDestinationNameMapper NameMapper { get; set; } + + public bool MapDestinationPropertyToSource(ProfileMap options, TypeDetails sourceType, Type destType, Type destMemberType, string nameToSearch, LinkedList resolvers, IMemberConfiguration parent = null) + { + if (string.IsNullOrEmpty(nameToSearch)) + return true; + var matchingMemberInfo = NameMapper.GetMatchingMemberInfo(sourceType, destType, destMemberType, nameToSearch); + + if (matchingMemberInfo != null) + resolvers.AddLast(matchingMemberInfo); + return matchingMemberInfo != null; + } + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Configuration/Conventions/DefaultName.cs b/tools/AutoMapper/Configuration/Conventions/DefaultName.cs new file mode 100644 index 0000000000..f862f9eb86 --- /dev/null +++ b/tools/AutoMapper/Configuration/Conventions/DefaultName.cs @@ -0,0 +1,6 @@ +namespace AutoMapper.Configuration.Conventions +{ + public class DefaultName : CaseSensitiveName + { + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Configuration/Conventions/IChildMemberConfiguration.cs b/tools/AutoMapper/Configuration/Conventions/IChildMemberConfiguration.cs new file mode 100644 index 0000000000..8c3383d05f --- /dev/null +++ b/tools/AutoMapper/Configuration/Conventions/IChildMemberConfiguration.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace AutoMapper.Configuration.Conventions +{ + public interface IChildMemberConfiguration + { + bool MapDestinationPropertyToSource(ProfileMap options, TypeDetails sourceType, Type destType, Type destMemberType, string nameToSearch, LinkedList resolvers, IMemberConfiguration parent); + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Configuration/Conventions/IGetTypeInfoMembers.cs b/tools/AutoMapper/Configuration/Conventions/IGetTypeInfoMembers.cs new file mode 100644 index 0000000000..14284b0ed1 --- /dev/null +++ b/tools/AutoMapper/Configuration/Conventions/IGetTypeInfoMembers.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace AutoMapper.Configuration.Conventions +{ + public interface IGetTypeInfoMembers + { + IEnumerable GetMemberInfos(TypeDetails typeInfo); + IGetTypeInfoMembers AddCondition(Func predicate); + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Configuration/Conventions/IMemberConfiguration.cs b/tools/AutoMapper/Configuration/Conventions/IMemberConfiguration.cs new file mode 100644 index 0000000000..9a761796d7 --- /dev/null +++ b/tools/AutoMapper/Configuration/Conventions/IMemberConfiguration.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace AutoMapper.Configuration.Conventions +{ + public interface IMemberConfiguration + { + IList MemberMappers { get; } + IMemberConfiguration AddMember(Action setupAction = null) + where TMemberMapper : IChildMemberConfiguration, new(); + + IMemberConfiguration AddName(Action setupAction = null) + where TNameMapper : ISourceToDestinationNameMapper, new(); + + IParentSourceToDestinationNameMapper NameMapper { get; set; } + bool MapDestinationPropertyToSource(ProfileMap options, TypeDetails sourceType, Type destType, Type destMemberType, string nameToSearch, LinkedList resolvers); + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Configuration/Conventions/IParentSourceToDestinationNameMapper.cs b/tools/AutoMapper/Configuration/Conventions/IParentSourceToDestinationNameMapper.cs new file mode 100644 index 0000000000..4d214423b0 --- /dev/null +++ b/tools/AutoMapper/Configuration/Conventions/IParentSourceToDestinationNameMapper.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace AutoMapper.Configuration.Conventions +{ + public interface IParentSourceToDestinationNameMapper + { + ICollection NamedMappers { get; } + IGetTypeInfoMembers GetMembers { get; } + MemberInfo GetMatchingMemberInfo(TypeDetails typeInfo, Type destType, Type destMemberType, string nameToSearch); + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Configuration/Conventions/ISourceToDestinationNameMapper.cs b/tools/AutoMapper/Configuration/Conventions/ISourceToDestinationNameMapper.cs new file mode 100644 index 0000000000..68d0dd6ca3 --- /dev/null +++ b/tools/AutoMapper/Configuration/Conventions/ISourceToDestinationNameMapper.cs @@ -0,0 +1,10 @@ +using System; +using System.Reflection; + +namespace AutoMapper.Configuration.Conventions +{ + public interface ISourceToDestinationNameMapper + { + MemberInfo GetMatchingMemberInfo(IGetTypeInfoMembers getTypeInfoMembers, TypeDetails typeInfo, Type destType, Type destMemberType, string nameToSearch); + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Configuration/Conventions/MapToAttribute.cs b/tools/AutoMapper/Configuration/Conventions/MapToAttribute.cs new file mode 100644 index 0000000000..c8c117276f --- /dev/null +++ b/tools/AutoMapper/Configuration/Conventions/MapToAttribute.cs @@ -0,0 +1,17 @@ +using System; +using System.Reflection; + +namespace AutoMapper.Configuration.Conventions +{ + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] + public class MapToAttribute : SourceToDestinationMapperAttribute + { + public string MatchingName { get; } + + public MapToAttribute(string matchingName) + => MatchingName = matchingName; + + public override bool IsMatch(TypeDetails typeInfo, MemberInfo memberInfo, Type destType, Type destMemberType, string nameToSearch) + => string.Compare(MatchingName, nameToSearch, StringComparison.OrdinalIgnoreCase) == 0; + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Configuration/Conventions/MemberConfiguration.cs b/tools/AutoMapper/Configuration/Conventions/MemberConfiguration.cs new file mode 100644 index 0000000000..c3450954d1 --- /dev/null +++ b/tools/AutoMapper/Configuration/Conventions/MemberConfiguration.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reflection; + +namespace AutoMapper.Configuration.Conventions +{ + public class MemberConfiguration : IMemberConfiguration + { + public IParentSourceToDestinationNameMapper NameMapper { get; set; } + + public IList MemberMappers { get; } = new Collection(); + + public IMemberConfiguration AddMember(Action setupAction = null) + where TMemberMapper : IChildMemberConfiguration, new() + { + GetOrAdd(_ => (IList)_.MemberMappers, setupAction); + return this; + } + + public IMemberConfiguration AddName(Action setupAction = null) + where TNameMapper : ISourceToDestinationNameMapper, new() + { + GetOrAdd(_ => (IList)_.NameMapper.NamedMappers, setupAction); + return this; + } + + private TMemberMapper GetOrAdd(Func getList, Action setupAction = null) + where TMemberMapper : new() + { + var child = getList(this).OfType().FirstOrDefault(); + if (child == null) + { + child = new TMemberMapper(); + getList(this).Add(child); + } + setupAction?.Invoke(child); + return child; + } + + public MemberConfiguration() + { + NameMapper = new ParentSourceToDestinationNameMapper(); + MemberMappers.Add(new DefaultMember { NameMapper = NameMapper }); + } + + public bool MapDestinationPropertyToSource(ProfileMap options, TypeDetails sourceType, Type destType, Type destMemberType, string nameToSearch, LinkedList resolvers) + { + var foundMap = false; + foreach (var memberMapper in MemberMappers) + { + foundMap = memberMapper.MapDestinationPropertyToSource(options, sourceType, destType, destMemberType, nameToSearch, resolvers, this); + if (foundMap) + break; + } + return foundMap; + } + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Configuration/Conventions/MemberNameReplacer.cs b/tools/AutoMapper/Configuration/Conventions/MemberNameReplacer.cs new file mode 100644 index 0000000000..a8d099c86d --- /dev/null +++ b/tools/AutoMapper/Configuration/Conventions/MemberNameReplacer.cs @@ -0,0 +1,14 @@ +namespace AutoMapper.Configuration.Conventions +{ + public class MemberNameReplacer + { + public MemberNameReplacer(string originalValue, string newValue) + { + OriginalValue = originalValue; + NewValue = newValue; + } + + public string OriginalValue { get; } + public string NewValue { get; } + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Configuration/Conventions/NameSplitMember.cs b/tools/AutoMapper/Configuration/Conventions/NameSplitMember.cs new file mode 100644 index 0000000000..363582f03b --- /dev/null +++ b/tools/AutoMapper/Configuration/Conventions/NameSplitMember.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text.RegularExpressions; + +namespace AutoMapper.Configuration.Conventions +{ + public class NameSplitMember : IChildMemberConfiguration + { + public INamingConvention SourceMemberNamingConvention { get; set; } + public INamingConvention DestinationMemberNamingConvention { get; set; } + + public NameSplitMember() + { + SourceMemberNamingConvention = new PascalCaseNamingConvention(); + DestinationMemberNamingConvention = new PascalCaseNamingConvention(); + } + + public bool MapDestinationPropertyToSource(ProfileMap options, TypeDetails sourceType, Type destType, Type destMemberType, string nameToSearch, LinkedList resolvers, IMemberConfiguration parent ) + { + var matches = DestinationMemberNamingConvention.SplittingExpression + .Matches(nameToSearch) + .Cast() + .Select(m => SourceMemberNamingConvention.ReplaceValue(m)) + .ToArray(); + MemberInfo matchingMemberInfo = null; + for (var i = 1; i <= matches.Length; i++) + { + var snippet = CreateNameSnippet(matches, i); + + matchingMemberInfo = parent.NameMapper.GetMatchingMemberInfo(sourceType, destType, destMemberType, snippet.First); + + if (matchingMemberInfo != null) + { + resolvers.AddLast(matchingMemberInfo); + + var details = options.CreateTypeDetails(matchingMemberInfo.GetMemberType()); + var foundMatch = parent.MapDestinationPropertyToSource(options, details, destType, destMemberType, snippet.Second, resolvers); + + if (!foundMatch) + resolvers.RemoveLast(); + else + break; + } + } + return matchingMemberInfo != null; + } + private NameSnippet CreateNameSnippet(IEnumerable matches, int i) + { + var first = string.Join(SourceMemberNamingConvention.SeparatorCharacter, matches.Take(i).Select(s => SourceMemberNamingConvention.SplittingExpression.Replace(s, SourceMemberNamingConvention.ReplaceValue)).ToArray()); + var second = string.Join(SourceMemberNamingConvention.SeparatorCharacter, matches.Skip(i).Select(s => SourceMemberNamingConvention.SplittingExpression.Replace(s, SourceMemberNamingConvention.ReplaceValue)).ToArray()); + return new NameSnippet + { + First = first, + Second =second + }; + } + private class NameSnippet + { + public string First { get; set; } + public string Second { get; set; } + } + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Configuration/Conventions/ParentSourceToDestinationNameMapper.cs b/tools/AutoMapper/Configuration/Conventions/ParentSourceToDestinationNameMapper.cs new file mode 100644 index 0000000000..62e8d24612 --- /dev/null +++ b/tools/AutoMapper/Configuration/Conventions/ParentSourceToDestinationNameMapper.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Reflection; + +namespace AutoMapper.Configuration.Conventions +{ + public class ParentSourceToDestinationNameMapper : IParentSourceToDestinationNameMapper + { + public IGetTypeInfoMembers GetMembers { get; } = new AllMemberInfo(); + + public ICollection NamedMappers { get; } = new Collection {new DefaultName(), new SourceToDestinationNameMapperAttributesMember()}; + + public MemberInfo GetMatchingMemberInfo(TypeDetails typeInfo, Type destType, Type destMemberType, string nameToSearch) + { + MemberInfo memberInfo = null; + foreach (var namedMapper in NamedMappers) + { + memberInfo = namedMapper.GetMatchingMemberInfo(GetMembers, typeInfo, destType, destMemberType, nameToSearch); + if (memberInfo != null) + break; + } + return memberInfo; + } + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Configuration/Conventions/PrePostfixName.cs b/tools/AutoMapper/Configuration/Conventions/PrePostfixName.cs new file mode 100644 index 0000000000..96aca98ec4 --- /dev/null +++ b/tools/AutoMapper/Configuration/Conventions/PrePostfixName.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reflection; + +namespace AutoMapper.Configuration.Conventions +{ + public class PrePostfixName : ISourceToDestinationNameMapper + { + public ICollection Prefixes { get; } = new Collection(); + public ICollection Postfixes { get; } = new Collection(); + public ICollection DestinationPrefixes { get; } = new Collection(); + public ICollection DestinationPostfixes { get; } = new Collection(); + + public PrePostfixName AddStrings(Func> getStringsFunc, params string[] names) + { + var strings = getStringsFunc(this); + foreach (var name in names) + strings.Add(name); + return this; + } + + public MemberInfo GetMatchingMemberInfo(IGetTypeInfoMembers getTypeInfoMembers, TypeDetails typeInfo, Type destType, Type destMemberType, string nameToSearch) + { + var possibleSourceNames = DestinationPostfixes.Any() || DestinationPrefixes.Any() + ? PossibleNames(nameToSearch, DestinationPrefixes, DestinationPostfixes) + : new[] {nameToSearch}; + + var all = + from sourceName in possibleSourceNames + from destName in typeInfo.DestinationMemberNames + select new { sourceName, destName }; + var match = + all.FirstOrDefault( + pair => pair.destName.Possibles.Any(p => string.Compare(p, pair.sourceName, StringComparison.OrdinalIgnoreCase) == 0)); + return match?.destName.Member; + } + + private IEnumerable PossibleNames(string memberName, IEnumerable prefixes, IEnumerable postfixes) + { + if (string.IsNullOrEmpty(memberName)) + yield break; + + yield return memberName; + + foreach (var withoutPrefix in prefixes.Where(prefix => memberName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)).Select(prefix => memberName.Substring(prefix.Length))) + { + yield return withoutPrefix; + foreach (var s in PostFixes(postfixes, withoutPrefix)) + yield return s; + } + foreach (var s in PostFixes(postfixes, memberName)) + yield return s; + } + + private IEnumerable PostFixes(IEnumerable postfixes, string name) + { + return + postfixes.Where(postfix => name.EndsWith(postfix, StringComparison.OrdinalIgnoreCase)) + .Select(postfix => name.Remove(name.Length - postfix.Length)); + } + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Configuration/Conventions/ReplaceName.cs b/tools/AutoMapper/Configuration/Conventions/ReplaceName.cs new file mode 100644 index 0000000000..31c7844715 --- /dev/null +++ b/tools/AutoMapper/Configuration/Conventions/ReplaceName.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reflection; + +namespace AutoMapper.Configuration.Conventions +{ + public class ReplaceName : ISourceToDestinationNameMapper + { + private ICollection MemberNameReplacers { get; } + + public ReplaceName() => MemberNameReplacers = new Collection(); + + public ReplaceName AddReplace(string original, string newValue) + { + MemberNameReplacers.Add(new MemberNameReplacer(original, newValue)); + return this; + } + public MemberInfo GetMatchingMemberInfo(IGetTypeInfoMembers getTypeInfoMembers, TypeDetails typeInfo, Type destType, Type destMemberType, string nameToSearch) + { + var possibleSourceNames = PossibleNames(nameToSearch); + var possibleDestNames = getTypeInfoMembers.GetMemberInfos(typeInfo).Select(mi => new { mi, possibles = PossibleNames(mi.Name) }); + + var all = + from sourceName in possibleSourceNames + from destName in possibleDestNames + select new { sourceName, destName }; + var match = + all.FirstOrDefault( + pair => pair.destName.possibles.Any(p => string.Compare(p, pair.sourceName, StringComparison.OrdinalIgnoreCase) == 0)); + + return match?.destName.mi; + } + + private IEnumerable PossibleNames(string nameToSearch) + { + return + MemberNameReplacers.Select(r => nameToSearch.Replace(r.OriginalValue, r.NewValue)) + .Concat(new[] { MemberNameReplacers.Aggregate(nameToSearch, (s, r) => s.Replace(r.OriginalValue, r.NewValue)), nameToSearch }) + .ToList(); + } + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Configuration/Conventions/SourceToDestinationMapperAttribute.cs b/tools/AutoMapper/Configuration/Conventions/SourceToDestinationMapperAttribute.cs new file mode 100644 index 0000000000..dc2b11f23d --- /dev/null +++ b/tools/AutoMapper/Configuration/Conventions/SourceToDestinationMapperAttribute.cs @@ -0,0 +1,10 @@ +using System; +using System.Reflection; + +namespace AutoMapper.Configuration.Conventions +{ + public abstract class SourceToDestinationMapperAttribute : Attribute + { + public abstract bool IsMatch(TypeDetails typeInfo, MemberInfo memberInfo, Type destType, Type destMemberType, string nameToSearch); + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Configuration/Conventions/SourceToDestinationNameMapperAttributesMember.cs b/tools/AutoMapper/Configuration/Conventions/SourceToDestinationNameMapperAttributesMember.cs new file mode 100644 index 0000000000..5fb68326bd --- /dev/null +++ b/tools/AutoMapper/Configuration/Conventions/SourceToDestinationNameMapperAttributesMember.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace AutoMapper.Configuration.Conventions +{ + public class SourceToDestinationNameMapperAttributesMember : ISourceToDestinationNameMapper + { + private static readonly SourceMember[] Empty = new SourceMember[0]; + private readonly Dictionary _allSourceMembers = new Dictionary(); + + public MemberInfo GetMatchingMemberInfo(IGetTypeInfoMembers getTypeInfoMembers, TypeDetails typeInfo, Type destType, Type destMemberType, string nameToSearch) + { + if (!_allSourceMembers.TryGetValue(typeInfo, out SourceMember[] sourceMembers)) + { + sourceMembers = getTypeInfoMembers.GetMemberInfos(typeInfo).Select(sourceMember => new SourceMember(sourceMember)).Where(s => s.Attribute != null).ToArray(); + _allSourceMembers[typeInfo] = sourceMembers.Length == 0 ? Empty : sourceMembers; + } + return sourceMembers.FirstOrDefault(d => d.Attribute.IsMatch(typeInfo, d.Member, destType, destMemberType, nameToSearch)).Member; + } + + struct SourceMember + { + public SourceMember(MemberInfo sourceMember) + { + Member = sourceMember; + Attribute = sourceMember.GetCustomAttribute(inherit:true); + } + + public MemberInfo Member { get; } + public SourceToDestinationMapperAttribute Attribute { get; } + } + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Configuration/IConfiguration.cs b/tools/AutoMapper/Configuration/IConfiguration.cs new file mode 100644 index 0000000000..74148f5f96 --- /dev/null +++ b/tools/AutoMapper/Configuration/IConfiguration.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; + +namespace AutoMapper.Configuration +{ + public interface IConfiguration : IProfileConfiguration + { + Func ServiceCtor { get; } + IEnumerable Profiles { get; } + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Configuration/IProfileConfiguration.cs b/tools/AutoMapper/Configuration/IProfileConfiguration.cs new file mode 100644 index 0000000000..10ce0e66d2 --- /dev/null +++ b/tools/AutoMapper/Configuration/IProfileConfiguration.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using AutoMapper.Configuration.Conventions; +using AutoMapper.Mappers; + +namespace AutoMapper.Configuration +{ + /// + /// Contains profile-specific configuration + /// + public interface IProfileConfiguration + { + IEnumerable MemberConfigurations { get; } + IEnumerable TypeConfigurations { get; } + bool? ConstructorMappingEnabled { get; } + bool? AllowNullDestinationValues { get; } + bool? AllowNullCollections { get; } + bool? EnableNullPropagationForQueryMapping { get; } + bool? CreateMissingTypeMaps { get; } + bool? ValidateInlineMaps { get; } + IEnumerable> AllTypeMapActions { get; } + IEnumerable> AllPropertyMapActions { get; } + + /// + /// Source extension methods included for search + /// + IEnumerable SourceExtensionMethods { get; } + + /// + /// Specify which properties should be mapped. + /// By default only public properties are mapped.e + /// + Func ShouldMapProperty { get; } + + /// + /// Specify which fields should be mapped. + /// By default only public fields are mapped. + /// + Func ShouldMapField { get; } + + string ProfileName { get; } + IEnumerable GlobalIgnores { get; } + INamingConvention SourceMemberNamingConvention { get; } + INamingConvention DestinationMemberNamingConvention { get; } + IEnumerable TypeMapConfigs { get; } + IEnumerable OpenTypeMapConfigs { get; } + IEnumerable ValueTransformers { get; } + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Configuration/IPropertyMapConfiguration.cs b/tools/AutoMapper/Configuration/IPropertyMapConfiguration.cs new file mode 100644 index 0000000000..6951006714 --- /dev/null +++ b/tools/AutoMapper/Configuration/IPropertyMapConfiguration.cs @@ -0,0 +1,11 @@ +using System.Reflection; + +namespace AutoMapper.Configuration +{ + public interface IPropertyMapConfiguration + { + void Configure(TypeMap typeMap); + MemberInfo DestinationMember { get; } + IPropertyMapConfiguration Reverse(); + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Configuration/ITypeMapConfiguration.cs b/tools/AutoMapper/Configuration/ITypeMapConfiguration.cs new file mode 100644 index 0000000000..bb03c14a95 --- /dev/null +++ b/tools/AutoMapper/Configuration/ITypeMapConfiguration.cs @@ -0,0 +1,14 @@ +using System; + +namespace AutoMapper.Configuration +{ + public interface ITypeMapConfiguration + { + void Configure(TypeMap typeMap); + Type SourceType { get; } + Type DestinationType { get; } + bool IsOpenGeneric { get; } + TypePair Types { get; } + ITypeMapConfiguration ReverseTypeMap { get; } + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Configuration/Internal/PrimitiveHelper.cs b/tools/AutoMapper/Configuration/Internal/PrimitiveHelper.cs new file mode 100644 index 0000000000..1756554764 --- /dev/null +++ b/tools/AutoMapper/Configuration/Internal/PrimitiveHelper.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace AutoMapper.Configuration.Internal +{ + public static class PrimitiveHelper + { + public static TValue GetOrDefault(IDictionary dictionary, TKey key) + { + dictionary.TryGetValue(key, out TValue value); + return value; + } + + private static IEnumerable GetAllMembers(this Type type) => + type.GetTypeInheritance().Concat(type.GetTypeInfo().ImplementedInterfaces).SelectMany(i => i.GetDeclaredMembers()); + + public static MemberInfo GetInheritedMember(this Type type, string name) => type.GetAllMembers().FirstOrDefault(mi => mi.Name == name); + + public static MethodInfo GetInheritedMethod(Type type, string name) + => type.GetInheritedMember(name) as MethodInfo ?? throw new ArgumentOutOfRangeException(nameof(name), $"Cannot find method {name} of type {type}."); + + public static MemberInfo GetFieldOrProperty(Type type, string name) + => type.GetInheritedMember(name) ?? throw new ArgumentOutOfRangeException(nameof(name), $"Cannot find member {name} of type {type}."); + + public static bool IsNullableType(Type type) + => type.IsGenericType(typeof(Nullable<>)); + + public static Type GetTypeOfNullable(Type type) + => type.GetTypeInfo().GenericTypeArguments[0]; + + public static bool IsCollectionType(Type type) + => type.ImplementsGenericInterface(typeof(ICollection<>)); + + public static bool IsEnumerableType(Type type) + => typeof(IEnumerable).IsAssignableFrom(type); + + public static bool IsQueryableType(Type type) + => typeof(IQueryable).IsAssignableFrom(type); + + public static bool IsListType(Type type) + => typeof(IList).IsAssignableFrom(type); + + public static bool IsListOrDictionaryType(Type type) + => type.IsListType() || type.IsDictionaryType(); + + public static bool IsDictionaryType(Type type) + => type.ImplementsGenericInterface(typeof(IDictionary<,>)); + + public static bool ImplementsGenericInterface(Type type, Type interfaceType) + { + return type.IsGenericType(interfaceType) + || type.GetTypeInfo().ImplementedInterfaces.Any(@interface => @interface.IsGenericType(interfaceType)); + } + + public static bool IsGenericType(Type type, Type genericType) + => type.IsGenericType() && type.GetGenericTypeDefinition() == genericType; + + public static Type GetIEnumerableType(Type type) + => type.GetGenericInterface(typeof(IEnumerable<>)); + + public static Type GetDictionaryType(Type type) + => type.GetGenericInterface(typeof(IDictionary<,>)); + + public static Type GetGenericInterface(Type type, Type genericInterface) + { + return type.IsGenericType(genericInterface) + ? type + : type.GetTypeInfo().ImplementedInterfaces.FirstOrDefault(t => t.IsGenericType(genericInterface)); + } + + public static Type GetGenericElementType(Type type) + => type.HasElementType ? type.GetElementType() : type.GetTypeInfo().GenericTypeArguments[0]; + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Configuration/MapperConfigurationExpression.cs b/tools/AutoMapper/Configuration/MapperConfigurationExpression.cs new file mode 100644 index 0000000000..ebab503594 --- /dev/null +++ b/tools/AutoMapper/Configuration/MapperConfigurationExpression.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using AutoMapper.Mappers; + +namespace AutoMapper.Configuration +{ + public class MapperConfigurationExpression : Profile, IMapperConfigurationExpression, IConfiguration + { + private readonly IList _profiles = new List(); + + public MapperConfigurationExpression() : base("") + { + IncludeSourceExtensionMethods(typeof(Enumerable)); + + Mappers = MapperRegistry.Mappers(); + } + + public IEnumerable Profiles => _profiles; + public Func ServiceCtor { get; private set; } = Activator.CreateInstance; + + public void CreateProfile(string profileName, Action config) + => AddProfile(new NamedProfile(profileName, config)); + + public IList Mappers { get; } + + public AdvancedConfiguration Advanced { get; } = new AdvancedConfiguration(); + + private class NamedProfile : Profile + { + public NamedProfile(string profileName, Action config) : base(profileName, config) + { + } + } + + public void AddProfile(Profile profile) + { + _profiles.Add(profile); + } + + public void AddProfile() where TProfile : Profile, new() => AddProfile(new TProfile()); + + public void AddProfile(Type profileType) => AddProfile((Profile)Activator.CreateInstance(profileType)); + + public void AddProfiles(IEnumerable assembliesToScan) + => AddProfilesCore(assembliesToScan); + + public void AddProfiles(params Assembly[] assembliesToScan) + => AddProfilesCore(assembliesToScan); + + public void AddProfiles(IEnumerable assemblyNamesToScan) + => AddProfilesCore(assemblyNamesToScan.Select(name => Assembly.Load(new AssemblyName(name)))); + + public void AddProfiles(params string[] assemblyNamesToScan) + => AddProfilesCore(assemblyNamesToScan.Select(name => Assembly.Load(new AssemblyName(name)))); + + public void AddProfiles(IEnumerable typesFromAssembliesContainingProfiles) + => AddProfilesCore(typesFromAssembliesContainingProfiles.Select(t => t.GetTypeInfo().Assembly)); + + public void AddProfiles(params Type[] typesFromAssembliesContainingProfiles) + => AddProfilesCore(typesFromAssembliesContainingProfiles.Select(t => t.GetTypeInfo().Assembly)); + + private void AddProfilesCore(IEnumerable assembliesToScan) + { + var allTypes = assembliesToScan.Where(a => !a.IsDynamic).SelectMany(a => a.GetDefinedTypes()).ToArray(); + + var profiles = + allTypes + .Where(t => typeof(Profile).GetTypeInfo().IsAssignableFrom(t)) + .Where(t => !t.IsAbstract) + .Select(t => t.AsType()); + + foreach (var profile in profiles) + { + AddProfile(profile); + } + + } + + + public void ConstructServicesUsing(Func constructor) => ServiceCtor = constructor; + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Configuration/MappingExpression.cs b/tools/AutoMapper/Configuration/MappingExpression.cs new file mode 100644 index 0000000000..e9f4141b11 --- /dev/null +++ b/tools/AutoMapper/Configuration/MappingExpression.cs @@ -0,0 +1,651 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using AutoMapper.Internal; +using AutoMapper.Configuration.Internal; + +namespace AutoMapper.Configuration +{ + using static Expression; + + public class MappingExpression : MappingExpression, IMappingExpression + { + public MappingExpression(TypePair types, MemberList memberList) : base(memberList, types) + { + } + + public new IMappingExpression ReverseMap() => (IMappingExpression) base.ReverseMap(); + + public IMappingExpression Substitute(Func substituteFunc) + => (IMappingExpression) base.Substitute(substituteFunc); + + public new IMappingExpression ConstructUsingServiceLocator() + => (IMappingExpression)base.ConstructUsingServiceLocator(); + + public void ForAllMembers(Action memberOptions) + => base.ForAllMembers(opts => memberOptions((IMemberConfigurationExpression)opts)); + + void IMappingExpression.ConvertUsing() + => ConvertUsing(typeof(TTypeConverter)); + + public void ConvertUsing(Type typeConverterType) + => TypeMapActions.Add(tm => tm.TypeConverterType = typeConverterType); + + public void ForAllOtherMembers(Action memberOptions) + => base.ForAllOtherMembers(o => memberOptions((IMemberConfigurationExpression)o)); + + public IMappingExpression ForMember(string name, Action memberOptions) + => (IMappingExpression)base.ForMember(name, c => memberOptions((IMemberConfigurationExpression)c)); + + public new IMappingExpression ForSourceMember(string sourceMemberName, Action memberOptions) + => (IMappingExpression)base.ForSourceMember(sourceMemberName, memberOptions); + + public new IMappingExpression Include(Type otherSourceType, Type otherDestinationType) + => (IMappingExpression)base.Include(otherSourceType, otherDestinationType); + + public new IMappingExpression IgnoreAllPropertiesWithAnInaccessibleSetter() + => (IMappingExpression)base.IgnoreAllPropertiesWithAnInaccessibleSetter(); + + public new IMappingExpression IgnoreAllSourcePropertiesWithAnInaccessibleSetter() + => (IMappingExpression)base.IgnoreAllSourcePropertiesWithAnInaccessibleSetter(); + + public new IMappingExpression IncludeBase(Type sourceBase, Type destinationBase) + => (IMappingExpression)base.IncludeBase(sourceBase, destinationBase); + + public new IMappingExpression BeforeMap(Action beforeFunction) + => (IMappingExpression)base.BeforeMap(beforeFunction); + + public new IMappingExpression BeforeMap() where TMappingAction : IMappingAction + => (IMappingExpression)base.BeforeMap(); + + public new IMappingExpression AfterMap(Action afterFunction) + => (IMappingExpression)base.AfterMap(afterFunction); + + public new IMappingExpression AfterMap() where TMappingAction : IMappingAction + => (IMappingExpression)base.AfterMap(); + + public new IMappingExpression ConstructUsing(Func ctor) + => (IMappingExpression)base.ConstructUsing(ctor); + + public new IMappingExpression ConstructUsing(Func ctor) + => (IMappingExpression)base.ConstructUsing(ctor); + + public IMappingExpression ConstructProjectionUsing(LambdaExpression ctor) + { + TypeMapActions.Add(tm => tm.ConstructExpression = ctor); + + return this; + } + + public new IMappingExpression ValidateMemberList(MemberList memberList) + => (IMappingExpression) base.ValidateMemberList(memberList); + + public new IMappingExpression MaxDepth(int depth) + => (IMappingExpression)base.MaxDepth(depth); + + public new IMappingExpression ForCtorParam(string ctorParamName, Action> paramOptions) + => (IMappingExpression)base.ForCtorParam(ctorParamName, paramOptions); + + public new IMappingExpression PreserveReferences() => (IMappingExpression)base.PreserveReferences(); + + protected override IPropertyMapConfiguration CreateMemberConfigurationExpression(MemberInfo member, Type sourceType) + => new MemberConfigurationExpression(member, sourceType); + + protected override MappingExpression CreateReverseMapExpression() + => new MappingExpression(new TypePair(DestinationType, SourceType), MemberList.Source); + + internal class MemberConfigurationExpression : MemberConfigurationExpression, IMemberConfigurationExpression + { + public MemberConfigurationExpression(MemberInfo destinationMember, Type sourceType) + : base(destinationMember, sourceType) + { + } + + public void ResolveUsing(Type valueResolverType) + { + var config = new ValueResolverConfiguration(valueResolverType, valueResolverType.GetGenericInterface(typeof(IValueResolver<,,>))); + + PropertyMapActions.Add(pm => pm.ValueResolverConfig = config); + } + + public void ResolveUsing(Type valueResolverType, string memberName) + { + var config = new ValueResolverConfiguration(valueResolverType, valueResolverType.GetGenericInterface(typeof(IMemberValueResolver<,,,>))) + { + SourceMemberName = memberName + }; + + PropertyMapActions.Add(pm => pm.ValueResolverConfig = config); + } + + public void ResolveUsing(IMemberValueResolver resolver, string memberName) + { + var config = new ValueResolverConfiguration(resolver, typeof(IMemberValueResolver)) + { + SourceMemberName = memberName + }; + + PropertyMapActions.Add(pm => pm.ValueResolverConfig = config); + } + } + + } + + public class MappingExpression : IMappingExpression, ITypeMapConfiguration + { + private readonly List _memberConfigurations = new List(); + private readonly List _sourceMemberConfigurations = new List(); + private readonly List> _ctorParamConfigurations = new List>(); + private readonly List _valueTransformers = new List(); + private MappingExpression _reverseMap; + private Action> _allMemberOptions; + private Func _memberFilter; + + public MappingExpression(MemberList memberList) + : this(memberList, typeof(TSource), typeof(TDestination)) + { + } + + public MappingExpression(MemberList memberList, Type sourceType, Type destinationType) + : this(memberList, new TypePair(sourceType, destinationType)) + { + } + + public MappingExpression(MemberList memberList, TypePair types) + { + Types = types; + IsOpenGeneric = types.SourceType.IsGenericTypeDefinition() || types.DestinationType.IsGenericTypeDefinition(); + TypeMapActions.Add(tm => tm.ConfiguredMemberList = memberList); + } + + public TypePair Types { get; } + public Type SourceType => Types.SourceType; + public Type DestinationType => Types.DestinationType; + public bool IsOpenGeneric { get; } + public ITypeMapConfiguration ReverseTypeMap => _reverseMap; + public IList ValueTransformers => _valueTransformers; + protected List> TypeMapActions { get; } = new List>(); + + public IMappingExpression PreserveReferences() + { + TypeMapActions.Add(tm => tm.PreserveReferences = true); + + return this; + } + + protected virtual IPropertyMapConfiguration CreateMemberConfigurationExpression(MemberInfo member, Type sourceType) + => new MemberConfigurationExpression(member, sourceType); + + protected virtual MappingExpression CreateReverseMapExpression() + => new MappingExpression(MemberList.None, DestinationType, SourceType); + + public IMappingExpression ForPath(Expression> destinationMember, + Action> memberOptions) + { + if(!destinationMember.IsMemberPath()) + { + throw new ArgumentOutOfRangeException(nameof(destinationMember), "Only member accesses are allowed."); + } + var expression = new PathConfigurationExpression(destinationMember); + var firstMember = expression.MemberPath.First; + var firstMemberConfig = GetDestinationMemberConfiguration(firstMember); + if(firstMemberConfig == null) + { + IgnoreDestinationMember(firstMember, ignorePaths: false); + } + _memberConfigurations.Add(expression); + memberOptions(expression); + return this; + } + + public IMappingExpression ForMember(Expression> destinationMember, + Action> memberOptions) + { + var memberInfo = ReflectionHelper.FindProperty(destinationMember); + return ForDestinationMember(memberInfo, memberOptions); + } + + public IMappingExpression ForMember(string name, + Action> memberOptions) + { + var member = DestinationType.GetFieldOrProperty(name); + return ForDestinationMember(member, memberOptions); + } + + public void ForAllOtherMembers(Action> memberOptions) + { + _allMemberOptions = memberOptions; + _memberFilter = m => GetDestinationMemberConfiguration(m) == null; + } + + public void ForAllMembers(Action> memberOptions) + { + _allMemberOptions = memberOptions; + _memberFilter = _ => true; + } + + public IMappingExpression IgnoreAllPropertiesWithAnInaccessibleSetter() + { + foreach(var property in DestinationType.PropertiesWithAnInaccessibleSetter()) + { + IgnoreDestinationMember(property); + } + return this; + } + + private void IgnoreDestinationMember(MemberInfo property, bool ignorePaths = true) => + ForDestinationMember(property, options => options.Ignore(ignorePaths)); + + public IMappingExpression IgnoreAllSourcePropertiesWithAnInaccessibleSetter() + { + foreach(var property in SourceType.PropertiesWithAnInaccessibleSetter()) + { + ForSourceMember(property.Name, options => options.Ignore()); + } + return this; + } + + public IMappingExpression Include() + where TOtherSource : TSource + where TOtherDestination : TDestination + => IncludeCore(typeof(TOtherSource), typeof(TOtherDestination)); + + public IMappingExpression Include(Type otherSourceType, Type otherDestinationType) + { + CheckIsDerived(otherSourceType, SourceType); + CheckIsDerived(otherDestinationType, DestinationType); + return IncludeCore(otherSourceType, otherDestinationType); + } + + IMappingExpression IncludeCore(Type otherSourceType, Type otherDestinationType) + { + TypeMapActions.Add(tm => tm.IncludeDerivedTypes(otherSourceType, otherDestinationType)); + return this; + } + + public IMappingExpression IncludeBase() + => IncludeBase(typeof(TSourceBase), typeof(TDestinationBase)); + + public IMappingExpression IncludeBase(Type sourceBase, Type destinationBase) + { + CheckIsDerived(SourceType, sourceBase); + CheckIsDerived(DestinationType, destinationBase); + TypeMapActions.Add(tm => tm.IncludeBaseTypes(sourceBase, destinationBase)); + return this; + } + + public void ProjectUsing(Expression> projectionExpression) + { + TypeMapActions.Add(tm => tm.CustomProjection = projectionExpression); + } + + public IMappingExpression MaxDepth(int depth) + { + TypeMapActions.Add(tm => tm.MaxDepth = depth); + + return this; + } + + public IMappingExpression ConstructUsingServiceLocator() + { + TypeMapActions.Add(tm => tm.ConstructDestinationUsingServiceLocator = true); + + return this; + } + + public IMappingExpression ReverseMap() + { + _reverseMap = CreateReverseMapExpression(); + _reverseMap._memberConfigurations.AddRange(_memberConfigurations.Select(m => m.Reverse()).Where(m=>m!=null)); + return _reverseMap; + } + + public IMappingExpression ForSourceMember(Expression> sourceMember, Action memberOptions) + { + var memberInfo = ReflectionHelper.FindProperty(sourceMember); + + var srcConfig = new SourceMappingExpression(memberInfo); + + memberOptions(srcConfig); + + _sourceMemberConfigurations.Add(srcConfig); + + return this; + } + + public IMappingExpression ForSourceMember(string sourceMemberName, Action memberOptions) + { + var memberInfo = SourceType.GetFieldOrProperty(sourceMemberName); + + var srcConfig = new SourceMappingExpression(memberInfo); + + memberOptions(srcConfig); + + _sourceMemberConfigurations.Add(srcConfig); + + return this; + } + + public IMappingExpression Substitute(Func substituteFunc) + { + TypeMapActions.Add(tm => + { + Expression> expr = (src, dest, ctxt) => substituteFunc(src); + + tm.Substitution = expr; + }); + + return this; + } + + public void ConvertUsing(Func mappingFunction) + { + TypeMapActions.Add(tm => + { + Expression> expr = + (src, dest, ctxt) => mappingFunction(src); + + tm.CustomMapper = expr; + }); + } + + public void ConvertUsing(Func mappingFunction) + { + TypeMapActions.Add(tm => + { + Expression> expr = + (src, dest, ctxt) => mappingFunction(src, dest); + + tm.CustomMapper = expr; + }); + } + + public void ConvertUsing(Func mappingFunction) + { + TypeMapActions.Add(tm => + { + Expression> expr = + (src, dest, ctxt) => mappingFunction(src, dest, ctxt); + + tm.CustomMapper = expr; + }); + } + + public void ConvertUsing(ITypeConverter converter) + { + ConvertUsing(converter.Convert); + } + + public void ConvertUsing() where TTypeConverter : ITypeConverter + { + TypeMapActions.Add(tm => tm.TypeConverterType = typeof (TTypeConverter)); + } + + public IMappingExpression BeforeMap(Action beforeFunction) + { + TypeMapActions.Add(tm => + { + Expression> expr = + (src, dest, ctxt) => beforeFunction(src, dest); + + tm.AddBeforeMapAction(expr); + }); + + return this; + } + + public IMappingExpression BeforeMap(Action beforeFunction) + { + TypeMapActions.Add(tm => + { + Expression> expr = + (src, dest, ctxt) => beforeFunction(src, dest, ctxt); + + tm.AddBeforeMapAction(expr); + }); + + return this; + } + + public IMappingExpression BeforeMap() where TMappingAction : IMappingAction + { + void BeforeFunction(TSource src, TDestination dest, ResolutionContext ctxt) + => ((TMappingAction) ctxt.Options.ServiceCtor(typeof(TMappingAction))).Process(src, dest); + + return BeforeMap(BeforeFunction); + } + + public IMappingExpression AfterMap(Action afterFunction) + { + TypeMapActions.Add(tm => + { + Expression> expr = + (src, dest, ctxt) => afterFunction(src, dest); + + tm.AddAfterMapAction(expr); + }); + + return this; + } + + public IMappingExpression AfterMap(Action afterFunction) + { + TypeMapActions.Add(tm => + { + Expression> expr = + (src, dest, ctxt) => afterFunction(src, dest, ctxt); + + tm.AddAfterMapAction(expr); + }); + + return this; + } + + public IMappingExpression AfterMap() where TMappingAction : IMappingAction + { + void AfterFunction(TSource src, TDestination dest, ResolutionContext ctxt) + => ((TMappingAction) ctxt.Options.ServiceCtor(typeof(TMappingAction))).Process(src, dest); + + return AfterMap(AfterFunction); + } + + public IMappingExpression ConstructUsing(Func ctor) + { + TypeMapActions.Add(tm => + { + Expression> expr = (src, ctxt) => ctor(src); + + tm.DestinationCtor = expr; + }); + + return this; + } + + public IMappingExpression ConstructUsing(Func ctor) + { + TypeMapActions.Add(tm => + { + Expression> expr = (src, ctxt) => ctor(src, ctxt); + + tm.DestinationCtor = expr; + }); + + return this; + } + + public IMappingExpression ConstructProjectionUsing(Expression> ctor) + { + TypeMapActions.Add(tm => + { + tm.ConstructExpression = ctor; + + var ctxtParam = Parameter(typeof (ResolutionContext), "ctxt"); + var srcParam = Parameter(typeof (TSource), "src"); + + var body = ctor.ReplaceParameters(srcParam); + + tm.DestinationCtor = Lambda(body, srcParam, ctxtParam); + }); + + return this; + } + + private IMappingExpression ForDestinationMember(MemberInfo destinationProperty, Action> memberOptions) + { + var expression = (MemberConfigurationExpression) CreateMemberConfigurationExpression(destinationProperty, SourceType); + + _memberConfigurations.Add(expression); + + memberOptions(expression); + + return this; + } + + public void As() where T : TDestination => As(typeof(T)); + + public void As(Type typeOverride) + { + CheckIsDerived(typeOverride, DestinationType); + TypeMapActions.Add(tm => tm.DestinationTypeOverride = typeOverride); + } + + private void CheckIsDerived(Type derivedType, Type baseType) + { + if(!baseType.IsAssignableFrom(derivedType) && !derivedType.IsGenericTypeDefinition() && !baseType.IsGenericTypeDefinition()) + { + throw new ArgumentOutOfRangeException(nameof(derivedType), $"{derivedType} is not derived from {baseType}."); + } + } + + public IMappingExpression ForCtorParam(string ctorParamName, Action> paramOptions) + { + var ctorParamExpression = new CtorParamConfigurationExpression(ctorParamName); + + paramOptions(ctorParamExpression); + + _ctorParamConfigurations.Add(ctorParamExpression); + + return this; + } + + public IMappingExpression DisableCtorValidation() + { + TypeMapActions.Add(tm => + { + tm.DisableConstructorValidation = true; + }); + + return this; + } + + public IMappingExpression AddTransform(Expression> transformer) + { + var config = new ValueTransformerConfiguration(typeof(TValue), transformer); + + _valueTransformers.Add(config); + + return this; + } + + public IMappingExpression ValidateMemberList(MemberList memberList) + { + TypeMapActions.Add(tm => + { + tm.ConfiguredMemberList = memberList; + }); + return this; + } + + private IPropertyMapConfiguration GetDestinationMemberConfiguration(MemberInfo destinationMember) => + _memberConfigurations.FirstOrDefault(m => m.DestinationMember == destinationMember); + + public void Configure(TypeMap typeMap) + { + foreach (var destProperty in typeMap.DestinationTypeDetails.PublicWriteAccessors) + { + var attrs = destProperty.GetCustomAttributes(true); + if (attrs.Any(x => x is IgnoreMapAttribute)) + { + IgnoreDestinationMember(destProperty); + var sourceProperty = typeMap.SourceType.GetInheritedMember(destProperty.Name); + if (sourceProperty != null) + { + _reverseMap?.IgnoreDestinationMember(sourceProperty); + } + } + if (typeMap.Profile.GlobalIgnores.Contains(destProperty.Name) && GetDestinationMemberConfiguration(destProperty) == null) + { + IgnoreDestinationMember(destProperty); + } + } + + if (_allMemberOptions != null) + { + foreach (var accessor in typeMap.DestinationTypeDetails.PublicReadAccessors.Where(_memberFilter)) + { + ForDestinationMember(accessor, _allMemberOptions); + } + } + + foreach (var action in TypeMapActions) + { + action(typeMap); + } + foreach (var memberConfig in _memberConfigurations) + { + memberConfig.Configure(typeMap); + } + foreach (var memberConfig in _sourceMemberConfigurations) + { + memberConfig.Configure(typeMap); + } + foreach (var paramConfig in _ctorParamConfigurations) + { + paramConfig.Configure(typeMap); + } + foreach (var valueTransformer in _valueTransformers) + { + typeMap.AddValueTransformation(valueTransformer); + } + + if (_reverseMap != null) + { + ReverseSourceMembers(typeMap); + foreach(var destProperty in typeMap.GetPropertyMaps().Where(pm => pm.Ignored)) + { + _reverseMap.ForSourceMember(destProperty.DestinationProperty.Name, opt => opt.Ignore()); + } + foreach(var includedDerivedType in typeMap.IncludedDerivedTypes) + { + _reverseMap.Include(includedDerivedType.DestinationType, includedDerivedType.SourceType); + } + foreach(var includedBaseType in typeMap.IncludedBaseTypes) + { + _reverseMap.IncludeBase(includedBaseType.DestinationType, includedBaseType.SourceType); + } + } + } + + private void ReverseSourceMembers(TypeMap typeMap) + { + foreach(var propertyMap in typeMap.GetPropertyMaps().Where(p => p.SourceMembers.Count > 1 && !p.SourceMembers.Any(s=>s is MethodInfo))) + { + _reverseMap.TypeMapActions.Add(reverseTypeMap => + { + var memberPath = new MemberPath(propertyMap.SourceMembers); + var newDestination = Parameter(reverseTypeMap.DestinationType, "destination"); + var path = propertyMap.SourceMembers.MemberAccesses(newDestination); + var forPathLambda = Lambda(path, newDestination); + + var pathMap = reverseTypeMap.FindOrCreatePathMapFor(forPathLambda, memberPath, reverseTypeMap); + + var newSource = Parameter(reverseTypeMap.SourceType, "source"); + pathMap.SourceExpression = Lambda(MakeMemberAccess(newSource, propertyMap.DestinationProperty), newSource); + }); + } + } + } +} + diff --git a/tools/AutoMapper/Configuration/MemberConfigurationExpression.cs b/tools/AutoMapper/Configuration/MemberConfigurationExpression.cs new file mode 100644 index 0000000000..3c6aa10c6a --- /dev/null +++ b/tools/AutoMapper/Configuration/MemberConfigurationExpression.cs @@ -0,0 +1,305 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; + +namespace AutoMapper.Configuration +{ + using static Expression; + + public class MemberConfigurationExpression : IMemberConfigurationExpression, IPropertyMapConfiguration + { + private readonly MemberInfo _destinationMember; + private LambdaExpression _sourceMember; + private readonly Type _sourceType; + protected List> PropertyMapActions { get; } = new List>(); + + public MemberConfigurationExpression(MemberInfo destinationMember, Type sourceType) + { + _destinationMember = destinationMember; + _sourceType = sourceType; + } + + public MemberInfo DestinationMember => _destinationMember; + + public void MapAtRuntime() + { + PropertyMapActions.Add(pm => pm.Inline = false); + } + + public void NullSubstitute(object nullSubstitute) + { + PropertyMapActions.Add(pm => pm.NullSubstitute = nullSubstitute); + } + + public void ResolveUsing() + where TValueResolver : IValueResolver + { + var config = new ValueResolverConfiguration(typeof(TValueResolver), typeof(IValueResolver)); + + PropertyMapActions.Add(pm => pm.ValueResolverConfig = config); + } + + public void ResolveUsing(Expression> sourceMember) + where TValueResolver : IMemberValueResolver + { + var config = new ValueResolverConfiguration(typeof(TValueResolver), typeof(IMemberValueResolver)) + { + SourceMember = sourceMember + }; + + PropertyMapActions.Add(pm => pm.ValueResolverConfig = config); + } + + public void ResolveUsing(string sourceMemberName) + where TValueResolver : IMemberValueResolver + { + var config = new ValueResolverConfiguration(typeof(TValueResolver), typeof(IMemberValueResolver)) + { + SourceMemberName = sourceMemberName + }; + + PropertyMapActions.Add(pm => pm.ValueResolverConfig = config); + } + + public void ResolveUsing(IValueResolver valueResolver) + { + var config = new ValueResolverConfiguration(valueResolver, typeof(IValueResolver)); + + PropertyMapActions.Add(pm => pm.ValueResolverConfig = config); + } + + public void ResolveUsing(IMemberValueResolver valueResolver, Expression> sourceMember) + { + var config = new ValueResolverConfiguration(valueResolver, typeof(IMemberValueResolver)) + { + SourceMember = sourceMember + }; + + PropertyMapActions.Add(pm => pm.ValueResolverConfig = config); + } + + public void ResolveUsing(Func resolver) + { + PropertyMapActions.Add(pm => + { + Expression> expr = (src, dest, destMember, ctxt) => resolver(src); + + pm.CustomResolver = expr; + }); + } + + public void ResolveUsing(Func resolver) + { + PropertyMapActions.Add(pm => + { + Expression> expr = (src, dest, destMember, ctxt) => resolver(src, dest); + + pm.CustomResolver = expr; + }); + } + + public void ResolveUsing(Func resolver) + { + PropertyMapActions.Add(pm => + { + Expression> expr = (src, dest, destMember, ctxt) => resolver(src, dest, destMember); + + pm.CustomResolver = expr; + }); + } + + public void ResolveUsing(Func resolver) + { + PropertyMapActions.Add(pm => + { + Expression> expr = (src, dest, destMember, ctxt) => resolver(src, dest, destMember, ctxt); + + pm.CustomResolver = expr; + }); + } + + public void MapFrom(Expression> sourceMember) + { + MapFromUntyped(sourceMember); + } + + internal void MapFromUntyped(LambdaExpression sourceExpression) + { + _sourceMember = sourceExpression; + PropertyMapActions.Add(pm => pm.MapFrom(sourceExpression)); + } + + public void MapFrom(string sourceMember) + { + _sourceType.GetFieldOrProperty(sourceMember); + PropertyMapActions.Add(pm => pm.CustomSourceMemberName = sourceMember); + } + + public void UseValue(TValue value) + { + MapFrom(s => value); + } + + public void Condition(Func condition) + { + PropertyMapActions.Add(pm => + { + Expression> expr = + (src, dest, srcMember, destMember, ctxt) => condition(src, dest, srcMember, destMember, ctxt); + + pm.Condition = expr; + }); + } + + public void Condition(Func condition) + { + PropertyMapActions.Add(pm => + { + Expression> expr = + (src, dest, srcMember, destMember, ctxt) => condition(src, dest, srcMember, destMember); + + pm.Condition = expr; + }); + } + + public void Condition(Func condition) + { + PropertyMapActions.Add(pm => + { + Expression> expr = + (src, dest, srcMember, destMember, ctxt) => condition(src, dest, srcMember); + + pm.Condition = expr; + }); + } + + public void Condition(Func condition) + { + PropertyMapActions.Add(pm => + { + Expression> expr = + (src, dest, srcMember, destMember, ctxt) => condition(src, dest); + + pm.Condition = expr; + }); + } + + public void Condition(Func condition) + { + PropertyMapActions.Add(pm => + { + Expression> expr = + (src, dest, srcMember, destMember, ctxt) => condition(src); + + pm.Condition = expr; + }); + } + + public void PreCondition(Func condition) + { + PropertyMapActions.Add(pm => + { + Expression> expr = + (src, ctxt) => condition(src); + + pm.PreCondition = expr; + }); + } + + public void PreCondition(Func condition) + { + PropertyMapActions.Add(pm => + { + Expression> expr = + (src, ctxt) => condition(ctxt); + + pm.PreCondition = expr; + }); + } + + public void PreCondition(Func condition) + { + PropertyMapActions.Add(pm => + { + Expression> expr = + (src, ctxt) => condition(src, ctxt); + + pm.PreCondition = expr; + }); + } + + public void AddTransform(Expression> transformer) + { + PropertyMapActions.Add(pm => + { + var config = new ValueTransformerConfiguration(typeof(TMember), transformer); + + pm.AddValueTransformation(config); + }); + } + + public void ExplicitExpansion() + { + PropertyMapActions.Add(pm => pm.ExplicitExpansion = true); + } + + public void Ignore() => Ignore(ignorePaths: true); + + internal void Ignore(bool ignorePaths) => + PropertyMapActions.Add(pm => + { + pm.Ignored = true; + if(ignorePaths) + { + pm.TypeMap.IgnorePaths(DestinationMember); + } + }); + + public void AllowNull() + { + PropertyMapActions.Add(pm => pm.AllowNull = true); + } + + public void UseDestinationValue() + { + PropertyMapActions.Add(pm => pm.UseDestinationValue = true); + } + + public void SetMappingOrder(int mappingOrder) + { + PropertyMapActions.Add(pm => pm.MappingOrder = mappingOrder); + } + + public void Configure(TypeMap typeMap) + { + var destMember = _destinationMember; + + if(destMember.DeclaringType.IsGenericType()) + { + destMember = typeMap.DestinationTypeDetails.PublicReadAccessors.Single(m => m.Name == destMember.Name); + } + + var propertyMap = typeMap.FindOrCreatePropertyMapFor(destMember); + + Apply(propertyMap); + } + + private void Apply(PropertyMap propertyMap) + { + foreach(var action in PropertyMapActions) + { + action(propertyMap); + } + } + + public IPropertyMapConfiguration Reverse() + { + var newSource = Parameter(DestinationMember.DeclaringType, "source"); + var newSourceProperty = MakeMemberAccess(newSource, _destinationMember); + var newSourceExpression = Lambda(newSourceProperty, newSource); + return PathConfigurationExpression.Create(_sourceMember, newSourceExpression); + } + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Configuration/MemberPath.cs b/tools/AutoMapper/Configuration/MemberPath.cs new file mode 100644 index 0000000000..69112aad3c --- /dev/null +++ b/tools/AutoMapper/Configuration/MemberPath.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace AutoMapper.Internal +{ + public struct MemberPath : IEquatable + { + public MemberInfo[] Members { get; } + + public MemberPath(IEnumerable members) + { + Members = members.ToArray(); + } + + public MemberInfo Last => Members[Members.Length - 1]; + + public MemberInfo First => Members[0]; + + public int Length => Members.Length; + + public bool Equals(MemberPath other) => Members.SequenceEqual(other.Members); + + public override bool Equals(object obj) + { + if(ReferenceEquals(null, obj)) return false; + return obj is MemberPath && Equals((MemberPath)obj); + } + + public override int GetHashCode() + { + var hashCode = 0; + foreach(var member in Members) + { + hashCode = HashCodeCombiner.CombineCodes(hashCode, member.GetHashCode()); + } + return hashCode; + } + + public static bool operator==(MemberPath left, MemberPath right) => left.Equals(right); + + public static bool operator!=(MemberPath left, MemberPath right) => !left.Equals(right); + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Configuration/PathConfigurationExpression.cs b/tools/AutoMapper/Configuration/PathConfigurationExpression.cs new file mode 100644 index 0000000000..dd276d6452 --- /dev/null +++ b/tools/AutoMapper/Configuration/PathConfigurationExpression.cs @@ -0,0 +1,94 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Reflection; +using AutoMapper.Internal; + +namespace AutoMapper.Configuration +{ + public class PathConfigurationExpression : IPathConfigurationExpression, IPropertyMapConfiguration + { + private readonly LambdaExpression _destinationExpression; + private LambdaExpression _sourceExpression; + protected List> PathMapActions { get; } = new List>(); + + public PathConfigurationExpression(LambdaExpression destinationExpression) + { + _destinationExpression = destinationExpression; + MemberPath = new MemberPath(MemberVisitor.GetMemberPath(destinationExpression).Reverse()); + } + + public MemberPath MemberPath { get; } + + public MemberInfo DestinationMember => MemberPath.Last; + + public void MapFrom(Expression> sourceExpression) + { + MapFromUntyped(sourceExpression); + } + + public void Ignore() + { + PathMapActions.Add(pm => pm.Ignored = true); + } + + public void MapFromUntyped(LambdaExpression sourceExpression) + { + _sourceExpression = sourceExpression; + PathMapActions.Add(pm => + { + pm.SourceExpression = sourceExpression; + pm.Ignored = false; + }); + } + + public void Configure(TypeMap typeMap) + { + var pathMap = typeMap.FindOrCreatePathMapFor(_destinationExpression, MemberPath, typeMap); + + Apply(pathMap); + } + + private void Apply(PathMap pathMap) + { + foreach(var action in PathMapActions) + { + action(pathMap); + } + } + + internal static IPropertyMapConfiguration Create(LambdaExpression destination, LambdaExpression source) + { + if(destination == null || !destination.IsMemberPath()) + { + return null; + } + var reversed = new PathConfigurationExpression(destination); + if(reversed.MemberPath.Length == 1) + { + var reversedMemberExpression = new MemberConfigurationExpression(reversed.DestinationMember, typeof(TSource)); + reversedMemberExpression.MapFromUntyped(source); + return reversedMemberExpression; + } + reversed.MapFromUntyped(source); + return reversed; + } + + public IPropertyMapConfiguration Reverse() + { + return Create(_sourceExpression, _destinationExpression); + } + + public void Condition(Func, bool> condition) + { + PathMapActions.Add(pm => + { + Expression> expr = + (src, dest, srcMember, destMember, ctxt) => + condition(new ConditionParameters(src, dest, srcMember, destMember, ctxt)); + pm.Condition = expr; + }); + } + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Configuration/PrimitiveExtensions.cs b/tools/AutoMapper/Configuration/PrimitiveExtensions.cs new file mode 100644 index 0000000000..e3cfa0310d --- /dev/null +++ b/tools/AutoMapper/Configuration/PrimitiveExtensions.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using AutoMapper.Configuration.Internal; + +namespace AutoMapper.Configuration +{ + internal static class PrimitiveExtensions + { + public static bool IsSetType(this Type type) + => type.ImplementsGenericInterface(typeof(ISet<>)); + + public static TValue GetOrDefault(this IDictionary dictionary, TKey key) + => PrimitiveHelper.GetOrDefault(dictionary, key); + + public static MethodInfo GetInheritedMethod(this Type type, string name) + => PrimitiveHelper.GetInheritedMethod(type, name); + + public static MemberInfo GetFieldOrProperty(this Type type, string name) + => PrimitiveHelper.GetFieldOrProperty(type, name); + + public static bool IsNullableType(this Type type) + => PrimitiveHelper.IsNullableType(type); + + public static Type GetTypeOfNullable(this Type type) + => PrimitiveHelper.GetTypeOfNullable(type); + + public static bool IsCollectionType(this Type type) + => PrimitiveHelper.IsCollectionType(type); + + public static bool IsEnumerableType(this Type type) + => PrimitiveHelper.IsEnumerableType(type); + + public static bool IsQueryableType(this Type type) + => PrimitiveHelper.IsQueryableType(type); + + public static bool IsListType(this Type type) + => PrimitiveHelper.IsListType(type); + + public static bool IsListOrDictionaryType(this Type type) + => PrimitiveHelper.IsListOrDictionaryType(type); + + public static bool IsDictionaryType(this Type type) + => PrimitiveHelper.IsDictionaryType(type); + + public static bool ImplementsGenericInterface(this Type type, Type interfaceType) + => PrimitiveHelper.ImplementsGenericInterface(type, interfaceType); + + public static bool IsGenericType(this Type type, Type genericType) + => PrimitiveHelper.IsGenericType(type, genericType); + + public static Type GetIEnumerableType(this Type type) + => PrimitiveHelper.GetIEnumerableType(type); + + public static Type GetDictionaryType(this Type type) + => PrimitiveHelper.GetDictionaryType(type); + + public static Type GetGenericInterface(this Type type, Type genericInterface) + => PrimitiveHelper.GetGenericInterface(type, genericInterface); + + public static Type GetGenericElementType(this Type type) + => PrimitiveHelper.GetGenericElementType(type); + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Configuration/ResolutionExpression.cs b/tools/AutoMapper/Configuration/ResolutionExpression.cs new file mode 100644 index 0000000000..861144dfc7 --- /dev/null +++ b/tools/AutoMapper/Configuration/ResolutionExpression.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; + +namespace AutoMapper.Configuration +{ + public class ResolutionExpression : IResolverConfigurationExpression, IResolutionExpression + { + private readonly ValueResolverConfiguration _config; + private readonly List> _propertyMapActions = new List>(); + + public ResolutionExpression(ValueResolverConfiguration config) + { + _config = config; + + _propertyMapActions.Add(pm => pm.ValueResolverConfig = _config); + } + + public void FromMember(Expression> sourceMember) + { + _config.SourceMember = sourceMember; + } + + public void FromMember(string sourcePropertyName) + { + _config.SourceMemberName = sourcePropertyName; + } + + public void Configure(PropertyMap propertyMap) + { + foreach (var action in _propertyMapActions) + { + action(propertyMap); + } + } + } + + public class ResolutionExpression : ResolutionExpression + { + public ResolutionExpression(ValueResolverConfiguration config) : base(config) + { + } + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Configuration/SourceMappingExpression.cs b/tools/AutoMapper/Configuration/SourceMappingExpression.cs new file mode 100644 index 0000000000..7fcd055549 --- /dev/null +++ b/tools/AutoMapper/Configuration/SourceMappingExpression.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace AutoMapper.Configuration +{ + public class SourceMappingExpression : ISourceMemberConfigurationExpression + { + private readonly MemberInfo _sourceMember; + private readonly List> _sourceMemberActions = new List>(); + + public SourceMappingExpression(MemberInfo sourceMember) => _sourceMember = sourceMember; + + public void Ignore() => _sourceMemberActions.Add(smc => smc.Ignore()); + + public void Configure(TypeMap typeMap) + { + var sourcePropertyConfig = typeMap.FindOrCreateSourceMemberConfigFor(_sourceMember); + + foreach (var action in _sourceMemberActions) + { + action(sourcePropertyConfig); + } + } + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Configuration/SourceMemberConfig.cs b/tools/AutoMapper/Configuration/SourceMemberConfig.cs new file mode 100644 index 0000000000..5b1d29f8c9 --- /dev/null +++ b/tools/AutoMapper/Configuration/SourceMemberConfig.cs @@ -0,0 +1,20 @@ +using System.Reflection; + +namespace AutoMapper.Configuration +{ + /// + /// Contains member configuration relating to source members + /// + public class SourceMemberConfig + { + private bool _ignored; + + public SourceMemberConfig(MemberInfo sourceMember) => SourceMember = sourceMember; + + public MemberInfo SourceMember { get; } + + public void Ignore() => _ignored = true; + + public bool IsIgnored() => _ignored; + } +} \ No newline at end of file diff --git a/tools/AutoMapper/ConfigurationValidator.cs b/tools/AutoMapper/ConfigurationValidator.cs new file mode 100644 index 0000000000..e5bd13e090 --- /dev/null +++ b/tools/AutoMapper/ConfigurationValidator.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using AutoMapper.Configuration; + +namespace AutoMapper +{ + public class ConfigurationValidator + { + private readonly IConfigurationProvider _config; + + public ConfigurationValidator(IConfigurationProvider config) => _config = config; + + public void AssertConfigurationIsValid(IEnumerable typeMaps) + { + var maps = typeMaps as TypeMap[] ?? typeMaps.ToArray(); + var badTypeMaps = + (from typeMap in maps + where typeMap.ShouldCheckForValid() + let unmappedPropertyNames = typeMap.GetUnmappedPropertyNames() + let canConstruct = typeMap.PassesCtorValidation() + where unmappedPropertyNames.Length > 0 || !canConstruct + select new AutoMapperConfigurationException.TypeMapConfigErrors(typeMap, unmappedPropertyNames, canConstruct) + ).ToArray(); + + if (badTypeMaps.Any()) + { + throw new AutoMapperConfigurationException(badTypeMaps); + } + + var typeMapsChecked = new List(); + var configExceptions = new List(); + + foreach (var typeMap in maps) + { + try + { + DryRunTypeMap(typeMapsChecked, typeMap.Types, typeMap, null); + } + catch (Exception e) + { + configExceptions.Add(e); + } + } + + if (configExceptions.Count > 1) + { + throw new AggregateException(configExceptions); + } + if (configExceptions.Count > 0) + { + throw configExceptions[0]; + } + } + + private void DryRunTypeMap(ICollection typeMapsChecked, TypePair types, TypeMap typeMap, PropertyMap propertyMap) + { + if(typeMap == null) + { + typeMap = _config.ResolveTypeMap(types.SourceType, types.DestinationType); + } + if (typeMap != null) + { + if(typeMapsChecked.Contains(typeMap)) + { + return; + } + typeMapsChecked.Add(typeMap); + if(typeMap.CustomMapper != null || typeMap.TypeConverterType != null) + { + return; + } + var context = new ValidationContext(types, propertyMap, typeMap); + _config.Validate(context); + CheckPropertyMaps(typeMapsChecked, typeMap); + typeMap.IsValid = true; + } + else + { + var mapperToUse = _config.FindMapper(types); + if (mapperToUse == null) + { + // Maps with no match get mapped at runtime yolo + if (propertyMap.TypeMap.Profile.CreateMissingTypeMaps) + return; + + throw new AutoMapperConfigurationException(propertyMap.TypeMap.Types) { PropertyMap = propertyMap }; + } + var context = new ValidationContext(types, propertyMap, mapperToUse); + _config.Validate(context); + if(mapperToUse is IObjectMapperInfo mapperInfo) + { + var newTypePair = mapperInfo.GetAssociatedTypes(types); + DryRunTypeMap(typeMapsChecked, newTypePair, null, propertyMap); + } + } + } + + private void CheckPropertyMaps(ICollection typeMapsChecked, TypeMap typeMap) + { + foreach (var propertyMap in typeMap.GetPropertyMaps()) + { + if (propertyMap.Ignored) continue; + + var sourceType = propertyMap.SourceType; + + if (sourceType == null) continue; + + // when we don't know what the source type is, bail + if (sourceType.IsGenericParameter || sourceType == typeof (object)) + return; + + var destinationType = propertyMap.DestinationProperty.GetMemberType(); + DryRunTypeMap(typeMapsChecked, new TypePair(sourceType, destinationType), null, propertyMap); + } + } + } +} \ No newline at end of file diff --git a/tools/AutoMapper/ConstructorMap.cs b/tools/AutoMapper/ConstructorMap.cs new file mode 100644 index 0000000000..1dee32b9f3 --- /dev/null +++ b/tools/AutoMapper/ConstructorMap.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using AutoMapper.Execution; +using AutoMapper.QueryableExtensions; +using AutoMapper.QueryableExtensions.Impl; + +namespace AutoMapper +{ + using static Expression; + + public class ConstructorMap + { + private readonly IList _ctorParams = new List(); + + public ConstructorInfo Ctor { get; } + public TypeMap TypeMap { get; } + internal IEnumerable CtorParams => _ctorParams; + + public ConstructorMap(ConstructorInfo ctor, TypeMap typeMap) + { + Ctor = ctor; + TypeMap = typeMap; + } + + private static readonly IExpressionResultConverter[] ExpressionResultConverters = + { + new MemberResolverExpressionResultConverter(), + new MemberGetterExpressionResultConverter() + }; + + public bool CanResolve => CtorParams.All(param => param.CanResolve); + + public Expression NewExpression(Expression instanceParameter) + { + var parameters = CtorParams.Select(map => + { + var result = new ExpressionResolutionResult(instanceParameter, Ctor.DeclaringType); + + var matchingExpressionConverter = + ExpressionResultConverters.FirstOrDefault(c => c.CanGetExpressionResolutionResult(result, map)); + + result = matchingExpressionConverter?.GetExpressionResolutionResult(result, map) + ?? throw new AutoMapperMappingException($"Unable to generate the instantiation expression for the constructor {Ctor}: no expression could be mapped for constructor parameter '{map.Parameter}'.", null, TypeMap.Types); + + return result; + }); + return New(Ctor, parameters.Select(p => p.ResolutionExpression)); + } + + public void AddParameter(ParameterInfo parameter, MemberInfo[] resolvers, bool canResolve) + { + _ctorParams.Add(new ConstructorParameterMap(parameter, resolvers, canResolve)); + } + } +} diff --git a/tools/AutoMapper/ConstructorParameterMap.cs b/tools/AutoMapper/ConstructorParameterMap.cs new file mode 100644 index 0000000000..054291268c --- /dev/null +++ b/tools/AutoMapper/ConstructorParameterMap.cs @@ -0,0 +1,30 @@ +using System; +using System.Linq.Expressions; +using System.Reflection; + +namespace AutoMapper +{ + public class ConstructorParameterMap + { + public ConstructorParameterMap(ParameterInfo parameter, MemberInfo[] sourceMembers, bool canResolve) + { + Parameter = parameter; + SourceMembers = sourceMembers; + CanResolve = canResolve; + } + + public ParameterInfo Parameter { get; } + + public MemberInfo[] SourceMembers { get; } + + public bool CanResolve { get; set; } + + public bool DefaultValue { get; set; } + + public LambdaExpression CustomExpression { get; set; } + + public LambdaExpression CustomValueResolver { get; set; } + + public Type DestinationType => Parameter.ParameterType; + } +} \ No newline at end of file diff --git a/tools/AutoMapper/DuplicateTypeMapConfigurationException.cs b/tools/AutoMapper/DuplicateTypeMapConfigurationException.cs new file mode 100644 index 0000000000..7c3fc82cd9 --- /dev/null +++ b/tools/AutoMapper/DuplicateTypeMapConfigurationException.cs @@ -0,0 +1,40 @@ +using System; +using System.Text; + +namespace AutoMapper +{ + public class DuplicateTypeMapConfigurationException : Exception + { + public TypeMapConfigErrors[] Errors { get; } + + public DuplicateTypeMapConfigurationException(TypeMapConfigErrors[] errors) + { + Errors = errors; + var builder = new StringBuilder(); + builder.AppendLine("The following type maps were found in multiple profiles:"); + foreach (var error in Errors) + { + builder.AppendLine($"{error.Types.SourceType.FullName} to {error.Types.DestinationType.FullName} defined in profiles:"); + builder.AppendLine(string.Join(Environment.NewLine, error.ProfileNames)); + } + builder.AppendLine("This can cause configuration collisions and inconsistent mapping."); + builder.AppendLine("Consolidate the CreateMap calls into one profile, or set the root Advanced.AllowAdditiveTypeMapCreation configuration value to 'true'."); + + Message = builder.ToString(); + } + + public class TypeMapConfigErrors + { + public string[] ProfileNames { get; } + public TypePair Types { get; } + + public TypeMapConfigErrors(TypePair types, string[] profileNames) + { + Types = types; + ProfileNames = profileNames; + } + } + + public override string Message { get; } + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Execution/DelegateFactory.cs b/tools/AutoMapper/Execution/DelegateFactory.cs new file mode 100644 index 0000000000..f3be598d4d --- /dev/null +++ b/tools/AutoMapper/Execution/DelegateFactory.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using AutoMapper.Configuration; +using AutoMapper.Mappers.Internal; + +namespace AutoMapper.Execution +{ + using static Expression; + using static Internal.ExpressionFactory; + using static ElementTypeHelper; + + public static class DelegateFactory + { + private static readonly LockingConcurrentDictionary> CtorCache = new LockingConcurrentDictionary>(GenerateConstructor); + + public static Func CreateCtor(Type type) => CtorCache.GetOrAdd(type); + + private static Func GenerateConstructor(Type type) + { + var ctorExpr = GenerateConstructorExpression(type); + + return Lambda>(Convert(ctorExpr, typeof(object))).Compile(); + } + + public static Expression GenerateConstructorExpression(Type type, ProfileMap configuration) => + configuration.AllowNullDestinationValues + ? GenerateConstructorExpression(type) + : GenerateNonNullConstructorExpression(type); + + public static Expression GenerateNonNullConstructorExpression(Type type) => type.IsValueType() + ? Default(type) + : (type == typeof(string) + ? Constant(string.Empty) + : GenerateConstructorExpression(type) + ); + + public static Expression GenerateConstructorExpression(Type type) + { + if (type.IsValueType()) + { + return Default(type); + } + + if (type == typeof(string)) + { + return Constant(null, typeof(string)); + } + + if (type.IsInterface()) + { + return type.ImplementsGenericInterface(typeof(IDictionary<,>)) + ? CreateCollection(type, typeof(Dictionary<,>)) + : (type.ImplementsGenericInterface(typeof(ICollection<>)) + ? CreateCollection(type, typeof(List<>)) + : InvalidType(type, $"Cannot create an instance of interface type {type}.")); + } + + if (type.IsAbstract()) + { + return InvalidType(type, $"Cannot create an instance of abstract type {type}."); + } + + var constructors = type + .GetDeclaredConstructors() + .Where(ci => !ci.IsStatic); + + //find a ctor with only optional args + var ctorWithOptionalArgs = constructors.FirstOrDefault(c => c.GetParameters().All(p => p.IsOptional)); + if (ctorWithOptionalArgs == null) + { + return InvalidType(type, $"{type} needs to have a constructor with 0 args or only optional args."); + } + //get all optional default values + var args = ctorWithOptionalArgs + .GetParameters() + .Select(p => Constant(p.GetDefaultValue(), p.ParameterType)).ToArray(); + + //create the ctor expression + return New(ctorWithOptionalArgs, args); + } + + private static Expression CreateCollection(Type type, Type collectionType) + { + var listType = collectionType.MakeGenericType(GetElementTypes(type, ElementTypeFlags.BreakKeyValuePair)); + if (type.IsAssignableFrom(listType)) + return ToType(New(listType), type); + + return InvalidType(type, $"Cannot create an instance of interface type {type}."); + } + + private static Expression InvalidType(Type type, string message) + { + var ex = new ArgumentException(message, "type"); + return Block(Throw(Constant(ex)), Constant(null, type)); + } + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Execution/ExpressionBuilder.cs b/tools/AutoMapper/Execution/ExpressionBuilder.cs new file mode 100644 index 0000000000..ac5702a0d5 --- /dev/null +++ b/tools/AutoMapper/Execution/ExpressionBuilder.cs @@ -0,0 +1,125 @@ + +namespace AutoMapper.Execution +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Linq.Expressions; + using System.Reflection; + using AutoMapper.Configuration; + using AutoMapper.Internal; + using AutoMapper.Mappers.Internal; + using static System.Linq.Expressions.Expression; + + public static class ExpressionBuilder + { + private static readonly Expression> CreateContext = + mapper => new ResolutionContext(mapper.DefaultContext.Options, mapper); + + private static readonly MethodInfo ContextMapMethod = + ExpressionFactory.Method(a => a.Map(null, null)).GetGenericMethodDefinition(); + + public static Expression MapExpression(IConfigurationProvider configurationProvider, + ProfileMap profileMap, + TypePair typePair, + Expression sourceParameter, + Expression contextParameter, + PropertyMap propertyMap = null, Expression destinationParameter = null) + { + if (destinationParameter == null) + destinationParameter = Default(typePair.DestinationType); + var typeMap = configurationProvider.ResolveTypeMap(typePair); + if (typeMap != null) + { + if (!typeMap.HasDerivedTypesToInclude()) + { + typeMap.Seal(configurationProvider); + + return typeMap.MapExpression != null + ? typeMap.MapExpression.ConvertReplaceParameters(sourceParameter, destinationParameter, + contextParameter) + : ContextMap(typePair, sourceParameter, contextParameter, destinationParameter); + } + return ContextMap(typePair, sourceParameter, contextParameter, destinationParameter); + } + var objectMapperExpression = ObjectMapperExpression(configurationProvider, profileMap, typePair, + sourceParameter, contextParameter, propertyMap, destinationParameter); + var nullCheckSource = NullCheckSource(profileMap, sourceParameter, destinationParameter, objectMapperExpression, propertyMap); + return ExpressionFactory.ToType(nullCheckSource, typePair.DestinationType); + } + + public static Expression NullCheckSource(ProfileMap profileMap, + Expression sourceParameter, + Expression destinationParameter, + Expression objectMapperExpression, + PropertyMap propertyMap = null) + { + var declaredDestinationType = destinationParameter.Type; + var destinationType = objectMapperExpression.Type; + var defaultDestination = DefaultDestination(destinationType, declaredDestinationType, profileMap); + var destination = propertyMap == null + ? destinationParameter.IfNullElse(defaultDestination, destinationParameter) + : (propertyMap.UseDestinationValue ? destinationParameter : defaultDestination); + var ifSourceNull = destinationParameter.Type.IsCollectionType() ? ClearDestinationCollection() : destination; + return sourceParameter.IfNullElse(ifSourceNull, objectMapperExpression); + Expression ClearDestinationCollection() + { + var destinationElementType = ElementTypeHelper.GetElementType(destinationParameter.Type); + var destinationCollectionType = typeof(ICollection<>).MakeGenericType(destinationElementType); + var destinationVariable = Variable(destinationCollectionType, "collectionDestination"); + var clear = Call(destinationVariable, destinationCollectionType.GetDeclaredMethod("Clear")); + var isReadOnly = Property(destinationVariable, "IsReadOnly"); + return Block(new[] {destinationVariable}, + Assign(destinationVariable, ExpressionFactory.ToType(destinationParameter, destinationCollectionType)), + Condition(OrElse(Equal(destinationVariable, Constant(null)), isReadOnly), Empty(), clear), + destination); + } + } + + private static Expression DefaultDestination(Type destinationType, Type declaredDestinationType, ProfileMap profileMap) + { + if(profileMap.AllowNullCollections || destinationType == typeof(string) || !destinationType.IsEnumerableType()) + { + return Default(declaredDestinationType); + } + if(destinationType.IsArray) + { + var destinationElementType = destinationType.GetElementType(); + return NewArrayBounds(destinationElementType, Enumerable.Repeat(Constant(0), destinationType.GetArrayRank())); + } + return DelegateFactory.GenerateNonNullConstructorExpression(destinationType); + } + + private static Expression ObjectMapperExpression(IConfigurationProvider configurationProvider, + ProfileMap profileMap, TypePair typePair, Expression sourceParameter, Expression contextParameter, + PropertyMap propertyMap, Expression destinationParameter) + { + var match = configurationProvider.FindMapper(typePair); + if (match != null) + { + var mapperExpression = match.MapExpression(configurationProvider, profileMap, propertyMap, + sourceParameter, destinationParameter, contextParameter); + return mapperExpression; + } + return ContextMap(typePair, sourceParameter, contextParameter, destinationParameter); + } + + public static Expression ContextMap(TypePair typePair, Expression sourceParameter, Expression contextParameter, + Expression destinationParameter) + { + var mapMethod = ContextMapMethod.MakeGenericMethod(typePair.SourceType, typePair.DestinationType); + return Call(contextParameter, mapMethod, sourceParameter, destinationParameter); + } + + public static ConditionalExpression CheckContext(TypeMap typeMap, Expression context) + { + if (typeMap.MaxDepth > 0 || typeMap.PreserveReferences) + { + var mapper = Property(context, "Mapper"); + return IfThen(Property(context, "IsDefault"), Assign(context, Invoke(CreateContext, mapper))); + } + return null; + } + + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Execution/PropertyEmitter.cs b/tools/AutoMapper/Execution/PropertyEmitter.cs new file mode 100644 index 0000000000..c5391c2e35 --- /dev/null +++ b/tools/AutoMapper/Execution/PropertyEmitter.cs @@ -0,0 +1,67 @@ +namespace AutoMapper.Execution +{ + using System; + using System.Linq; + using System.Reflection; + using System.Reflection.Emit; + + public class PropertyEmitter + { + private static readonly MethodInfo ProxyBaseNotifyPropertyChanged = + typeof (ProxyBase).GetTypeInfo().DeclaredMethods.Single(m => m.Name == "NotifyPropertyChanged"); + + private readonly FieldBuilder _fieldBuilder; + private readonly MethodBuilder _getterBuilder; + private readonly PropertyBuilder _propertyBuilder; + private readonly MethodBuilder _setterBuilder; + + public PropertyEmitter(TypeBuilder owner, PropertyDescription property, FieldBuilder propertyChangedField) + { + var name = property.Name; + var propertyType = property.Type; + _fieldBuilder = owner.DefineField($"<{name}>", propertyType, FieldAttributes.Private); + _propertyBuilder = owner.DefineProperty(name, PropertyAttributes.None, propertyType, null); + _getterBuilder = owner.DefineMethod($"get_{name}", + MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig | + MethodAttributes.SpecialName, propertyType, new Type[0]); + ILGenerator getterIl = _getterBuilder.GetILGenerator(); + getterIl.Emit(OpCodes.Ldarg_0); + getterIl.Emit(OpCodes.Ldfld, _fieldBuilder); + getterIl.Emit(OpCodes.Ret); + _propertyBuilder.SetGetMethod(_getterBuilder); + if(!property.CanWrite) + { + return; + } + _setterBuilder = owner.DefineMethod($"set_{name}", + MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig | + MethodAttributes.SpecialName, typeof (void), new[] {propertyType}); + ILGenerator setterIl = _setterBuilder.GetILGenerator(); + setterIl.Emit(OpCodes.Ldarg_0); + setterIl.Emit(OpCodes.Ldarg_1); + setterIl.Emit(OpCodes.Stfld, _fieldBuilder); + if (propertyChangedField != null) + { + setterIl.Emit(OpCodes.Ldarg_0); + setterIl.Emit(OpCodes.Dup); + setterIl.Emit(OpCodes.Ldfld, propertyChangedField); + setterIl.Emit(OpCodes.Ldstr, name); + setterIl.Emit(OpCodes.Call, ProxyBaseNotifyPropertyChanged); + } + setterIl.Emit(OpCodes.Ret); + _propertyBuilder.SetSetMethod(_setterBuilder); + } + + public Type PropertyType => _propertyBuilder.PropertyType; + + public MethodBuilder GetGetter(Type requiredType) + => !requiredType.IsAssignableFrom(PropertyType) + ? throw new InvalidOperationException("Types are not compatible") + : _getterBuilder; + + public MethodBuilder GetSetter(Type requiredType) + => !PropertyType.IsAssignableFrom(requiredType) + ? throw new InvalidOperationException("Types are not compatible") + : _setterBuilder; + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Execution/ProxyBase.cs b/tools/AutoMapper/Execution/ProxyBase.cs new file mode 100644 index 0000000000..da3495dbf9 --- /dev/null +++ b/tools/AutoMapper/Execution/ProxyBase.cs @@ -0,0 +1,12 @@ +using System.ComponentModel; + +namespace AutoMapper.Execution +{ + public abstract class ProxyBase + { + protected void NotifyPropertyChanged(PropertyChangedEventHandler handler, string method) + { + handler?.Invoke(this, new PropertyChangedEventArgs(method)); + } + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Execution/ProxyGenerator.cs b/tools/AutoMapper/Execution/ProxyGenerator.cs new file mode 100644 index 0000000000..705f7d9cc0 --- /dev/null +++ b/tools/AutoMapper/Execution/ProxyGenerator.cs @@ -0,0 +1,244 @@ +namespace AutoMapper.Execution +{ + using System; + using System.Collections.Generic; + using System.ComponentModel; + using System.Diagnostics; + using System.Linq; + using System.Reflection; + using System.Reflection.Emit; + using System.Text.RegularExpressions; + + public static class ProxyGenerator + { + private static readonly byte[] privateKey = + StringToByteArray( + "002400000480000094000000060200000024000052534131000400000100010079dfef85ed6ba841717e154f13182c0a6029a40794a6ecd2886c7dc38825f6a4c05b0622723a01cd080f9879126708eef58f134accdc99627947425960ac2397162067507e3c627992aa6b92656ad3380999b30b5d5645ba46cc3fcc6a1de5de7afebcf896c65fb4f9547a6c0c6433045fceccb1fa15e960d519d0cd694b29a4"); + + private static readonly byte[] privateKeyToken = StringToByteArray("be96cd2c38ef1005"); + + private static readonly MethodInfo delegate_Combine = typeof(Delegate).GetDeclaredMethod("Combine", new[] { typeof(Delegate), typeof(Delegate) }); + + private static readonly MethodInfo delegate_Remove = typeof(Delegate).GetDeclaredMethod("Remove", new[] { typeof(Delegate), typeof(Delegate) }); + + private static readonly EventInfo iNotifyPropertyChanged_PropertyChanged = + typeof(INotifyPropertyChanged).GetRuntimeEvent("PropertyChanged"); + + private static readonly ConstructorInfo proxyBase_ctor = + typeof(ProxyBase).GetDeclaredConstructor(new Type[0]); + + private static readonly ModuleBuilder proxyModule = CreateProxyModule(); + + private static readonly LockingConcurrentDictionary proxyTypes = new LockingConcurrentDictionary(EmitProxy); + + private static ModuleBuilder CreateProxyModule() + { + AssemblyName name = new AssemblyName("AutoMapper.Proxies"); + name.SetPublicKey(privateKey); + name.SetPublicKeyToken(privateKeyToken); + +#if NET40 + AssemblyBuilder builder = AppDomain.CurrentDomain.DefineDynamicAssembly(name, AssemblyBuilderAccess.Run); +#else + AssemblyBuilder builder = AssemblyBuilder.DefineDynamicAssembly(name, AssemblyBuilderAccess.Run); +#endif + + return builder.DefineDynamicModule("AutoMapper.Proxies.emit"); + } + + private static Type EmitProxy(TypeDescription typeDescription) + { + var interfaceType = typeDescription.Type; + var additionalProperties = typeDescription.AdditionalProperties; + var propertyNames = string.Join("_", additionalProperties.Select(p => p.Name)); + string name = + $"Proxy{propertyNames}<{Regex.Replace(interfaceType.AssemblyQualifiedName ?? interfaceType.FullName ?? interfaceType.Name, @"[\s,]+", "_")}>"; + var allInterfaces = new List { interfaceType }; + allInterfaces.AddRange(interfaceType.GetTypeInfo().ImplementedInterfaces); + Debug.WriteLine(name, "Emitting proxy type"); + TypeBuilder typeBuilder = proxyModule.DefineType(name, + TypeAttributes.Class | TypeAttributes.Sealed | TypeAttributes.Public, typeof(ProxyBase), + interfaceType.IsInterface() ? new[] { interfaceType } : new Type[0]); + ConstructorBuilder constructorBuilder = typeBuilder.DefineConstructor(MethodAttributes.Public, + CallingConventions.Standard, new Type[0]); + ILGenerator ctorIl = constructorBuilder.GetILGenerator(); + ctorIl.Emit(OpCodes.Ldarg_0); + ctorIl.Emit(OpCodes.Call, proxyBase_ctor); + ctorIl.Emit(OpCodes.Ret); + FieldBuilder propertyChangedField = null; + if(typeof(INotifyPropertyChanged).IsAssignableFrom(interfaceType)) + { + propertyChangedField = typeBuilder.DefineField("PropertyChanged", typeof(PropertyChangedEventHandler), + FieldAttributes.Private); + MethodBuilder addPropertyChangedMethod = typeBuilder.DefineMethod("add_PropertyChanged", + MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | + MethodAttributes.NewSlot | MethodAttributes.Virtual, typeof(void), + new[] { typeof(PropertyChangedEventHandler) }); + ILGenerator addIl = addPropertyChangedMethod.GetILGenerator(); + addIl.Emit(OpCodes.Ldarg_0); + addIl.Emit(OpCodes.Dup); + addIl.Emit(OpCodes.Ldfld, propertyChangedField); + addIl.Emit(OpCodes.Ldarg_1); + addIl.Emit(OpCodes.Call, delegate_Combine); + addIl.Emit(OpCodes.Castclass, typeof(PropertyChangedEventHandler)); + addIl.Emit(OpCodes.Stfld, propertyChangedField); + addIl.Emit(OpCodes.Ret); + MethodBuilder removePropertyChangedMethod = typeBuilder.DefineMethod("remove_PropertyChanged", + MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | + MethodAttributes.NewSlot | MethodAttributes.Virtual, typeof(void), + new[] { typeof(PropertyChangedEventHandler) }); + ILGenerator removeIl = removePropertyChangedMethod.GetILGenerator(); + removeIl.Emit(OpCodes.Ldarg_0); + removeIl.Emit(OpCodes.Dup); + removeIl.Emit(OpCodes.Ldfld, propertyChangedField); + removeIl.Emit(OpCodes.Ldarg_1); + removeIl.Emit(OpCodes.Call, delegate_Remove); + removeIl.Emit(OpCodes.Castclass, typeof(PropertyChangedEventHandler)); + removeIl.Emit(OpCodes.Stfld, propertyChangedField); + removeIl.Emit(OpCodes.Ret); + typeBuilder.DefineMethodOverride(addPropertyChangedMethod, + iNotifyPropertyChanged_PropertyChanged.GetAddMethod()); + typeBuilder.DefineMethodOverride(removePropertyChangedMethod, + iNotifyPropertyChanged_PropertyChanged.GetRemoveMethod()); + } + var propertiesToImplement = new List(); + // first we collect all properties, those with setters before getters in order to enable less specific redundant getters + foreach(var property in + allInterfaces.Where(intf => intf != typeof(INotifyPropertyChanged)) + .SelectMany(intf => intf.GetProperties()) + .Select(p => new PropertyDescription(p)) + .Concat(additionalProperties)) + { + if(property.CanWrite) + { + propertiesToImplement.Insert(0, property); + } + else + { + propertiesToImplement.Add(property); + } + } + var fieldBuilders = new Dictionary(); + foreach(var property in propertiesToImplement) + { + PropertyEmitter propertyEmitter; + if(fieldBuilders.TryGetValue(property.Name, out propertyEmitter)) + { + if((propertyEmitter.PropertyType != property.Type) && + ((property.CanWrite) || (!property.Type.IsAssignableFrom(propertyEmitter.PropertyType)))) + { + throw new ArgumentException( + $"The interface has a conflicting property {property.Name}", + nameof(interfaceType)); + } + } + else + { + fieldBuilders.Add(property.Name, + propertyEmitter = + new PropertyEmitter(typeBuilder, property, propertyChangedField)); + } + } + return typeBuilder.CreateType(); + } + + public static Type GetProxyType(Type interfaceType) + { + var key = new TypeDescription(interfaceType); + if(!interfaceType.IsInterface()) + { + throw new ArgumentException("Only interfaces can be proxied", nameof(interfaceType)); + } + return proxyTypes.GetOrAdd(key); + } + + public static Type GetSimilarType(Type sourceType, IEnumerable additionalProperties) + { + return proxyTypes.GetOrAdd(new TypeDescription(sourceType, additionalProperties)); + } + + private static byte[] StringToByteArray(string hex) + { + int numberChars = hex.Length; + byte[] bytes = new byte[numberChars / 2]; + for(int i = 0; i < numberChars; i += 2) + bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16); + return bytes; + } + } + + public struct TypeDescription : IEquatable + { + public TypeDescription(Type type) : this(type, PropertyDescription.Empty) + { + } + + public TypeDescription(Type type, IEnumerable additionalProperties) + { + Type = type ?? throw new ArgumentNullException(nameof(type)); + AdditionalProperties = additionalProperties?.ToArray() ?? throw new ArgumentNullException(nameof(additionalProperties)); + } + + public Type Type { get; } + + public PropertyDescription[] AdditionalProperties { get; } + + public override int GetHashCode() + { + var hashCode = Type.GetHashCode(); + foreach(var property in AdditionalProperties) + { + hashCode = HashCodeCombiner.CombineCodes(hashCode, property.GetHashCode()); + } + return hashCode; + } + + public override bool Equals(object other) => other is TypeDescription && Equals((TypeDescription)other); + + public bool Equals(TypeDescription other) => Type == other.Type && AdditionalProperties.SequenceEqual(other.AdditionalProperties); + + public static bool operator ==(TypeDescription left, TypeDescription right) => left.Equals(right); + + public static bool operator !=(TypeDescription left, TypeDescription right) => !left.Equals(right); + } + + [DebuggerDisplay("{Name}-{Type.Name}")] + public struct PropertyDescription : IEquatable + { + internal static PropertyDescription[] Empty = new PropertyDescription[0]; + + public PropertyDescription(string name, Type type, bool canWrite = true) + { + Name = name; + Type = type; + CanWrite = canWrite; + } + + public PropertyDescription(PropertyInfo property) + { + Name = property.Name; + Type = property.PropertyType; + CanWrite = property.CanWrite; + } + + public string Name { get; } + + public Type Type { get; } + + public bool CanWrite { get; } + + public override int GetHashCode() + { + var code = HashCodeCombiner.Combine(Name, Type); + return HashCodeCombiner.CombineCodes(code, CanWrite.GetHashCode()); + } + + public override bool Equals(object other) => other is PropertyDescription && Equals((PropertyDescription)other); + + public bool Equals(PropertyDescription other) => Name == other.Name && Type == other.Type && CanWrite == other.CanWrite; + + public static bool operator ==(PropertyDescription left, PropertyDescription right) => left.Equals(right); + + public static bool operator !=(PropertyDescription left, PropertyDescription right) => !left.Equals(right); + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Execution/TypeMapPlanBuilder.cs b/tools/AutoMapper/Execution/TypeMapPlanBuilder.cs new file mode 100644 index 0000000000..d524d4e79f --- /dev/null +++ b/tools/AutoMapper/Execution/TypeMapPlanBuilder.cs @@ -0,0 +1,580 @@ +namespace AutoMapper.Execution +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Linq.Expressions; + using System.Reflection; + using Configuration; + using static System.Linq.Expressions.Expression; + using static Internal.ExpressionFactory; + using static ExpressionBuilder; + using System.Diagnostics; + + public class TypeMapPlanBuilder + { + private static readonly Expression> CtorExpression = + () => new AutoMapperMappingException(null, null, default(TypePair), null, null); + + private static readonly Expression> IncTypeDepthInfo = + ctxt => ctxt.IncrementTypeDepth(default(TypePair)); + + private static readonly Expression> ValidateMap = + ctxt => ctxt.ValidateMap(default(TypeMap)); + + private static readonly Expression> DecTypeDepthInfo = + ctxt => ctxt.DecrementTypeDepth(default(TypePair)); + + private static readonly Expression> GetTypeDepthInfo = + ctxt => ctxt.GetTypeDepth(default(TypePair)); + + private readonly IConfigurationProvider _configurationProvider; + private readonly ParameterExpression _destination; + private readonly ParameterExpression _initialDestination; + private readonly TypeMap _typeMap; + + public TypeMapPlanBuilder(IConfigurationProvider configurationProvider, TypeMap typeMap) + { + _configurationProvider = configurationProvider; + _typeMap = typeMap; + Source = Parameter(typeMap.SourceType, "src"); + _initialDestination = Parameter(typeMap.DestinationTypeToUse, "dest"); + Context = Parameter(typeof(ResolutionContext), "ctxt"); + _destination = Variable(_initialDestination.Type, "typeMapDestination"); + } + + public ParameterExpression Source { get; } + + public ParameterExpression Context { get; } + + public LambdaExpression CreateMapperLambda(Stack typeMapsPath) + { + if (_typeMap.SourceType.IsGenericTypeDefinition() || + _typeMap.DestinationTypeToUse.IsGenericTypeDefinition()) + return null; + var customExpression = TypeConverterMapper() ?? + _typeMap.Substitution ?? _typeMap.CustomMapper ?? _typeMap.CustomProjection; + if (customExpression != null) + return Lambda(customExpression.ReplaceParameters(Source, _initialDestination, Context), Source, + _initialDestination, Context); + + CheckForCycles(typeMapsPath); + + var destinationFunc = CreateDestinationFunc(out bool constructorMapping); + + var assignmentFunc = CreateAssignmentFunc(destinationFunc, constructorMapping); + + var mapperFunc = CreateMapperFunc(assignmentFunc); + + var checkContext = CheckContext(_typeMap, Context); + var lambaBody = checkContext != null ? new[] {checkContext, mapperFunc} : new[] {mapperFunc}; + + return Lambda(Block(new[] {_destination}, lambaBody), Source, _initialDestination, Context); + } + + private void CheckForCycles(Stack typeMapsPath) + { + if(_typeMap.PreserveReferences) + { + return; + } + if(typeMapsPath == null) + { + typeMapsPath = new Stack(); + } + typeMapsPath.Push(_typeMap); + var properties = + from pm in _typeMap.GetPropertyMaps() where pm.CanResolveValue() + let propertyTypeMap = ResolvePropertyTypeMap(pm) + where propertyTypeMap != null && !propertyTypeMap.PreserveReferences + select new { PropertyTypeMap = propertyTypeMap, PropertyMap = pm }; + foreach(var property in properties) + { + if(typeMapsPath.Count % _configurationProvider.MaxExecutionPlanDepth == 0) + { + property.PropertyMap.Inline = false; + Debug.WriteLine($"Resetting Inline: {property.PropertyMap.DestinationProperty} in {_typeMap.SourceType} - {_typeMap.DestinationType}"); + } + + var propertyTypeMap = property.PropertyTypeMap; + if(typeMapsPath.Contains(propertyTypeMap) && !propertyTypeMap.SourceType.IsValueType()) + { + SetPreserveReferences(propertyTypeMap); + foreach(var derivedTypeMap in propertyTypeMap.IncludedDerivedTypes.Select(ResolveTypeMap)) + { + SetPreserveReferences(derivedTypeMap); + } + } + else + { + propertyTypeMap.Seal(_configurationProvider, typeMapsPath); + } + } + typeMapsPath.Pop(); + } + + private void SetPreserveReferences(TypeMap propertyTypeMap) + { + Debug.WriteLine($"Setting PreserveReferences: {_typeMap.SourceType} - {_typeMap.DestinationType} => {propertyTypeMap.SourceType} - {propertyTypeMap.DestinationType}"); + propertyTypeMap.PreserveReferences = true; + } + + private TypeMap ResolvePropertyTypeMap(PropertyMap propertyMap) + { + if(propertyMap.SourceType == null) + { + return null; + } + var types = new TypePair(propertyMap.SourceType, propertyMap.DestinationPropertyType); + return ResolveTypeMap(types); + } + + private TypeMap ResolveTypeMap(TypePair types) + { + var typeMap = _configurationProvider.ResolveTypeMap(types); + if(typeMap == null && _configurationProvider.FindMapper(types) is IObjectMapperInfo mapper) + { + typeMap = _configurationProvider.ResolveTypeMap(mapper.GetAssociatedTypes(types)); + } + return typeMap; + } + + private LambdaExpression TypeConverterMapper() + { + if (_typeMap.TypeConverterType == null) + return null; + Type type; + if (_typeMap.TypeConverterType.IsGenericTypeDefinition()) + { + var genericTypeParam = _typeMap.SourceType.IsGenericType() + ? _typeMap.SourceType.GetTypeInfo().GenericTypeArguments[0] + : _typeMap.DestinationTypeToUse.GetTypeInfo().GenericTypeArguments[0]; + type = _typeMap.TypeConverterType.MakeGenericType(genericTypeParam); + } + else + { + type = _typeMap.TypeConverterType; + } + // (src, dest, ctxt) => ((ITypeConverter)ctxt.Options.CreateInstance()).ToType(src, ctxt); + var converterInterfaceType = + typeof(ITypeConverter<,>).MakeGenericType(_typeMap.SourceType, _typeMap.DestinationTypeToUse); + return Lambda( + Call( + ToType(CreateInstance(type), converterInterfaceType), + converterInterfaceType.GetDeclaredMethod("Convert"), + Source, _initialDestination, Context + ), + Source, _initialDestination, Context); + } + + private Expression CreateDestinationFunc(out bool constructorMapping) + { + var newDestFunc = ToType(CreateNewDestinationFunc(out constructorMapping), _typeMap.DestinationTypeToUse); + + var getDest = _typeMap.DestinationTypeToUse.IsValueType() + ? newDestFunc + : Coalesce(_initialDestination, newDestFunc); + + Expression destinationFunc = Assign(_destination, getDest); + + if (_typeMap.PreserveReferences) + { + var dest = Variable(typeof(object), "dest"); + var setValue = Context.Type.GetDeclaredMethod("CacheDestination"); + var set = Call(Context, setValue, Source, Constant(_destination.Type), _destination); + var setCache = IfThen(NotEqual(Source, Constant(null)), set); + + destinationFunc = Block(new[] {dest}, Assign(dest, destinationFunc), setCache, dest); + } + return destinationFunc; + } + + private Expression CreateAssignmentFunc(Expression destinationFunc, bool constructorMapping) + { + var actions = new List(); + foreach (var propertyMap in _typeMap.GetPropertyMaps().Where(pm => pm.CanResolveValue())) + { + var property = TryPropertyMap(propertyMap); + if (constructorMapping && _typeMap.ConstructorParameterMatches(propertyMap.DestinationProperty.Name)) + property = _initialDestination.IfNullElse(Empty(), property); + actions.Add(property); + } + foreach (var pathMap in _typeMap.PathMaps.Where(pm => !pm.Ignored)) + actions.Add(HandlePath(pathMap)); + foreach (var beforeMapAction in _typeMap.BeforeMapActions) + actions.Insert(0, beforeMapAction.ReplaceParameters(Source, _destination, Context)); + actions.Insert(0, destinationFunc); + if (_typeMap.MaxDepth > 0) + { + actions.Insert(0, + Call(Context, ((MethodCallExpression) IncTypeDepthInfo.Body).Method, Constant(_typeMap.Types))); + } + if (_typeMap.IsConventionMap && _typeMap.Profile.ValidateInlineMaps) + { + actions.Insert(0, Call(Context, ((MethodCallExpression)ValidateMap.Body).Method, Constant(_typeMap))); + } + actions.AddRange( + _typeMap.AfterMapActions.Select( + afterMapAction => afterMapAction.ReplaceParameters(Source, _destination, Context))); + + if (_typeMap.MaxDepth > 0) + actions.Add(Call(Context, ((MethodCallExpression) DecTypeDepthInfo.Body).Method, + Constant(_typeMap.Types))); + + actions.Add(_destination); + + return Block(actions); + } + + private Expression HandlePath(PathMap pathMap) + { + var destination = ((MemberExpression) pathMap.DestinationExpression.ConvertReplaceParameters(_destination)) + .Expression; + var createInnerObjects = CreateInnerObjects(destination); + var setFinalValue = CreatePropertyMapFunc(new PropertyMap(pathMap), destination); + return Block(createInnerObjects, setFinalValue); + } + + private Expression CreateInnerObjects(Expression destination) => Block(destination.GetMembers() + .Select(NullCheck) + .Reverse() + .Concat(new[] {Empty()})); + + private Expression NullCheck(MemberExpression memberExpression) + { + var setter = GetSetter(memberExpression); + var ifNull = setter == null + ? (Expression) + Throw(Constant(new NullReferenceException( + $"{memberExpression} cannot be null because it's used by ForPath."))) + : Assign(setter, DelegateFactory.GenerateConstructorExpression(memberExpression.Type)); + return memberExpression.IfNullElse(ifNull); + } + + private Expression CreateMapperFunc(Expression assignmentFunc) + { + var mapperFunc = assignmentFunc; + + if(_typeMap.Condition != null) + mapperFunc = + Condition(_typeMap.Condition.Body, + mapperFunc, Default(_typeMap.DestinationTypeToUse)); + + if(_typeMap.MaxDepth > 0) + mapperFunc = Condition( + LessThanOrEqual( + Call(Context, ((MethodCallExpression)GetTypeDepthInfo.Body).Method, Constant(_typeMap.Types)), + Constant(_typeMap.MaxDepth) + ), + mapperFunc, + Default(_typeMap.DestinationTypeToUse)); + + if(_typeMap.Profile.AllowNullDestinationValues) + mapperFunc = Source.IfNullElse(Default(_typeMap.DestinationTypeToUse), mapperFunc); + + return CheckReferencesCache(mapperFunc); + } + + private Expression CheckReferencesCache(Expression valueBuilder) + { + if(!_typeMap.PreserveReferences) + { + return valueBuilder; + } + var cache = Variable(_typeMap.DestinationTypeToUse, "cachedDestination"); + var getDestination = Context.Type.GetDeclaredMethod("GetDestination"); + var assignCache = + Assign(cache, + ToType(Call(Context, getDestination, Source, Constant(_destination.Type)), _destination.Type)); + var condition = Condition( + AndAlso(NotEqual(Source, Constant(null)), NotEqual(assignCache, Constant(null))), + cache, + valueBuilder); + return Block(new[] { cache }, condition); + } + + private Expression CreateNewDestinationFunc(out bool constructorMapping) + { + constructorMapping = false; + if (_typeMap.DestinationCtor != null) + return _typeMap.DestinationCtor.ReplaceParameters(Source, Context); + + if (_typeMap.ConstructDestinationUsingServiceLocator) + return CreateInstance(_typeMap.DestinationTypeToUse); + + if (_typeMap.ConstructorMap?.CanResolve == true) + { + constructorMapping = true; + return CreateNewDestinationExpression(_typeMap.ConstructorMap); + } + + if (_typeMap.DestinationTypeToUse.IsInterface()) + { + var ctor = Call(null, + typeof(DelegateFactory).GetDeclaredMethod(nameof(DelegateFactory.CreateCtor), new[] { typeof(Type) }), + Call(null, + typeof(ProxyGenerator).GetDeclaredMethod(nameof(ProxyGenerator.GetProxyType)), + Constant(_typeMap.DestinationTypeToUse))); + // We're invoking a delegate here to make it have the right accessibility + return Invoke(ctor); + } + + return DelegateFactory.GenerateConstructorExpression(_typeMap.DestinationTypeToUse); + } + + private Expression CreateNewDestinationExpression(ConstructorMap constructorMap) + { + var ctorArgs = constructorMap.CtorParams.Select(CreateConstructorParameterExpression); + var variables = constructorMap.Ctor.GetParameters().Select(parameter => Variable(parameter.ParameterType, parameter.Name)).ToArray(); + var body = variables.Zip(ctorArgs, + (variable, expression) => (Expression) Assign(variable, ToType(expression, variable.Type))) + .Concat(new[] { CheckReferencesCache(New(constructorMap.Ctor, variables)) }) + .ToArray(); + return Block(variables, body); + } + + private Expression CreateConstructorParameterExpression(ConstructorParameterMap ctorParamMap) + { + var valueResolverExpression = ResolveSource(ctorParamMap); + var sourceType = valueResolverExpression.Type; + var resolvedValue = Variable(sourceType, "resolvedValue"); + return Block(new[] {resolvedValue}, + Assign(resolvedValue, valueResolverExpression), + MapExpression(_configurationProvider, _typeMap.Profile, new TypePair(sourceType, ctorParamMap.DestinationType), resolvedValue, Context)); + } + + private Expression ResolveSource(ConstructorParameterMap ctorParamMap) + { + if (ctorParamMap.CustomExpression != null) + return ctorParamMap.CustomExpression.ConvertReplaceParameters(Source) + .NullCheck(ctorParamMap.DestinationType); + if (ctorParamMap.CustomValueResolver != null) + return ctorParamMap.CustomValueResolver.ConvertReplaceParameters(Source, Context); + if (ctorParamMap.Parameter.IsOptional) + { + ctorParamMap.DefaultValue = true; + return Constant(ctorParamMap.Parameter.GetDefaultValue(), ctorParamMap.Parameter.ParameterType); + } + return Chain(ctorParamMap.SourceMembers, ctorParamMap.DestinationType); + } + + private Expression TryPropertyMap(PropertyMap propertyMap) + { + var pmExpression = CreatePropertyMapFunc(propertyMap, _destination); + + if (pmExpression == null) + return null; + + var exception = Parameter(typeof(Exception), "ex"); + + var mappingExceptionCtor = ((NewExpression) CtorExpression.Body).Constructor; + + return TryCatch(Block(typeof(void), pmExpression), + MakeCatchBlock(typeof(Exception), exception, + Throw(New(mappingExceptionCtor, Constant("Error mapping types."), exception, + Constant(propertyMap.TypeMap.Types), Constant(propertyMap.TypeMap), Constant(propertyMap))), + null)); + } + + private Expression CreatePropertyMapFunc(PropertyMap propertyMap, Expression destination) + { + var destMember = MakeMemberAccess(destination, propertyMap.DestinationProperty); + + Expression getter; + + if (propertyMap.DestinationProperty is PropertyInfo pi && pi.GetGetMethod(true) == null) + getter = Default(propertyMap.DestinationPropertyType); + else + getter = destMember; + + Expression destValueExpr; + if (propertyMap.UseDestinationValue) + { + destValueExpr = getter; + } + else + { + if (_initialDestination.Type.IsValueType()) + destValueExpr = Default(propertyMap.DestinationPropertyType); + else + destValueExpr = Condition(Equal(_initialDestination, Constant(null)), + Default(propertyMap.DestinationPropertyType), getter); + } + + var valueResolverExpr = BuildValueResolverFunc(propertyMap, getter); + var resolvedValue = Variable(valueResolverExpr.Type, "resolvedValue"); + var setResolvedValue = Assign(resolvedValue, valueResolverExpr); + valueResolverExpr = resolvedValue; + + var typePair = new TypePair(valueResolverExpr.Type, propertyMap.DestinationPropertyType); + valueResolverExpr = propertyMap.Inline + ? MapExpression(_configurationProvider, _typeMap.Profile, typePair, valueResolverExpr, Context, + propertyMap, destValueExpr) + : ContextMap(typePair, valueResolverExpr, Context, destValueExpr); + + valueResolverExpr = propertyMap.ValueTransformers + .Concat(_typeMap.ValueTransformers) + .Concat(_typeMap.Profile.ValueTransformers) + .Where(vt => vt.IsMatch(propertyMap)) + .Aggregate(valueResolverExpr, (current, vtConfig) => ToType(ReplaceParameters(vtConfig.TransformerExpression, ToType(current, vtConfig.ValueType)), propertyMap.DestinationPropertyType)); + + ParameterExpression propertyValue; + Expression setPropertyValue; + if (valueResolverExpr == resolvedValue) + { + propertyValue = resolvedValue; + setPropertyValue = setResolvedValue; + } + else + { + propertyValue = Variable(valueResolverExpr.Type, "propertyValue"); + setPropertyValue = Assign(propertyValue, valueResolverExpr); + } + + Expression mapperExpr; + if (propertyMap.DestinationProperty is FieldInfo) + { + mapperExpr = propertyMap.SourceType != propertyMap.DestinationPropertyType + ? Assign(destMember, ToType(propertyValue, propertyMap.DestinationPropertyType)) + : Assign(getter, propertyValue); + } + else + { + var setter = ((PropertyInfo) propertyMap.DestinationProperty).GetSetMethod(true); + if (setter == null) + mapperExpr = propertyValue; + else + mapperExpr = Assign(destMember, ToType(propertyValue, propertyMap.DestinationPropertyType)); + } + + if (propertyMap.Condition != null) + mapperExpr = IfThen( + propertyMap.Condition.ConvertReplaceParameters( + Source, + _destination, + ToType(propertyValue, propertyMap.Condition.Parameters[2].Type), + ToType(getter, propertyMap.Condition.Parameters[2].Type), + Context + ), + mapperExpr + ); + + mapperExpr = Block(new[] {setResolvedValue, setPropertyValue, mapperExpr}.Distinct()); + + if (propertyMap.PreCondition != null) + mapperExpr = IfThen( + propertyMap.PreCondition.ConvertReplaceParameters(Source, Context), + mapperExpr + ); + + return Block(new[] {resolvedValue, propertyValue}.Distinct(), mapperExpr); + } + + private Expression BuildValueResolverFunc(PropertyMap propertyMap, Expression destValueExpr) + { + Expression valueResolverFunc; + var destinationPropertyType = propertyMap.DestinationPropertyType; + var valueResolverConfig = propertyMap.ValueResolverConfig; + var typeMap = propertyMap.TypeMap; + + if (valueResolverConfig != null) + { + valueResolverFunc = ToType(BuildResolveCall(destValueExpr, valueResolverConfig), + destinationPropertyType); + } + else if (propertyMap.CustomResolver != null) + { + valueResolverFunc = + propertyMap.CustomResolver.ConvertReplaceParameters(Source, _destination, destValueExpr, Context); + } + else if (propertyMap.CustomExpression != null) + { + var nullCheckedExpression = propertyMap.CustomExpression.ReplaceParameters(Source) + .NullCheck(destinationPropertyType); + var destinationNullable = destinationPropertyType.IsNullableType(); + var returnType = destinationNullable && destinationPropertyType.GetTypeOfNullable() == + nullCheckedExpression.Type + ? destinationPropertyType + : nullCheckedExpression.Type; + valueResolverFunc = + TryCatch( + ToType(nullCheckedExpression, returnType), + Catch(typeof(NullReferenceException), Default(returnType)), + Catch(typeof(ArgumentNullException), Default(returnType)) + ); + } + else if (propertyMap.SourceMembers.Any() + && propertyMap.SourceType != null + ) + { + var last = propertyMap.SourceMembers.Last(); + if (last is PropertyInfo pi && pi.GetGetMethod(true) == null) + { + valueResolverFunc = Default(last.GetMemberType()); + } + else + { + valueResolverFunc = Chain(propertyMap.SourceMembers, destinationPropertyType); + } + } + else if (propertyMap.SourceMember != null) + { + valueResolverFunc = MakeMemberAccess(Source, propertyMap.SourceMember); + } + else + { + valueResolverFunc = Throw(Constant(new Exception("I done blowed up"))); + } + + if (propertyMap.NullSubstitute != null) + { + var nullSubstitute = Constant(propertyMap.NullSubstitute); + valueResolverFunc = Coalesce(valueResolverFunc, ToType(nullSubstitute, valueResolverFunc.Type)); + } + else if (!typeMap.Profile.AllowNullDestinationValues) + { + var toCreate = propertyMap.SourceType ?? destinationPropertyType; + if (!toCreate.IsAbstract() && toCreate.IsClass()) + valueResolverFunc = Coalesce( + valueResolverFunc, + ToType(DelegateFactory.GenerateNonNullConstructorExpression(toCreate), propertyMap.SourceType) + ); + } + + return valueResolverFunc; + } + + private Expression Chain(IEnumerable members, Type destinationType) => + members + .Aggregate( + (Expression) Source, + (inner, getter) => getter is MethodInfo method ? + (getter.IsStatic() ? Call(null, method, inner) : (Expression) Call(inner, method)) : + MakeMemberAccess(getter.IsStatic() ? null : inner, getter)) + .NullCheck(destinationType); + + private Expression CreateInstance(Type type) + => Call(Property(Context, nameof(ResolutionContext.Options)), + nameof(IMappingOperationOptions.CreateInstance), new[] {type}); + + private Expression BuildResolveCall(Expression destValueExpr, ValueResolverConfiguration valueResolverConfig) + { + var resolverInstance = valueResolverConfig.Instance != null + ? Constant(valueResolverConfig.Instance) + : CreateInstance(valueResolverConfig.ConcreteType); + + var sourceMember = valueResolverConfig.SourceMember?.ReplaceParameters(Source) ?? + (valueResolverConfig.SourceMemberName != null + ? PropertyOrField(Source, valueResolverConfig.SourceMemberName) + : null); + + var iResolverType = valueResolverConfig.InterfaceType; + + var parameters = new[] {Source, _destination, sourceMember, destValueExpr}.Where(p => p != null) + .Zip(iResolverType.GetGenericArguments(), ToType) + .Concat(new[] {Context}); + return Call(ToType(resolverInstance, iResolverType), iResolverType.GetDeclaredMethod("Resolve"), + parameters); + } + } +} \ No newline at end of file diff --git a/tools/AutoMapper/ExpressionExtensions.cs b/tools/AutoMapper/ExpressionExtensions.cs new file mode 100644 index 0000000000..0799ba5d2e --- /dev/null +++ b/tools/AutoMapper/ExpressionExtensions.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using AutoMapper.Internal; + +namespace AutoMapper +{ + using static Expression; + + internal static class ExpressionExtensions + { + public static Expression MemberAccesses(this IEnumerable members, Expression obj) => + members.Aggregate(obj, (expression, member) => MakeMemberAccess(expression, member)); + + public static IEnumerable GetMembers(this Expression expression) + { + var memberExpression = expression as MemberExpression; + if(memberExpression == null) + { + return new MemberExpression[0]; + } + return memberExpression.GetMembers(); + } + + public static IEnumerable GetMembers(this MemberExpression expression) + { + while(expression != null) + { + yield return expression; + expression = expression.Expression as MemberExpression; + } + } + + public static bool IsMemberPath(this LambdaExpression exp) + { + return exp.Body.GetMembers().LastOrDefault()?.Expression == exp.Parameters.First(); + } + + public static Expression ReplaceParameters(this LambdaExpression exp, params Expression[] replace) + => ExpressionFactory.ReplaceParameters(exp, replace); + + public static Expression ConvertReplaceParameters(this LambdaExpression exp, params Expression[] replace) + => ExpressionFactory.ConvertReplaceParameters(exp, replace); + + public static Expression Replace(this Expression exp, Expression old, Expression replace) + => ExpressionFactory.Replace(exp, old, replace); + + public static LambdaExpression Concat(this LambdaExpression expr, LambdaExpression concat) + => ExpressionFactory.Concat(expr, concat); + + public static Expression NullCheck(this Expression expression, Type destinationType) + => ExpressionFactory.NullCheck(expression, destinationType); + + public static Expression IfNullElse(this Expression expression, Expression then, Expression @else = null) + => ExpressionFactory.IfNullElse(expression, then, @else); + } +} \ No newline at end of file diff --git a/tools/AutoMapper/IConfigurationProvider.cs b/tools/AutoMapper/IConfigurationProvider.cs new file mode 100644 index 0000000000..959172c30d --- /dev/null +++ b/tools/AutoMapper/IConfigurationProvider.cs @@ -0,0 +1,170 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using AutoMapper.Configuration; +using AutoMapper.QueryableExtensions; + +namespace AutoMapper +{ + public interface IConfigurationProvider + { + void Validate(ValidationContext context); + + /// + /// Get all configured type maps created + /// + /// All configured type maps + TypeMap[] GetAllTypeMaps(); + + /// + /// Find the for the configured source and destination type + /// + /// Configured source type + /// Configured destination type + /// Type map configuration + TypeMap FindTypeMapFor(Type sourceType, Type destinationType); + + /// + /// Find the for the configured type pair + /// + /// Type pair + /// Type map configuration + TypeMap FindTypeMapFor(TypePair typePair); + + /// + /// Find the for the configured source and destination type + /// + /// Source type + /// Destination type + /// Type map configuration + TypeMap FindTypeMapFor(); + + /// + /// Resolve the for the configured source and destination type, checking parent types + /// + /// Configured source type + /// Configured destination type + /// Type map configuration + TypeMap ResolveTypeMap(Type sourceType, Type destinationType); + + /// + /// Resolve the for the configured source and destination type, checking parent types + /// + /// Configured source type + /// Configured destination type + /// Inline type map configuration if exists + /// Type map configuration + TypeMap ResolveTypeMap(Type sourceType, Type destinationType, ITypeMapConfiguration inlineConfiguration); + + /// + /// Resolve the for the configured type pair, checking parent types + /// + /// Type pair + /// Inline type map configuration if exists + /// Type map configuration + TypeMap ResolveTypeMap(TypePair typePair, ITypeMapConfiguration inlineConfiguration); + + /// + /// Resolve the for the configured type pair, checking parent types + /// + /// Type pair + /// Type map configuration + TypeMap ResolveTypeMap(TypePair typePair); + + /// + /// Dry run all configured type maps and throw for each problem + /// + void AssertConfigurationIsValid(); + + /// + /// Dry run single type map + /// + /// Type map to check + void AssertConfigurationIsValid(TypeMap typeMap); + + /// + /// Dry run all type maps in given profile + /// + /// Profile name of type maps to test + void AssertConfigurationIsValid(string profileName); + + /// + /// Dry run all type maps in given profile + /// + /// Profile type + void AssertConfigurationIsValid() where TProfile : Profile, new(); + + /// + /// Get all configured mappers + /// + /// List of mappers + IEnumerable GetMappers(); + + /// + /// Find a matching object mapper. + /// + /// the types to match + /// the matching mapper or null + IObjectMapper FindMapper(TypePair types); + + /// + /// Factory method to create formatters, resolvers and type converters + /// + Func ServiceCtor { get; } + + /// + /// Allows to enable null-value propagation for query mapping. + /// Some providers (such as EntityFrameworkQueryVisitor) do not work with this feature enabled! + /// + bool EnableNullPropagationForQueryMapping { get; } + + int MaxExecutionPlanDepth { get; } + + IExpressionBuilder ExpressionBuilder { get; } + + /// + /// Create a mapper instance based on this configuration. Mapper instances are lightweight and can be created as needed. + /// + /// The mapper instance + IMapper CreateMapper(); + + /// + /// Create a mapper instance with the specified service constructor to be used for resolvers and type converters. + /// + /// Service factory to create services + /// The mapper instance + IMapper CreateMapper(Func serviceCtor); + + Func GetMapperFunc(TypePair types); + Func GetMapperFunc(MapRequest mapRequest); + + /// + /// Compile all underlying mapping expressions to cached delegates. + /// Use if you want AutoMapper to compile all mappings up front instead of deferring expression compilation for each first map. + /// + void CompileMappings(); + + Delegate GetMapperFunc(MapRequest request); + + Func GetUntypedMapperFunc(MapRequest mapRequest); + + /// + /// Builds the execution plan used to map the source to destination. + /// Useful to understand what exactly is happening during mapping. + /// See the wiki for details. + /// + /// the runtime type of the source object + /// the runtime type of the destination object + /// the execution plan + LambdaExpression BuildExecutionPlan(Type sourceType, Type destinationType); + + /// + /// Builds the execution plan used to map the source to destination. + /// Useful to understand what exactly is happening during mapping. + /// See the wiki for details. + /// + /// The source/destination map request + /// the execution plan + LambdaExpression BuildExecutionPlan(MapRequest mapRequest); + } +} \ No newline at end of file diff --git a/tools/AutoMapper/ICtorParamConfigurationExpression.cs b/tools/AutoMapper/ICtorParamConfigurationExpression.cs new file mode 100644 index 0000000000..39afbffe0c --- /dev/null +++ b/tools/AutoMapper/ICtorParamConfigurationExpression.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; + +namespace AutoMapper +{ + public interface ICtorParamConfigurationExpression + { + /// + /// Map constructor parameter from member expression + /// + /// Member type + /// Member expression + void MapFrom(Expression> sourceMember); + + /// + /// Map constructor parameter from custom func + /// + /// Custom func + void ResolveUsing(Func resolver); + + /// + /// Map constructor parameter from custom func that has access to + /// + /// Custom func + void ResolveUsing(Func resolver); + } + + public class CtorParamConfigurationExpression : ICtorParamConfigurationExpression + { + private readonly string _ctorParamName; + private readonly List> _ctorParamActions = new List>(); + + public CtorParamConfigurationExpression(string ctorParamName) => _ctorParamName = ctorParamName; + + public void MapFrom(Expression> sourceMember) + { + _ctorParamActions.Add(cpm => cpm.CustomExpression = sourceMember); + } + + public void ResolveUsing(Func resolver) + { + Expression> resolverExpression = (src, ctxt) => resolver(src); + _ctorParamActions.Add(cpm => cpm.CustomValueResolver = resolverExpression); + } + + public void ResolveUsing(Func resolver) + { + Expression> resolverExpression = (src, ctxt) => resolver(src, ctxt); + _ctorParamActions.Add(cpm => cpm.CustomValueResolver = resolverExpression); + } + + public void Configure(TypeMap typeMap) + { + var ctorParams = typeMap.ConstructorMap?.CtorParams; + if (ctorParams == null) + { + throw new AutoMapperConfigurationException($"The type {typeMap.Types.DestinationType.Name} does not have a constructor.\n{typeMap.Types.DestinationType.FullName}"); + } + + var parameter = ctorParams.SingleOrDefault(p => p.Parameter.Name == _ctorParamName); + if (parameter == null) + { + throw new AutoMapperConfigurationException($"{typeMap.Types.DestinationType.Name} does not have a constructor with a parameter named '{_ctorParamName}'.\n{typeMap.Types.DestinationType.FullName}"); + } + parameter.CanResolve = true; + + foreach (var action in _ctorParamActions) + { + action(parameter); + } + } + } +} \ No newline at end of file diff --git a/tools/AutoMapper/IMapper.cs b/tools/AutoMapper/IMapper.cs new file mode 100644 index 0000000000..7e64f5badb --- /dev/null +++ b/tools/AutoMapper/IMapper.cs @@ -0,0 +1,126 @@ +using System; + +namespace AutoMapper +{ + public interface IMapper + { + /// + /// Execute a mapping from the source object to a new destination object. + /// The source type is inferred from the source object. + /// + /// Destination type to create + /// Source object to map from + /// Mapped destination object + TDestination Map(object source); + + /// + /// Execute a mapping from the source object to a new destination object with supplied mapping options. + /// + /// Destination type to create + /// Source object to map from + /// Mapping options + /// Mapped destination object + TDestination Map(object source, Action opts); + + /// + /// Execute a mapping from the source object to a new destination object. + /// + /// Source type to use, regardless of the runtime type + /// Destination type to create + /// Source object to map from + /// Mapped destination object + TDestination Map(TSource source); + + /// + /// Execute a mapping from the source object to a new destination object with supplied mapping options. + /// + /// Source type to use + /// Destination type to create + /// Source object to map from + /// Mapping options + /// Mapped destination object + TDestination Map(TSource source, + Action> opts); + + /// + /// Execute a mapping from the source object to the existing destination object. + /// + /// Source type to use + /// Destination type + /// Source object to map from + /// Destination object to map into + /// The mapped destination object, same instance as the object + TDestination Map(TSource source, TDestination destination); + + /// + /// Execute a mapping from the source object to the existing destination object with supplied mapping options. + /// + /// Source type to use + /// Destination type + /// Source object to map from + /// Destination object to map into + /// Mapping options + /// The mapped destination object, same instance as the object + TDestination Map(TSource source, TDestination destination, + Action> opts); + + /// + /// Execute a mapping from the source object to a new destination object with explicit objects + /// + /// Source object to map from + /// Source type to use + /// Destination type to create + /// Mapped destination object + object Map(object source, Type sourceType, Type destinationType); + + /// + /// Execute a mapping from the source object to a new destination object with explicit objects and supplied mapping options. + /// + /// Source object to map from + /// Source type to use + /// Destination type to create + /// Mapping options + /// Mapped destination object + object Map(object source, Type sourceType, Type destinationType, Action opts); + + /// + /// Execute a mapping from the source object to existing destination object with explicit objects + /// + /// Source object to map from + /// Destination object to map into + /// Source type to use + /// Destination type to use + /// Mapped destination object, same instance as the object + object Map(object source, object destination, Type sourceType, Type destinationType); + + /// + /// Execute a mapping from the source object to existing destination object with supplied mapping options and explicit objects + /// + /// Source object to map from + /// Destination object to map into + /// Source type to use + /// Destination type to use + /// Mapping options + /// Mapped destination object, same instance as the object + object Map(object source, object destination, Type sourceType, Type destinationType, + Action opts); + + + /// + /// Configuration provider for performing maps + /// + IConfigurationProvider ConfigurationProvider { get; } + + /// + /// Factory method for creating runtime instances of converters, resolvers etc. + /// + Func ServiceCtor { get; } + } + + public interface IRuntimeMapper : IMapper + { + ResolutionContext DefaultContext { get; } + object Map(object source, object destination, Type sourceType, Type destinationType, ResolutionContext parent); + TDestination Map(TSource source, TDestination destination, ResolutionContext parent); + } +} diff --git a/tools/AutoMapper/IMapperConfigurationExpression.cs b/tools/AutoMapper/IMapperConfigurationExpression.cs new file mode 100644 index 0000000000..df5ac96a27 --- /dev/null +++ b/tools/AutoMapper/IMapperConfigurationExpression.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace AutoMapper +{ + public interface IMapperConfigurationExpression : IProfileExpression + { + /// + /// Create missing type maps during mapping, if necessary + /// + bool? CreateMissingTypeMaps { get; set; } + + /// + /// Add an existing profile + /// + /// Profile to add + void AddProfile(Profile profile); + + /// + /// Add an existing profile type. Profile will be instantiated and added to the configuration. + /// + /// Profile type + void AddProfile() where TProfile : Profile, new(); + + /// + /// Add an existing profile type. Profile will be instantiated and added to the configuration. + /// + /// Profile type + void AddProfile(Type profileType); + + /// + /// Add profiles contained in assemblies + /// + /// Assemblies containing profiles + void AddProfiles(IEnumerable assembliesToScan); + + /// + /// Add profiles contained in assemblies + /// + /// Assemblies containing profiles + void AddProfiles(params Assembly[] assembliesToScan); + + /// + /// Add profiles contained in assemblies + /// + /// Assembly names to load and scan containing profiles + void AddProfiles(IEnumerable assemblyNamesToScan); + + /// + /// Add profiles contained in assemblies + /// + /// Assembly names to load and scan containing profiles + void AddProfiles(params string[] assemblyNamesToScan); + + /// + /// Add profiles contained in assemblies + /// + /// Types from assemblies containing profiles + void AddProfiles(IEnumerable typesFromAssembliesContainingProfiles); + + /// + /// Add profiles contained in assemblies + /// + /// Types from assemblies containing profiles + void AddProfiles(params Type[] typesFromAssembliesContainingProfiles); + + /// + /// Supply a factory method callback for creating resolvers and type converters + /// + /// Factory method + void ConstructServicesUsing(Func constructor); + + /// + /// Create a named profile with the supplied configuration + /// + /// Profile name, must be unique + /// Profile configuration + void CreateProfile(string profileName, Action config); + + /// + /// Object mappers + /// + IList Mappers { get; } + + /// + /// Advance Configuration + /// + AdvancedConfiguration Advanced { get; } + } +} \ No newline at end of file diff --git a/tools/AutoMapper/IMappingAction.cs b/tools/AutoMapper/IMappingAction.cs new file mode 100644 index 0000000000..6d0c76f634 --- /dev/null +++ b/tools/AutoMapper/IMappingAction.cs @@ -0,0 +1,17 @@ +namespace AutoMapper +{ + /// + /// Custom mapping action + /// + /// Source type + /// Destination type + public interface IMappingAction + { + /// + /// Implementors can modify both the source and destination objects + /// + /// Source object + /// Destination object + void Process(TSource source, TDestination destination); + } +} \ No newline at end of file diff --git a/tools/AutoMapper/IMappingExpression.cs b/tools/AutoMapper/IMappingExpression.cs new file mode 100644 index 0000000000..c5aeae6dd2 --- /dev/null +++ b/tools/AutoMapper/IMappingExpression.cs @@ -0,0 +1,471 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; + +namespace AutoMapper +{ + /// + /// Mapping configuration options for non-generic maps + /// + public interface IMappingExpression + { + /// + /// Preserve object identity. Useful for circular references. + /// + /// + IMappingExpression PreserveReferences(); + + /// + /// Customize configuration for individual constructor parameter + /// + /// Constructor parameter name + /// Options + /// Itself + IMappingExpression ForCtorParam(string ctorParamName, Action> paramOptions); + + /// + /// Create a type mapping from the destination to the source type, using the destination members as validation. + /// + /// Itself + IMappingExpression ReverseMap(); + + /// + /// Replace the original runtime instance with a new source instance. Useful when ORMs return proxy types with no relationships to runtime types. + /// The returned source object will be mapped instead of what was supplied in the original source object. + /// + /// Substitution function + /// New source object to map. + IMappingExpression Substitute(Func substituteFunc); + + /// + /// Construct the destination object using the service locator + /// + /// Itself + IMappingExpression ConstructUsingServiceLocator(); + + /// + /// For self-referential types, limit recurse depth. + /// Enables PreserveReferences. + /// + /// Number of levels to limit to + /// Itself + IMappingExpression MaxDepth(int depth); + + /// + /// Supply a custom instantiation expression for the destination type for LINQ projection + /// + /// Callback to create the destination type given the source object + /// Itself + IMappingExpression ConstructProjectionUsing(LambdaExpression ctor); + + /// + /// Supply a custom instantiation function for the destination type, based on the entire resolution context + /// + /// Callback to create the destination type given the source object and current resolution context + /// Itself + IMappingExpression ConstructUsing(Func ctor); + + /// + /// Supply a custom instantiation function for the destination type + /// + /// Callback to create the destination type given the source object + /// Itself + IMappingExpression ConstructUsing(Func ctor); + + /// + /// Skip member mapping and use a custom expression during LINQ projection + /// + /// Projection expression + void ProjectUsing(Expression> projectionExpression); + + /// + /// Customize configuration for all members + /// + /// Callback for member options + void ForAllMembers(Action memberOptions); + + /// + /// Customize configuration for members not previously configured + /// + /// Callback for member options + void ForAllOtherMembers(Action memberOptions); + + /// + /// Customize configuration for an individual source member + /// + /// Source member name + /// Callback for member configuration options + /// Itself + IMappingExpression ForSourceMember(string sourceMemberName, Action memberOptions); + + /// + /// Skip normal member mapping and convert using a instantiated during mapping + /// + /// Type converter type + void ConvertUsing(); + + /// + /// Skip normal member mapping and convert using a instantiated during mapping + /// Use this method if you need to specify the converter type at runtime + /// + /// Type converter type + void ConvertUsing(Type typeConverterType); + + /// + /// Override the destination type mapping for looking up configuration and instantiation + /// + /// + void As(Type typeOverride); + + /// + /// Customize individual members + /// + /// Name of the member + /// Callback for configuring member + /// Itself + IMappingExpression ForMember(string name, Action memberOptions); + + /// + /// Include this configuration in derived types' maps + /// + /// Derived source type + /// Derived destination type + /// Itself + IMappingExpression Include(Type derivedSourceType, Type derivedDestinationType); + + /// + /// Ignores all destination properties that have either a private or protected setter, forcing the mapper to respect encapsulation (note: order matters, so place this before explicit configuration of any properties with an inaccessible setter) + /// + /// Itself + IMappingExpression IgnoreAllPropertiesWithAnInaccessibleSetter(); + + /// + /// When using ReverseMap, ignores all source properties that have either a private or protected setter, keeping the reverse mapping consistent with the forward mapping (note: destination properties with an inaccessible setter may still be mapped unless IgnoreAllPropertiesWithAnInaccessibleSetter is also used) + /// + /// Itself + IMappingExpression IgnoreAllSourcePropertiesWithAnInaccessibleSetter(); + + /// + /// Include the base type map's configuration in this map + /// + /// Base source type + /// Base destination type + /// + IMappingExpression IncludeBase(Type sourceBase, Type destinationBase); + + /// + /// Execute a custom function to the source and/or destination types before member mapping + /// + /// Callback for the source/destination types + /// Itself + IMappingExpression BeforeMap(Action beforeFunction); + + /// + /// Execute a custom mapping action before member mapping + /// + /// Mapping action type instantiated during mapping + /// Itself + IMappingExpression BeforeMap() + where TMappingAction : IMappingAction; + + /// + /// Execute a custom function to the source and/or destination types after member mapping + /// + /// Callback for the source/destination types + /// Itself + IMappingExpression AfterMap(Action afterFunction); + + /// + /// Execute a custom mapping action after member mapping + /// + /// Mapping action type instantiated during mapping + /// Itself + IMappingExpression AfterMap() + where TMappingAction : IMappingAction; + + IList ValueTransformers { get; } + + /// + /// Specify which member list to validate + /// + /// Member list to validate + /// Itself + IMappingExpression ValidateMemberList(MemberList memberList); + } + + /// + /// Mapping configuration options + /// + /// Source type + /// Destination type + public interface IMappingExpression + { + /// + /// Customize configuration for a path inside the destination object. + /// + /// Expression to the destination subobject + /// Callback for member options + /// Itself + IMappingExpression ForPath(Expression> destinationMember, + Action> memberOptions); + + /// + /// Preserve object identity. Useful for circular references. + /// + /// + IMappingExpression PreserveReferences(); + + /// + /// Customize configuration for members not previously configured + /// + /// Callback for member options + void ForAllOtherMembers(Action> memberOptions); + + /// + /// Customize configuration for individual member + /// + /// Expression to the top-level destination member. This must be a member on the TDestination type + /// Callback for member options + /// Itself + IMappingExpression ForMember(Expression> destinationMember, + Action> memberOptions); + + /// + /// Customize configuration for individual member. Used when the name isn't known at compile-time + /// + /// Destination member name + /// Callback for member options + /// + IMappingExpression ForMember(string name, + Action> memberOptions); + + /// + /// Customize configuration for all members + /// + /// Callback for member options + void ForAllMembers(Action> memberOptions); + + /// + /// Ignores all properties that have either a private or protected setter, forcing the mapper to respect encapsulation (note: order matters, so place this before explicit configuration of any properties with an inaccessible setter) + /// + /// Itself + IMappingExpression IgnoreAllPropertiesWithAnInaccessibleSetter(); + + /// + /// When using ReverseMap, ignores all properties that have either a private or protected setter, keeping the reverse mapping consistent with the forward mapping (note: properties with an inaccessible setter may still be mapped unless IgnoreAllPropertiesWithAnInaccessibleSetter is also used) + /// + /// Itself + IMappingExpression IgnoreAllSourcePropertiesWithAnInaccessibleSetter(); + + /// + /// Include this configuration in derived types' maps + /// + /// Derived source type + /// Derived destination type + /// Itself + IMappingExpression Include() + where TOtherSource : TSource + where TOtherDestination : TDestination; + + /// + /// Include the base type map's configuration in this map + /// + /// Base source type + /// Base destination type + /// Itself + IMappingExpression IncludeBase(); + + /// + /// Include this configuration in derived types' maps + /// + /// Derived source type + /// Derived destination type + /// Itself + IMappingExpression Include(Type derivedSourceType, Type derivedDestinationType); + + /// + /// Skip member mapping and use a custom expression during LINQ projection + /// + /// Projection expression + void ProjectUsing(Expression> projectionExpression); + + /// + /// Skip member mapping and use a custom function to convert to the destination type + /// + /// Callback to convert from source type to destination type + void ConvertUsing(Func mappingFunction); + + /// + /// Skip member mapping and use a custom function to convert to the destination type + /// + /// Callback to convert from source type to destination type, including destination object + void ConvertUsing(Func mappingFunction); + + /// + /// Skip member mapping and use a custom function to convert to the destination type + /// + /// Callback to convert from source type to destination type, with source, destination and context + void ConvertUsing(Func mappingFunction); + + /// + /// Skip member mapping and use a custom type converter instance to convert to the destination type + /// + /// Type converter instance + void ConvertUsing(ITypeConverter converter); + + /// + /// Skip member mapping and use a custom type converter instance to convert to the destination type + /// + /// Type converter type + void ConvertUsing() where TTypeConverter : ITypeConverter; + + /// + /// Execute a custom function to the source and/or destination types before member mapping + /// + /// Callback for the source/destination types + /// Itself + IMappingExpression BeforeMap(Action beforeFunction); + + /// + /// Execute a custom function to the source and/or destination types before member mapping + /// + /// Callback for the source/destination types + /// Itself + IMappingExpression BeforeMap(Action beforeFunction); + + /// + /// Execute a custom mapping action before member mapping + /// + /// Mapping action type instantiated during mapping + /// Itself + IMappingExpression BeforeMap() + where TMappingAction : IMappingAction; + + /// + /// Execute a custom function to the source and/or destination types after member mapping + /// + /// Callback for the source/destination types + /// Itself + IMappingExpression AfterMap(Action afterFunction); + + /// + /// Execute a custom function to the source and/or destination types after member mapping + /// + /// Callback for the source/destination types + /// Itself + IMappingExpression AfterMap(Action afterFunction); + + /// + /// Execute a custom mapping action after member mapping + /// + /// Mapping action type instantiated during mapping + /// Itself + IMappingExpression AfterMap() + where TMappingAction : IMappingAction; + + /// + /// Supply a custom instantiation function for the destination type + /// + /// Callback to create the destination type given the source object + /// Itself + IMappingExpression ConstructUsing(Func ctor); + + /// + /// Supply a custom instantiation expression for the destination type for LINQ projection + /// + /// Callback to create the destination type given the source object + /// Itself + IMappingExpression ConstructProjectionUsing(Expression> ctor); + + /// + /// Supply a custom instantiation function for the destination type, based on the entire resolution context + /// + /// Callback to create the destination type given the current resolution context + /// Itself + IMappingExpression ConstructUsing(Func ctor); + + /// + /// Override the destination type mapping for looking up configuration and instantiation + /// + /// Destination type to use + void As() where T : TDestination; + + /// + /// For self-referential types, limit recurse depth. + /// Enables PreserveReferences. + /// + /// Number of levels to limit to + /// Itself + IMappingExpression MaxDepth(int depth); + + /// + /// Construct the destination object using the service locator + /// + /// Itself + IMappingExpression ConstructUsingServiceLocator(); + + /// + /// Create a type mapping from the destination to the source type, using the members as validation + /// + /// Itself + IMappingExpression ReverseMap(); + + /// + /// Customize configuration for an individual source member + /// + /// Expression to source member. Must be a member of the type + /// Callback for member configuration options + /// Itself + IMappingExpression ForSourceMember(Expression> sourceMember, + Action memberOptions); + + /// + /// Customize configuration for an individual source member. Member name not known until runtime + /// + /// Expression to source member. Must be a member of the type + /// Callback for member configuration options + /// Itself + IMappingExpression ForSourceMember(string sourceMemberName, + Action memberOptions); + + /// + /// Replace the original runtime instance with a new source instance. Useful when ORMs return proxy types with no relationships to runtime types. + /// The returned source object will be mapped instead of what was supplied in the original source object. + /// + /// Substitution function + /// New source object to map. + IMappingExpression Substitute(Func substituteFunc); + + /// + /// Customize configuration for individual constructor parameter + /// + /// Constructor parameter name + /// Options + /// Itself + IMappingExpression ForCtorParam(string ctorParamName, Action> paramOptions); + + /// + /// Disable constructor validation. During mapping this map is used against an existing destination object and never constructed itself. + /// + /// Itself + IMappingExpression DisableCtorValidation(); + + IList ValueTransformers { get; } + + /// + /// Apply a transformation function after any resolved destination member value with the given type + /// + /// Value type to match and transform + /// Transformation expression + /// Itself + IMappingExpression AddTransform(Expression> transformer); + + /// + /// Specify which member list to validate + /// + /// Member list to validate + /// Itself + IMappingExpression ValidateMemberList(MemberList memberList); + + } +} \ No newline at end of file diff --git a/tools/AutoMapper/IMappingOperationOptions.cs b/tools/AutoMapper/IMappingOperationOptions.cs new file mode 100644 index 0000000000..051d33069a --- /dev/null +++ b/tools/AutoMapper/IMappingOperationOptions.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; + +namespace AutoMapper +{ + /// + /// Options for a single map operation + /// + public interface IMappingOperationOptions + { + T CreateInstance(); + + Func ServiceCtor { get; } + + /// + /// Construct services using this callback. Use this for child/nested containers + /// + /// + void ConstructServicesUsing(Func constructor); + + /// + /// Add context items to be accessed at map time inside an or + /// + IDictionary Items { get; } + + /// + /// Execute a custom function to the source and/or destination types before member mapping + /// + /// Callback for the source/destination types + void BeforeMap(Action beforeFunction); + + /// + /// Execute a custom function to the source and/or destination types after member mapping + /// + /// Callback for the source/destination types + void AfterMap(Action afterFunction); + } + + public interface IMappingOperationOptions : IMappingOperationOptions + { + /// + /// Execute a custom function to the source and/or destination types before member mapping + /// + /// Callback for the source/destination types + void BeforeMap(Action beforeFunction); + + /// + /// Execute a custom function to the source and/or destination types after member mapping + /// + /// Callback for the source/destination types + void AfterMap(Action afterFunction); + + /// + /// Configure inline map + /// + /// Mapping configuration expression + IMappingExpression ConfigureMap(); + + /// + /// Configure inline map with member list to validate + /// + /// Member list to validate for the inline map + /// Mapping configuration expression + IMappingExpression ConfigureMap(MemberList memberList); + } +} \ No newline at end of file diff --git a/tools/AutoMapper/IMemberConfigurationExpression.cs b/tools/AutoMapper/IMemberConfigurationExpression.cs new file mode 100644 index 0000000000..dbaf26a7be --- /dev/null +++ b/tools/AutoMapper/IMemberConfigurationExpression.cs @@ -0,0 +1,238 @@ +using System; +using System.Linq.Expressions; +using System.Reflection; + +namespace AutoMapper +{ + /// + /// Member configuration options + /// + /// Source type for this member + /// Type for this member + /// Destination type for this map + public interface IMemberConfigurationExpression + { + /// + /// Do not precompute the execution plan for this member, just map it at runtime. + /// Simplifies the execution plan by not inlining. + /// + void MapAtRuntime(); + + /// + /// Substitute a custom value when the source member resolves as null + /// + /// Value to use + void NullSubstitute(object nullSubstitute); + + /// + /// Resolve destination member using a custom value resolver + /// + /// Value resolver type + /// Value resolver configuration options + void ResolveUsing() + where TValueResolver : IValueResolver; + + /// + /// Resolve destination member using a custom value resolver from a source member + /// + /// Value resolver type + /// Source member to supply + /// Value resolver configuration options + void ResolveUsing(Expression> sourceMember) + where TValueResolver : IMemberValueResolver; + + /// + /// Resolve destination member using a custom value resolver from a source member + /// + /// Value resolver type + /// Source member to supply + /// Source member name + /// Value resolver configuration options + void ResolveUsing(string sourceMemberName) + where TValueResolver : IMemberValueResolver; + + /// + /// Resolve destination member using a custom value resolver instance + /// + /// Value resolver instance to use + /// Resolution expression + void ResolveUsing(IValueResolver valueResolver); + + /// + /// Resolve destination member using a custom value resolver instance + /// + /// Value resolver instance to use + /// Source member to supply to value resolver + /// Resolution expression + void ResolveUsing(IMemberValueResolver valueResolver, Expression> sourceMember); + + /// + /// Resolve destination member using a custom value resolver callback. Used instead of MapFrom when not simply redirecting a source member + /// This method cannot be used in conjunction with LINQ query projection + /// + /// Callback function to resolve against source type + void ResolveUsing(Func resolver); + + /// + /// Resolve destination member using a custom value resolver callback. Used instead of MapFrom when not simply redirecting a source member + /// Access both the source object and destination member for additional mapping, context items + /// This method cannot be used in conjunction with LINQ query projection + /// + /// Callback function to resolve against source type + void ResolveUsing(Func resolver); + + /// + /// Resolve destination member using a custom value resolver callback. Used instead of MapFrom when not simply redirecting a source member + /// Access both the source object and destination member for additional mapping, context items + /// This method cannot be used in conjunction with LINQ query projection + /// + /// Callback function to resolve against source type + void ResolveUsing(Func resolver); + + /// + /// Resolve destination member using a custom value resolver callback. Used instead of MapFrom when not simply redirecting a source member + /// Access both the source object and current resolution context for additional mapping, context items + /// This method cannot be used in conjunction with LINQ query projection + /// + /// Callback function to resolve against source type + void ResolveUsing(Func resolver); + + /// + /// Specify the source member to map from. Can only reference a member on the type + /// This method can be used in mapping to LINQ query projections, while ResolveUsing cannot. + /// Any null reference exceptions in this expression will be ignored (similar to flattening behavior) + /// + /// Member type of the source member to use + /// Expression referencing the source member to map against + void MapFrom(Expression> sourceMember); + + /// + /// Specify the source member to map from. Can only reference a member on the type + /// This method can be used in mapping to LINQ query projections, while ResolveUsing cannot. + /// Any null reference exceptions in this expression will be ignored (similar to flattening behavior) + /// + /// Propertyname referencing the source member to map against + void MapFrom(string property); + + /// + /// Ignore this member for configuration validation and skip during mapping + /// + void Ignore(); + + /// + /// Allow this member to be null. This prevents generating a check condition for it. + /// + void AllowNull(); + + /// + /// Supply a custom mapping order instead of what the .NET runtime returns + /// + /// Mapping order value + void SetMappingOrder(int mappingOrder); + + /// + /// Use the destination value instead of mapping from the source value or creating a new instance + /// + void UseDestinationValue(); + + /// + /// Use a custom value + /// + /// Value type + /// Value to use + void UseValue(TValue value); + + /// + /// Conditionally map this member against the source, destination, source and destination members + /// + /// Condition to evaluate using the source object + void Condition(Func condition); + + /// + /// Conditionally map this member + /// + /// Condition to evaluate using the source object + void Condition(Func condition); + + /// + /// Conditionally map this member + /// + /// Condition to evaluate using the source object + void Condition(Func condition); + + /// + /// Conditionally map this member + /// + /// Condition to evaluate using the source object + void Condition(Func condition); + + /// + /// Conditionally map this member + /// + /// Condition to evaluate using the source object + void Condition(Func condition); + + /// + /// Conditionally map this member, evaluated before accessing the source value + /// + /// Condition to evaluate using the source object + void PreCondition(Func condition); + + /// + /// Conditionally map this member, evaluated before accessing the source value + /// + /// Condition to evaluate using the current resolution context + void PreCondition(Func condition); + + /// + /// Conditionally map this member, evaluated before accessing the source value + /// + /// Condition to evaluate using the source object and the current resolution context + void PreCondition(Func condition); + + /// + /// Ignore this member for LINQ projections unless explicitly expanded during projection + /// + void ExplicitExpansion(); + + /// + /// The destination member being configured. + /// + MemberInfo DestinationMember { get; } + + /// + /// Apply a transformation function after any resolved destination member value with the given type + /// + /// Transformation expression + void AddTransform(Expression> transformer); + } + + /// + /// Configuration options for an individual member + /// + public interface IMemberConfigurationExpression : IMemberConfigurationExpression + { + /// + /// Resolve destination member using a custom value resolver. Used when the value resolver is not known at compile-time + /// + /// Value resolver type + /// Value resolver configuration options + void ResolveUsing(Type valueResolverType); + + /// + /// Resolve destination member using a custom value resolver. Used when the value resolver is not known at compile-time + /// + /// Value resolver type + /// Member to supply to value resolver + /// Value resolver configuration options + void ResolveUsing(Type valueResolverType, string memberName); + + /// + /// Resolve destination member using a custom value resolver instance + /// + /// Value resolver instance to use + /// Source member to supply to value resolver + /// Resolution expression + void ResolveUsing(IMemberValueResolver valueResolver, string memberName); + } +} \ No newline at end of file diff --git a/tools/AutoMapper/INamingConvention.cs b/tools/AutoMapper/INamingConvention.cs new file mode 100644 index 0000000000..66a01a24b8 --- /dev/null +++ b/tools/AutoMapper/INamingConvention.cs @@ -0,0 +1,19 @@ +using System.Text.RegularExpressions; + +namespace AutoMapper +{ + /// + /// Defines a naming convention strategy + /// + public interface INamingConvention + { + /// + /// Regular expression on how to tokenize a member + /// + Regex SplittingExpression { get; } + + string SeparatorCharacter { get; } + + string ReplaceValue(Match match); + } +} \ No newline at end of file diff --git a/tools/AutoMapper/IObjectMapper.cs b/tools/AutoMapper/IObjectMapper.cs new file mode 100644 index 0000000000..92e13ad4ed --- /dev/null +++ b/tools/AutoMapper/IObjectMapper.cs @@ -0,0 +1,70 @@ +using System.Linq.Expressions; +using System.Reflection; +using AutoMapper.Internal; + +namespace AutoMapper +{ + using static Expression; + using static ExpressionFactory; + + /// + /// Mapping execution strategy, as a chain of responsibility + /// + public interface IObjectMapper + { + /// + /// When true, the mapping engine will use this mapper as the strategy + /// + /// Resolution context + /// Is match + bool IsMatch(TypePair context); + + /// + /// Builds a mapping expression equivalent to the base Map method + /// + /// + /// + /// + /// Source parameter + /// Destination parameter + /// ResulotionContext parameter + /// Map expression + Expression MapExpression(IConfigurationProvider configurationProvider, ProfileMap profileMap, PropertyMap propertyMap, Expression sourceExpression, Expression destExpression, Expression contextExpression); + } + + /// + /// Base class for simple object mappers that don't want to use expressions. + /// + /// type of the source + /// type of the destination + public abstract class ObjectMapper : IObjectMapper + { + private static readonly MethodInfo MapMethod = typeof(ObjectMapper).GetDeclaredMethod("Map"); + + /// + /// When true, the mapping engine will use this mapper as the strategy + /// + /// Resolution context + /// Is match + public abstract bool IsMatch(TypePair context); + + /// + /// Performs conversion from source to destination type + /// + /// Source object + /// Destination object + /// Resolution context + /// Destination object + public abstract TDestination Map(TSource source, TDestination destination, ResolutionContext context); + + public Expression MapExpression(IConfigurationProvider configurationProvider, ProfileMap profileMap, + PropertyMap propertyMap, Expression sourceExpression, Expression destExpression, + Expression contextExpression) => + Call( + Constant(this), + MapMethod, + ToType(sourceExpression, typeof(TSource)), + ToType(destExpression, typeof(TDestination)), + contextExpression); + } +} \ No newline at end of file diff --git a/tools/AutoMapper/IObjectMapperInfo.cs b/tools/AutoMapper/IObjectMapperInfo.cs new file mode 100644 index 0000000000..ee22239254 --- /dev/null +++ b/tools/AutoMapper/IObjectMapperInfo.cs @@ -0,0 +1,7 @@ +namespace AutoMapper +{ + public interface IObjectMapperInfo : IObjectMapper + { + TypePair GetAssociatedTypes(TypePair initialTypes); + } +} \ No newline at end of file diff --git a/tools/AutoMapper/IPathConfigurationExpression.cs b/tools/AutoMapper/IPathConfigurationExpression.cs new file mode 100644 index 0000000000..7df7818090 --- /dev/null +++ b/tools/AutoMapper/IPathConfigurationExpression.cs @@ -0,0 +1,47 @@ +using System; +using System.Linq.Expressions; + +namespace AutoMapper +{ + /// + /// Member configuration options + /// + /// Source type for this member + /// Destination type for this map + /// Type for this member + public interface IPathConfigurationExpression + { + /// + /// Specify the source member to map from. Can only reference a member on the type + /// This method can be used in mapping to LINQ query projections, while ResolveUsing cannot. + /// Any null reference exceptions in this expression will be ignored (similar to flattening behavior) + /// + /// Member type of the source member to use + /// Expression referencing the source member to map against + void MapFrom(Expression> sourceMember); + + /// + /// Ignore this member for configuration validation and skip during mapping + /// + void Ignore(); + + void Condition(Func, bool> condition); + } + + public struct ConditionParameters + { + public ConditionParameters(TSource source, TDestination destination, TMember sourceMember, TMember destinationMember, ResolutionContext context) + { + Source = source; + Destination = destination; + SourceMember = sourceMember; + DestinationMember = destinationMember; + Context = context; + } + public TSource Source { get; } + public TDestination Destination { get; } + public TMember SourceMember { get; } + public TMember DestinationMember { get; } + public ResolutionContext Context { get; } + } +} \ No newline at end of file diff --git a/tools/AutoMapper/IProfileExpression.cs b/tools/AutoMapper/IProfileExpression.cs new file mode 100644 index 0000000000..8cecf71947 --- /dev/null +++ b/tools/AutoMapper/IProfileExpression.cs @@ -0,0 +1,168 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Reflection; +using AutoMapper.Configuration.Conventions; +using AutoMapper.Mappers; + +namespace AutoMapper +{ + /// + /// Configuration for profile-specific maps + /// + public interface IProfileExpression + { + /// + /// Disable constructor mapping. Use this if you don't intend to have AutoMapper try to map to constructors + /// + void DisableConstructorMapping(); + + /// + /// Creates a mapping configuration from the type to the type + /// + /// Source type + /// Destination type + /// Mapping expression for more configuration options + IMappingExpression CreateMap(); + + /// + /// Creates a mapping configuration from the type to the type. + /// Specify the member list to validate against during configuration validation. + /// + /// Source type + /// Destination type + /// Member list to validate + /// Mapping expression for more configuration options + IMappingExpression CreateMap(MemberList memberList); + + /// + /// Create a mapping configuration from the source type to the destination type. + /// Use this method when the source and destination type are known at runtime and not compile time. + /// + /// Source type + /// Destination type + /// Mapping expression for more configuration options + IMappingExpression CreateMap(Type sourceType, Type destinationType); + + /// + /// Creates a mapping configuration from the source type to the destination type. + /// Specify the member list to validate against during configuration validation. + /// + /// Source type + /// Destination type + /// Member list to validate + /// Mapping expression for more configuration options + IMappingExpression CreateMap(Type sourceType, Type destinationType, MemberList memberList); + + /// + /// Clear the list of recognized prefixes. + /// + void ClearPrefixes(); + + /// + /// Recognize a list of prefixes to be removed from source member names when matching + /// + /// List of prefixes + void RecognizePrefixes(params string[] prefixes); + + /// + /// Recognize a list of postfixes to be removed from source member names when matching + /// + /// List of postfixes + void RecognizePostfixes(params string[] postfixes); + + /// + /// Provide an alias for a member name when matching source member names + /// + /// Original member name + /// Alias to match against + void RecognizeAlias(string original, string alias); + + + /// + /// Provide a new value for a part of a members name + /// + /// Original member value + /// New member value + void ReplaceMemberName(string original, string newValue); + + /// + /// Recognize a list of prefixes to be removed from destination member names when matching + /// + /// List of prefixes + void RecognizeDestinationPrefixes(params string[] prefixes); + + /// + /// Recognize a list of postfixes to be removed from destination member names when matching + /// + /// List of postfixes + void RecognizeDestinationPostfixes(params string[] postfixes); + + /// + /// Add a property name to globally ignore. Matches against the beginning of the property names. + /// + /// Property name to match against + void AddGlobalIgnore(string propertyNameStartingWith); + + /// + /// Allow null destination values. If false, destination objects will be created for deep object graphs. Default true. + /// + bool? AllowNullDestinationValues { get; set; } + + /// + /// Allow null destination collections. If true, null source collections result in null destination collections. Default false. + /// + bool? AllowNullCollections { get; set; } + + /// + /// Allows to enable null-value propagation for query mapping. + /// Some providers (such as EntityFrameworkQueryVisitor) do not work with this feature enabled! + /// + bool? EnableNullPropagationForQueryMapping { get; set; } + + /// + /// Naming convention for source members + /// + INamingConvention SourceMemberNamingConvention { get; set; } + + /// + /// Naming convention for destination members + /// + INamingConvention DestinationMemberNamingConvention { get; set; } + + /// + /// Specify common configuration for all type maps. + /// + /// configuration callback + void ForAllMaps(Action configuration); + + /// + /// Customize configuration for all members across all maps + /// + /// Condition + /// Callback for member options. Use the property map for conditional maps. + void ForAllPropertyMaps(Func condition, Action memberOptions); + + Func ShouldMapProperty { get; set; } + Func ShouldMapField { get; set; } + string ProfileName { get; } + IMemberConfiguration AddMemberConfiguration(); + IConditionalObjectMapper AddConditionalObjectMapper(); + + /// + /// Include extension methods against source members for matching destination members to. Default source extension methods from + /// + /// Static type that contains extension methods + void IncludeSourceExtensionMethods(Type type); + + /// + /// Value transformers. Modify the list directly or use + /// + IList ValueTransformers { get; } + + /// + /// Validate maps created dynamically/inline on the first map. Defaults to true. + /// + bool? ValidateInlineMaps { get; set; } + } +} \ No newline at end of file diff --git a/tools/AutoMapper/IResolutionExpression.cs b/tools/AutoMapper/IResolutionExpression.cs new file mode 100644 index 0000000000..5a679afa4f --- /dev/null +++ b/tools/AutoMapper/IResolutionExpression.cs @@ -0,0 +1,30 @@ +using System; +using System.Linq.Expressions; + +namespace AutoMapper +{ + /// + /// Custom resolver options + /// + /// Source type + public interface IResolutionExpression : IResolutionExpression + { + /// + /// Use the specified member as the input to the resolver instead of the root object + /// + /// Expression for the source member + void FromMember(Expression> sourceMember); + } + + /// + /// Custom resolver options + /// + public interface IResolutionExpression + { + /// + /// Use the supplied member as the input to the resolver instead of the root source object + /// + /// Property name to use + void FromMember(string sourcePropertyName); + } +} \ No newline at end of file diff --git a/tools/AutoMapper/IResolverConfigurationExpression.cs b/tools/AutoMapper/IResolverConfigurationExpression.cs new file mode 100644 index 0000000000..cc9a23b7e4 --- /dev/null +++ b/tools/AutoMapper/IResolverConfigurationExpression.cs @@ -0,0 +1,26 @@ +using System; +using System.Linq.Expressions; + +namespace AutoMapper +{ + /// + /// Custom resolver options + /// + /// Source type + public interface IResolverConfigurationExpression + { + /// + /// Use the specified member as the input to the resolver instead of the root object + /// + /// Expression for the source member + /// Itself + void FromMember(Expression> sourceMember); + + /// + /// Use the specified member as the input to the resolver instead of the root object + /// + /// Name of the source member + /// Itself + void FromMember(string sourcePropertyName); + } +} \ No newline at end of file diff --git a/tools/AutoMapper/ISourceMemberConfigurationExpression.cs b/tools/AutoMapper/ISourceMemberConfigurationExpression.cs new file mode 100644 index 0000000000..96f92d231d --- /dev/null +++ b/tools/AutoMapper/ISourceMemberConfigurationExpression.cs @@ -0,0 +1,13 @@ +namespace AutoMapper +{ + /// + /// Source member configuration options + /// + public interface ISourceMemberConfigurationExpression + { + /// + /// Ignore this member for configuration validation and skip during mapping + /// + void Ignore(); + } +} \ No newline at end of file diff --git a/tools/AutoMapper/ITypeConverter.cs b/tools/AutoMapper/ITypeConverter.cs new file mode 100644 index 0000000000..5194049679 --- /dev/null +++ b/tools/AutoMapper/ITypeConverter.cs @@ -0,0 +1,19 @@ +namespace AutoMapper +{ + /// + /// Converts source type to destination type instead of normal member mapping + /// + /// Source type + /// Destination type + public interface ITypeConverter + { + /// + /// Performs conversion from source to destination type + /// + /// Source object + /// Destination object + /// Resolution context + /// Destination object + TDestination Convert(TSource source, TDestination destination, ResolutionContext context); + } +} diff --git a/tools/AutoMapper/IValueResolver.cs b/tools/AutoMapper/IValueResolver.cs new file mode 100644 index 0000000000..49ca4bf567 --- /dev/null +++ b/tools/AutoMapper/IValueResolver.cs @@ -0,0 +1,35 @@ +namespace AutoMapper +{ + /// + /// Extension point to provide custom resolution for a destination value + /// + public interface IValueResolver + { + /// + /// Implementors use source object to provide a destination object. + /// + /// Source object + /// Destination object, if exists + /// Destination member + /// The context of the mapping + /// Result, typically build from the source resolution result + TDestMember Resolve(TSource source, TDestination destination, TDestMember destMember, ResolutionContext context); + } + + /// + /// Extension point to provide custom resolution for a destination value + /// + public interface IMemberValueResolver + { + /// + /// Implementors use source object to provide a destination object. + /// + /// Source object + /// Destination object, if exists + /// Source member + /// Destination member + /// The context of the mapping + /// Result, typically build from the source resolution result + TDestMember Resolve(TSource source, TDestination destination, TSourceMember sourceMember, TDestMember destMember, ResolutionContext context); + } +} diff --git a/tools/AutoMapper/IgnoreMapAttribute.cs b/tools/AutoMapper/IgnoreMapAttribute.cs new file mode 100644 index 0000000000..66d7fff705 --- /dev/null +++ b/tools/AutoMapper/IgnoreMapAttribute.cs @@ -0,0 +1,12 @@ +using System; + +namespace AutoMapper +{ + /// + /// Ignore this member for validation and skip during mapping + /// + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] + public class IgnoreMapAttribute : Attribute + { + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Internal/ExpressionFactory.cs b/tools/AutoMapper/Internal/ExpressionFactory.cs new file mode 100644 index 0000000000..6216284c60 --- /dev/null +++ b/tools/AutoMapper/Internal/ExpressionFactory.cs @@ -0,0 +1,242 @@ +using System; +using System.Diagnostics; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using AutoMapper.Configuration; + +namespace AutoMapper.Internal +{ + using static Expression; + + public static class ExpressionFactory + { + public static MemberExpression MemberAccesses(string members, Expression obj) => + (MemberExpression) ReflectionHelper.GetMemberPath(obj.Type, members).MemberAccesses(obj); + + public static Expression GetSetter(MemberExpression memberExpression) + { + var propertyOrField = memberExpression.Member; + return ReflectionHelper.CanBeSet(propertyOrField) ? + MakeMemberAccess(memberExpression.Expression, propertyOrField) : + null; + } + + public static MethodInfo Method(Expression> expression) => GetExpressionBodyMethod(expression); + + public static MethodInfo Method(Expression> expression) => GetExpressionBodyMethod(expression); + + private static MethodInfo GetExpressionBodyMethod(LambdaExpression expression) => ((MethodCallExpression) expression.Body).Method; + + public static Expression ForEach(Expression collection, ParameterExpression loopVar, Expression loopContent) + { + if(collection.Type.IsArray) + { + return ForEachArrayItem(collection, arrayItem => Block(new[] { loopVar }, Assign(loopVar, arrayItem), loopContent)); + } + var getEnumerator = collection.Type.GetInheritedMethod("GetEnumerator"); + var getEnumeratorCall = Call(collection, getEnumerator); + var enumeratorType = getEnumeratorCall.Type; + var enumeratorVar = Variable(enumeratorType, "enumerator"); + var enumeratorAssign = Assign(enumeratorVar, getEnumeratorCall); + + var moveNext = enumeratorType.GetInheritedMethod("MoveNext"); + var moveNextCall = Call(enumeratorVar, moveNext); + + var breakLabel = Label("LoopBreak"); + + var loop = Block(new[] { enumeratorVar }, + enumeratorAssign, + Loop( + IfThenElse( + Equal(moveNextCall, Constant(true)), + Block(new[] { loopVar }, + Assign(loopVar, ToType(Property(enumeratorVar, "Current"), loopVar.Type)), + loopContent + ), + Break(breakLabel) + ), + breakLabel) + ); + + return loop; + } + + public static Expression ForEachArrayItem(Expression array, Func body) + { + var length = Property(array, "Length"); + return For(length, index => body(ArrayAccess(array, index))); + } + + public static Expression For(Expression count, Func body) + { + var breakLabel = Label("LoopBreak"); + var index = Variable(typeof(int), "sourceArrayIndex"); + var initialize = Assign(index, Constant(0, typeof(int))); + var loop = Block(new[] { index }, + initialize, + Loop( + IfThenElse( + LessThan(index, count), + Block(body(index), PostIncrementAssign(index)), + Break(breakLabel) + ), + breakLabel) + ); + return loop; + } + + public static Expression ToObject(Expression expression) => ToType(expression, typeof(object)); + + public static Expression ToType(Expression expression, Type type) => expression.Type == type ? expression : Convert(expression, type); + + public static Expression ReplaceParameters(LambdaExpression exp, params Expression[] replace) + { + var replaceExp = exp.Body; + for (var i = 0; i < Math.Min(replace.Length, exp.Parameters.Count); i++) + replaceExp = Replace(replaceExp, exp.Parameters[i], replace[i]); + return replaceExp; + } + + public static Expression ConvertReplaceParameters(LambdaExpression exp, params Expression[] replace) + { + var replaceExp = exp.Body; + for (var i = 0; i < Math.Min(replace.Length, exp.Parameters.Count); i++) + replaceExp = new ConvertingVisitor(exp.Parameters[i], replace[i]).Visit(replaceExp); + return replaceExp; + } + + public static Expression Replace(Expression exp, Expression old, Expression replace) => new ReplaceExpressionVisitor(old, replace).Visit(exp); + + public static LambdaExpression Concat(LambdaExpression expr, LambdaExpression concat) => (LambdaExpression)new ExpressionConcatVisitor(expr).Visit(concat); + + public static Expression NullCheck(Expression expression, Type destinationType) + { + var target = expression; + Expression nullConditions = Constant(false); + do + { + if(target is MemberExpression member) + { + target = member.Expression; + NullCheck(); + } + else if(target is MethodCallExpression method) + { + target = method.Method.IsStatic() ? method.Arguments.FirstOrDefault() : method.Object; + NullCheck(); + } + else if(target?.NodeType == ExpressionType.Parameter) + { + var returnType = Nullable.GetUnderlyingType(destinationType) == expression.Type ? destinationType : expression.Type; + var nullCheck = Condition(nullConditions, Default(returnType), ToType(expression, returnType)); + return nullCheck; + } + else + { + return expression; + } + } + while(true); + void NullCheck() + { + if(target == null || target.Type.IsValueType()) + { + return; + } + nullConditions = OrElse(Equal(target, Constant(null, target.Type)), nullConditions); + } + } + + public static Expression IfNullElse(Expression expression, Expression then, Expression @else = null) + { + var nonNullElse = ToType(@else ?? Default(then.Type), then.Type); + if(expression.Type.IsValueType() && !expression.Type.IsNullableType()) + { + return nonNullElse; + } + return Condition(Equal(expression, Constant(null)), then, nonNullElse); + } + + internal class ConvertingVisitor : ExpressionVisitor + { + private readonly Expression _newParam; + private readonly ParameterExpression _oldParam; + + public ConvertingVisitor(ParameterExpression oldParam, Expression newParam) + { + _newParam = newParam; + _oldParam = oldParam; + } + + protected override Expression VisitMember(MemberExpression node) + { + if (node.Expression == _oldParam) + { + node = MakeMemberAccess(ToType(_newParam, _oldParam.Type), node.Member); + } + + return base.VisitMember(node); + } + + protected override Expression VisitParameter(ParameterExpression node) => + node == _oldParam + ? ToType(_newParam, _oldParam.Type) + : base.VisitParameter(node); + + protected override Expression VisitMethodCall(MethodCallExpression node) + { + if (node.Object == _oldParam) + { + node = Call(ToType(_newParam, _oldParam.Type), node.Method, node.Arguments); + } + + return base.VisitMethodCall(node); + } + } + + internal class ReplaceExpressionVisitor : ExpressionVisitor + { + private readonly Expression _oldExpression; + private readonly Expression _newExpression; + + public ReplaceExpressionVisitor(Expression oldExpression, Expression newExpression) + { + _oldExpression = oldExpression; + _newExpression = newExpression; + } + + public override Expression Visit(Expression node) + { + if (_oldExpression == node) + node = _newExpression; + + return base.Visit(node); + } + } + + internal class ExpressionConcatVisitor : ExpressionVisitor + { + private readonly LambdaExpression _overrideExpression; + + public ExpressionConcatVisitor(LambdaExpression overrideExpression) => _overrideExpression = overrideExpression; + + public override Expression Visit(Expression node) + { + if (_overrideExpression == null) + return node; + if (node.NodeType != ExpressionType.Lambda && node.NodeType != ExpressionType.Parameter) + { + var expression = node; + if (node.Type == typeof(object)) + expression = Convert(node, _overrideExpression.Parameters[0].Type); + + return ReplaceParameters(_overrideExpression, expression); + } + return base.Visit(node); + } + + protected override Expression VisitLambda(Expression node) => Lambda(Visit(node.Body), node.Parameters); + } + } +} diff --git a/tools/AutoMapper/Internal/MemberVisitor.cs b/tools/AutoMapper/Internal/MemberVisitor.cs new file mode 100644 index 0000000000..e1deb53ac6 --- /dev/null +++ b/tools/AutoMapper/Internal/MemberVisitor.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; + +namespace AutoMapper.Internal +{ + public class MemberVisitor : ExpressionVisitor + { + public static IEnumerable GetMemberPath(Expression expression) + { + var memberVisitor = new MemberVisitor(); + memberVisitor.Visit(expression); + return memberVisitor.MemberPath; + } + + protected override Expression VisitMember(MemberExpression node) + { + MemberPath = node.GetMembers().Select(e=>e.Member); + return node; + } + + public IEnumerable MemberPath { get; private set; } + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Internal/ReflectionHelper.cs b/tools/AutoMapper/Internal/ReflectionHelper.cs new file mode 100644 index 0000000000..fe78ff0cb1 --- /dev/null +++ b/tools/AutoMapper/Internal/ReflectionHelper.cs @@ -0,0 +1,190 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Dynamic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; + +namespace AutoMapper.Internal +{ + using Configuration; + + public static class ReflectionHelper + { + public static bool CanBeSet(MemberInfo propertyOrField) + { + return propertyOrField is FieldInfo field ? + !field.IsInitOnly : + ((PropertyInfo)propertyOrField).CanWrite; + } + + public static object GetDefaultValue(ParameterInfo parameter) + { + if (parameter.DefaultValue == null && parameter.ParameterType.IsValueType()) + { + return Activator.CreateInstance(parameter.ParameterType); + } + return parameter.DefaultValue; + } + + public static object MapMember(ResolutionContext context, MemberInfo member, object value, object destination) + { + var memberType = GetMemberType(member); + var destValue = GetMemberValue(member, destination); + return context.Mapper.Map(value, destValue, value?.GetType() ?? memberType, memberType, context); + } + + public static object MapMember(ResolutionContext context, MemberInfo member, object value) + { + var memberType = GetMemberType(member); + return context.Mapper.Map(value, null, value?.GetType() ?? memberType, memberType, context); + } + + public static bool IsDynamic(object obj) => obj is IDynamicMetaObjectProvider; + + public static bool IsDynamic(Type type) => typeof(IDynamicMetaObjectProvider).IsAssignableFrom(type); + + public static void SetMemberValue(MemberInfo propertyOrField, object target, object value) + { + if (propertyOrField is PropertyInfo property) + { + property.SetValue(target, value, null); + return; + } + if (propertyOrField is FieldInfo field) + { + field.SetValue(target, value); + return; + } + throw Expected(propertyOrField); + } + + private static ArgumentOutOfRangeException Expected(MemberInfo propertyOrField) + => new ArgumentOutOfRangeException(nameof(propertyOrField), "Expected a property or field, not " + propertyOrField); + + public static object GetMemberValue(MemberInfo propertyOrField, object target) + { + if (propertyOrField is PropertyInfo property) + { + return property.GetValue(target, null); + } + if (propertyOrField is FieldInfo field) + { + return field.GetValue(target); + } + throw Expected(propertyOrField); + } + + public static IEnumerable GetMemberPath(Type type, string fullMemberName) + { + MemberInfo property = null; + foreach (var memberName in fullMemberName.Split('.')) + { + var currentType = GetCurrentType(property, type); + yield return property = currentType.GetFieldOrProperty(memberName); + } + } + + private static Type GetCurrentType(MemberInfo member, Type type) + { + var memberType = member?.GetMemberType() ?? type; + if (memberType.IsGenericType() && typeof(IEnumerable).IsAssignableFrom(memberType)) + { + memberType = memberType.GetTypeInfo().GenericTypeArguments[0]; + } + return memberType; + } + + public static MemberInfo GetFieldOrProperty(LambdaExpression expression) + { + var memberExpression = expression.Body as MemberExpression; + return memberExpression != null + ? memberExpression.Member + : throw new ArgumentOutOfRangeException(nameof(expression), "Expected a property/field access expression, not " + expression); + } + + public static MemberInfo FindProperty(LambdaExpression lambdaExpression) + { + Expression expressionToCheck = lambdaExpression; + + var done = false; + + while (!done) + { + switch (expressionToCheck.NodeType) + { + case ExpressionType.Convert: + expressionToCheck = ((UnaryExpression)expressionToCheck).Operand; + break; + case ExpressionType.Lambda: + expressionToCheck = ((LambdaExpression)expressionToCheck).Body; + break; + case ExpressionType.MemberAccess: + var memberExpression = ((MemberExpression)expressionToCheck); + + if (memberExpression.Expression.NodeType != ExpressionType.Parameter && + memberExpression.Expression.NodeType != ExpressionType.Convert) + { + throw new ArgumentException( + $"Expression '{lambdaExpression}' must resolve to top-level member and not any child object's properties. You can use ForPath, a custom resolver on the child type or the AfterMap option instead.", + nameof(lambdaExpression)); + } + + var member = memberExpression.Member; + + return member; + default: + done = true; + break; + } + } + + throw new AutoMapperConfigurationException( + "Custom configuration for members is only supported for top-level individual members on a type."); + } + + public static Type GetMemberType(MemberInfo memberInfo) + { + switch (memberInfo) + { + case MethodInfo mInfo: + return mInfo.ReturnType; + case PropertyInfo pInfo: + return pInfo.PropertyType; + case FieldInfo fInfo: + return fInfo.FieldType; + case null: + throw new ArgumentNullException(nameof(memberInfo)); + default: + throw new ArgumentOutOfRangeException(nameof(memberInfo)); + } + } + + /// + /// if targetType is oldType, method will return newType + /// if targetType is not oldType, method will return targetType + /// if targetType is generic type with oldType arguments, method will replace all oldType arguments on newType + /// + /// + /// + /// + /// + public static Type ReplaceItemType(Type targetType, Type oldType, Type newType) + { + if (targetType == oldType) + return newType; + + if (targetType.IsGenericType()) + { + var genSubArgs = targetType.GetTypeInfo().GenericTypeArguments; + var newGenSubArgs = new Type[genSubArgs.Length]; + for (var i = 0; i < genSubArgs.Length; i++) + newGenSubArgs[i] = ReplaceItemType(genSubArgs[i], oldType, newType); + return targetType.GetGenericTypeDefinition().MakeGenericType(newGenSubArgs); + } + + return targetType; + } + } +} diff --git a/tools/AutoMapper/LockingConcurrentDictionary.cs b/tools/AutoMapper/LockingConcurrentDictionary.cs new file mode 100644 index 0000000000..642b3934ab --- /dev/null +++ b/tools/AutoMapper/LockingConcurrentDictionary.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; + +namespace AutoMapper +{ + internal struct LockingConcurrentDictionary + { + private readonly ConcurrentDictionary> _dictionary; + private readonly Func> _valueFactory; + + public LockingConcurrentDictionary(Func valueFactory) + { + _dictionary = new ConcurrentDictionary>(); + _valueFactory = key => new Lazy(() => valueFactory(key)); + } + + public TValue GetOrAdd(TKey key) => _dictionary.GetOrAdd(key, _valueFactory).Value; + public TValue GetOrAdd(TKey key, Func> valueFactory) => _dictionary.GetOrAdd(key, valueFactory).Value; + + public TValue this[TKey key] + { + get => _dictionary[key].Value; + set => _dictionary[key] = new Lazy(() => value); + } + + public bool TryGetValue(TKey key, out TValue value) + { + if (_dictionary.TryGetValue(key, out Lazy lazy)) + { + value = lazy.Value; + return true; + } + value = default(TValue); + return false; + } + + public bool ContainsKey(TKey key) => _dictionary.ContainsKey(key); + + public ICollection Keys => _dictionary.Keys; + } +} \ No newline at end of file diff --git a/tools/AutoMapper/LowerUnderscoreNamingConvention.cs b/tools/AutoMapper/LowerUnderscoreNamingConvention.cs new file mode 100644 index 0000000000..bcbd56dbd6 --- /dev/null +++ b/tools/AutoMapper/LowerUnderscoreNamingConvention.cs @@ -0,0 +1,13 @@ +using System.Text.RegularExpressions; + +namespace AutoMapper +{ + public class LowerUnderscoreNamingConvention : INamingConvention + { + public Regex SplittingExpression { get; } = new Regex(@"[\p{Ll}\p{Lu}0-9]+(?=_?)"); + + public string SeparatorCharacter => "_"; + + public string ReplaceValue(Match match) => match.Value.ToLower(); + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Mapper.cs b/tools/AutoMapper/Mapper.cs new file mode 100644 index 0000000000..cc2f06b6d3 --- /dev/null +++ b/tools/AutoMapper/Mapper.cs @@ -0,0 +1,375 @@ +using System; +using AutoMapper.Configuration; + +namespace AutoMapper +{ + using ObjectMappingOperationOptions = MappingOperationOptions; + + public class Mapper : IRuntimeMapper + { + private const string InvalidOperationMessage = "Mapper not initialized. Call Initialize with appropriate configuration. If you are trying to use mapper instances through a container or otherwise, make sure you do not have any calls to the static Mapper.Map methods, and if you're using ProjectTo or UseAsDataSource extension methods, make sure you pass in the appropriate IConfigurationProvider instance."; + private const string AlreadyInitialized = "Mapper already initialized. You must call Initialize once per application domain/process."; + + #region Static API + + private static IConfigurationProvider _configuration; + private static IMapper _instance; + + /// + /// Configuration provider for performing maps + /// + public static IConfigurationProvider Configuration + { + get => _configuration ?? throw new InvalidOperationException(InvalidOperationMessage); + private set => _configuration = (_configuration == null) ? value : throw new InvalidOperationException(AlreadyInitialized); + } + + /// + /// Static mapper instance. You can also create a instance directly using the instance. + /// + public static IMapper Instance + { + get => _instance ?? throw new InvalidOperationException(InvalidOperationMessage); + private set => _instance = value; + } + + /// + /// Initialize static configuration instance + /// + /// Configuration action + public static void Initialize(Action config) + { + Configuration = new MapperConfiguration(config); + Instance = new Mapper(Configuration); + } + + /// + /// Initialize static configuration instance + /// + /// Configuration action + public static void Initialize(MapperConfigurationExpression config) + { + Configuration = new MapperConfiguration(config); + Instance = new Mapper(Configuration); + } + + /// + /// Resets the mapper configuration. Not intended for production use, but for testing scenarios. + /// + public static void Reset() + { + _configuration = null; + _instance = null; + } + + /// + /// Execute a mapping from the source object to a new destination object. + /// The source type is inferred from the source object. + /// + /// Destination type to create + /// Source object to map from + /// Mapped destination object + public static TDestination Map(object source) => Instance.Map(source); + + /// + /// Execute a mapping from the source object to a new destination object with supplied mapping options. + /// + /// Destination type to create + /// Source object to map from + /// Mapping options + /// Mapped destination object + public static TDestination Map(object source, Action opts) + => Instance.Map(source, opts); + + /// + /// Execute a mapping from the source object to a new destination object. + /// + /// Source type to use, regardless of the runtime type + /// Destination type to create + /// Source object to map from + /// Mapped destination object + public static TDestination Map(TSource source) + => Instance.Map(source); + + /// + /// Execute a mapping from the source object to a new destination object with supplied mapping options. + /// + /// Source type to use + /// Destination type to create + /// Source object to map from + /// Mapping options + /// Mapped destination object + public static TDestination Map(TSource source, Action> opts) + => Instance.Map(source, opts); + + /// + /// Execute a mapping from the source object to the existing destination object. + /// + /// Source type to use + /// Dsetination type + /// Source object to map from + /// Destination object to map into + /// The mapped destination object, same instance as the object + public static TDestination Map(TSource source, TDestination destination) + => Instance.Map(source, destination); + + /// + /// Execute a mapping from the source object to the existing destination object with supplied mapping options. + /// + /// Source type to use + /// Destination type + /// Source object to map from + /// Destination object to map into + /// Mapping options + /// The mapped destination object, same instance as the object + public static TDestination Map(TSource source, TDestination destination, Action> opts) + => Instance.Map(source, destination, opts); + + /// + /// Execute a mapping from the source object to a new destination object with explicit objects + /// + /// Source object to map from + /// Source type to use + /// Destination type to create + /// Mapped destination object + public static object Map(object source, Type sourceType, Type destinationType) + => Instance.Map(source, sourceType, destinationType); + + /// + /// Execute a mapping from the source object to a new destination object with explicit objects and supplied mapping options. + /// + /// Source object to map from + /// Source type to use + /// Destination type to create + /// Mapping options + /// Mapped destination object + public static object Map(object source, Type sourceType, Type destinationType, Action opts) + => Instance.Map(source, sourceType, destinationType, opts); + + /// + /// Execute a mapping from the source object to existing destination object with explicit objects + /// + /// Source object to map from + /// Destination object to map into + /// Source type to use + /// Destination type to use + /// Mapped destination object, same instance as the object + public static object Map(object source, object destination, Type sourceType, Type destinationType) + => Instance.Map(source, destination, sourceType, destinationType); + + /// + /// Execute a mapping from the source object to existing destination object with supplied mapping options and explicit objects + /// + /// Source object to map from + /// Destination object to map into + /// Source type to use + /// Destination type to use + /// Mapping options + /// Mapped destination object, same instance as the object + public static object Map(object source, object destination, Type sourceType, Type destinationType, Action opts) + => Instance.Map(source, destination, sourceType, destinationType, opts); + + /// + /// Dry run all configured type maps and throw for each problem + /// + public static void AssertConfigurationIsValid() => Configuration.AssertConfigurationIsValid(); + + #endregion + + private readonly IConfigurationProvider _configurationProvider; + private readonly Func _serviceCtor; + + public Mapper(IConfigurationProvider configurationProvider) + : this(configurationProvider, configurationProvider.ServiceCtor) + { + } + + public Mapper(IConfigurationProvider configurationProvider, Func serviceCtor) + { + _configurationProvider = configurationProvider; + _serviceCtor = serviceCtor; + DefaultContext = new ResolutionContext(new ObjectMappingOperationOptions(serviceCtor), this); + } + + public ResolutionContext DefaultContext { get; } + + Func IMapper.ServiceCtor => _serviceCtor; + + IConfigurationProvider IMapper.ConfigurationProvider => _configurationProvider; + + TDestination IMapper.Map(object source) + { + if (source == null) + return default(TDestination); + + var types = new TypePair(source.GetType(), typeof(TDestination)); + + var func = _configurationProvider.GetUntypedMapperFunc(new MapRequest(types, types)); + + return (TDestination) func(source, null, DefaultContext); + } + + TDestination IMapper.Map(object source, Action opts) + { + var mappedObject = default(TDestination); + + if (source == null) return mappedObject; + + var sourceType = source.GetType(); + var destinationType = typeof(TDestination); + + mappedObject = (TDestination)((IMapper)this).Map(source, sourceType, destinationType, opts); + return mappedObject; + } + + TDestination IMapper.Map(TSource source) + { + var types = TypePair.Create(source, typeof(TSource), typeof (TDestination)); + + var func = _configurationProvider.GetMapperFunc(types); + + var destination = default(TDestination); + + return func(source, destination, DefaultContext); + } + + TDestination IMapper.Map(TSource source, Action> opts) + { + var types = TypePair.Create(source, typeof(TSource), typeof(TDestination)); + + var key = new TypePair(typeof(TSource), typeof(TDestination)); + + var typedOptions = new MappingOperationOptions(_serviceCtor); + + opts(typedOptions); + + var mapRequest = new MapRequest(key, types, typedOptions.InlineConfiguration); + + var func = _configurationProvider.GetMapperFunc(mapRequest); + + var destination = default(TDestination); + + typedOptions.BeforeMapAction(source, destination); + + var context = new ResolutionContext(typedOptions, this); + + destination = func(source, destination, context); + + typedOptions.AfterMapAction(source, destination); + + return destination; + } + + TDestination IMapper.Map(TSource source, TDestination destination) + { + var types = TypePair.Create(source, destination, typeof(TSource), typeof(TDestination)); + + var func = _configurationProvider.GetMapperFunc(types); + + return func(source, destination, DefaultContext); + } + + TDestination IMapper.Map(TSource source, TDestination destination, Action> opts) + { + var types = TypePair.Create(source, destination, typeof(TSource), typeof(TDestination)); + var key = new TypePair(typeof(TSource), typeof(TDestination)); + + var typedOptions = new MappingOperationOptions(_serviceCtor); + + opts(typedOptions); + + var mapRequest = new MapRequest(key, types, typedOptions.InlineConfiguration); + + var func = _configurationProvider.GetMapperFunc(mapRequest); + + typedOptions.BeforeMapAction(source, destination); + + var context = new ResolutionContext(typedOptions, this); + + destination = func(source, destination, context); + + typedOptions.AfterMapAction(source, destination); + + return destination; + } + + object IMapper.Map(object source, Type sourceType, Type destinationType) + { + var types = TypePair.Create(source, sourceType, destinationType); + + var func = _configurationProvider.GetUntypedMapperFunc(new MapRequest(new TypePair(sourceType, destinationType), types)); + + return func(source, null, DefaultContext); + } + + object IMapper.Map(object source, Type sourceType, Type destinationType, Action opts) + { + var types = TypePair.Create(source, sourceType, destinationType); + + var options = new ObjectMappingOperationOptions(_serviceCtor); + + opts(options); + + var func = _configurationProvider.GetUntypedMapperFunc(new MapRequest(new TypePair(sourceType, destinationType), types, options.InlineConfiguration)); + + options.BeforeMapAction(source, null); + + var context = new ResolutionContext(options, this); + + var destination = func(source, null, context); + + options.AfterMapAction(source, destination); + + return destination; + } + + object IMapper.Map(object source, object destination, Type sourceType, Type destinationType) + { + var types = TypePair.Create(source, destination, sourceType, destinationType); + + var func = _configurationProvider.GetUntypedMapperFunc(new MapRequest(new TypePair(sourceType, destinationType), types)); + + return func(source, destination, DefaultContext); + } + + object IMapper.Map(object source, object destination, Type sourceType, Type destinationType, + Action opts) + { + var types = TypePair.Create(source, destination, sourceType, destinationType); + + var options = new ObjectMappingOperationOptions(_serviceCtor); + + opts(options); + + var func = _configurationProvider.GetUntypedMapperFunc(new MapRequest(new TypePair(sourceType, destinationType), types, options.InlineConfiguration)); + + options.BeforeMapAction(source, destination); + + var context = new ResolutionContext(options, this); + + destination = func(source, destination, context); + + options.AfterMapAction(source, destination); + + return destination; + } + + object IRuntimeMapper.Map(object source, object destination, Type sourceType, Type destinationType, ResolutionContext context) + { + var types = TypePair.Create(source, destination, sourceType, destinationType); + + var func = _configurationProvider.GetUntypedMapperFunc(new MapRequest(new TypePair(sourceType, destinationType), types)); + + return func(source, destination, context); + } + + TDestination IRuntimeMapper.Map(TSource source, TDestination destination, ResolutionContext context) + { + var types = TypePair.Create(source, destination, typeof(TSource), typeof(TDestination)); + + var func = _configurationProvider.GetMapperFunc(types); + + return func(source, destination, context); + } + } +} \ No newline at end of file diff --git a/tools/AutoMapper/MapperConfiguration.cs b/tools/AutoMapper/MapperConfiguration.cs new file mode 100644 index 0000000000..699e11481b --- /dev/null +++ b/tools/AutoMapper/MapperConfiguration.cs @@ -0,0 +1,470 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using AutoMapper.Configuration; +using AutoMapper.Internal; +using AutoMapper.QueryableExtensions; +using AutoMapper.QueryableExtensions.Impl; + +namespace AutoMapper +{ + using static Expression; + using static ExpressionFactory; + using static Execution.ExpressionBuilder; + using UntypedMapperFunc = Func; + using Validator = Action; + + public class MapperConfiguration : IConfigurationProvider + { + private static readonly Type[] ExcludedTypes = { typeof(object), typeof(ValueType), typeof(Enum) }; + private static readonly ConstructorInfo ExceptionConstructor = typeof(AutoMapperMappingException).GetDeclaredConstructors().Single(c => c.GetParameters().Length == 3); + + private readonly IEnumerable _mappers; + private readonly TypeMapRegistry _typeMapRegistry = new TypeMapRegistry(); + private LockingConcurrentDictionary _typeMapPlanCache; + private readonly LockingConcurrentDictionary _mapPlanCache; + private readonly ConfigurationValidator _validator; + private readonly MapperConfigurationExpressionValidator _expressionValidator; + + public MapperConfiguration(MapperConfigurationExpression configurationExpression) + { + _mappers = configurationExpression.Mappers.ToArray(); + _typeMapPlanCache = new LockingConcurrentDictionary(GetTypeMap); + _mapPlanCache = new LockingConcurrentDictionary(CreateMapperFuncs); + Validators = configurationExpression.Advanced.GetValidators(); + _validator = new ConfigurationValidator(this); + _expressionValidator = new MapperConfigurationExpressionValidator(configurationExpression); + ExpressionBuilder = new ExpressionBuilder(this); + + ServiceCtor = configurationExpression.ServiceCtor; + EnableNullPropagationForQueryMapping = configurationExpression.EnableNullPropagationForQueryMapping ?? false; + MaxExecutionPlanDepth = configurationExpression.Advanced.MaxExecutionPlanDepth + 1; + + Configuration = new ProfileMap(configurationExpression); + Profiles = new[] { Configuration }.Concat(configurationExpression.Profiles.Select(p => new ProfileMap(p, configurationExpression))).ToArray(); + + foreach (var beforeSealAction in configurationExpression.Advanced.BeforeSealActions) + beforeSealAction?.Invoke(this); + Seal(); + } + + + public MapperConfiguration(Action configure) + : this(Build(configure)) + { + } + + + public void Validate(ValidationContext context) + { + foreach (var validator in Validators) + { + validator(context); + } + } + + private Validator[] Validators { get; } + + public IExpressionBuilder ExpressionBuilder { get; } + + public Func ServiceCtor { get; } + + public bool EnableNullPropagationForQueryMapping { get; } + + public int MaxExecutionPlanDepth { get; } + + private ProfileMap Configuration { get; } + + public IEnumerable Profiles { get; } + + public Func GetMapperFunc(TypePair types) + { + var key = new TypePair(typeof(TSource), typeof(TDestination)); + var mapRequest = new MapRequest(key, types); + return GetMapperFunc(mapRequest); + } + + public Func GetMapperFunc(MapRequest mapRequest) + => (Func)GetMapperFunc(mapRequest); + + public void CompileMappings() + { + foreach (var request in _typeMapPlanCache.Keys.Select(types => new MapRequest(types, types)).ToArray()) + { + GetMapperFunc(request); + } + } + + public Delegate GetMapperFunc(MapRequest mapRequest) => _mapPlanCache.GetOrAdd(mapRequest).Typed; + + public UntypedMapperFunc GetUntypedMapperFunc(MapRequest mapRequest) => _mapPlanCache.GetOrAdd(mapRequest).Untyped; + + private MapperFuncs CreateMapperFuncs(MapRequest mapRequest) => new MapperFuncs(mapRequest, BuildExecutionPlan(mapRequest)); + + public LambdaExpression BuildExecutionPlan(Type sourceType, Type destinationType) + { + var typePair = new TypePair(sourceType, destinationType); + return BuildExecutionPlan(new MapRequest(typePair, typePair)); + } + + public LambdaExpression BuildExecutionPlan(MapRequest mapRequest) + { + var typeMap = ResolveTypeMap(mapRequest.RuntimeTypes, mapRequest.InlineConfig) ?? ResolveTypeMap(mapRequest.RequestedTypes, mapRequest.InlineConfig); + if (typeMap != null) + { + return GenerateTypeMapExpression(mapRequest, typeMap); + } + var mapperToUse = FindMapper(mapRequest.RuntimeTypes); + return GenerateObjectMapperExpression(mapRequest, mapperToUse, this); + } + + private static LambdaExpression GenerateTypeMapExpression(MapRequest mapRequest, TypeMap typeMap) + { + var mapExpression = typeMap.MapExpression; + var typeMapSourceParameter = mapExpression.Parameters[0]; + var typeMapDestinationParameter = mapExpression.Parameters[1]; + var requestedSourceType = mapRequest.RequestedTypes.SourceType; + var requestedDestinationType = mapRequest.RequestedTypes.DestinationType; + + if (typeMapSourceParameter.Type != requestedSourceType || typeMapDestinationParameter.Type != requestedDestinationType) + { + var requestedSourceParameter = Parameter(requestedSourceType, "source"); + var requestedDestinationParameter = Parameter(requestedDestinationType, "typeMapDestination"); + var contextParameter = Parameter(typeof(ResolutionContext), "context"); + + mapExpression = Lambda(ToType(Invoke(typeMap.MapExpression, + ToType(requestedSourceParameter, typeMapSourceParameter.Type), + ToType(requestedDestinationParameter, typeMapDestinationParameter.Type), + contextParameter + ), mapRequest.RuntimeTypes.DestinationType), + requestedSourceParameter, requestedDestinationParameter, contextParameter); + } + + return mapExpression; + } + + private LambdaExpression GenerateObjectMapperExpression(MapRequest mapRequest, IObjectMapper mapperToUse, MapperConfiguration mapperConfiguration) + { + var destinationType = mapRequest.RequestedTypes.DestinationType; + + var source = Parameter(mapRequest.RequestedTypes.SourceType, "source"); + var destination = Parameter(destinationType, "mapperDestination"); + var context = Parameter(typeof(ResolutionContext), "context"); + Expression fullExpression; + if (mapperToUse == null) + { + var message = Constant("Missing type map configuration or unsupported mapping."); + fullExpression = Block(Throw(New(ExceptionConstructor, message, Constant(null, typeof(Exception)), Constant(mapRequest.RequestedTypes))), Default(destinationType)); + } + else + { + var map = mapperToUse.MapExpression(mapperConfiguration, Configuration, null, + ToType(source, mapRequest.RuntimeTypes.SourceType), + ToType(destination, mapRequest.RuntimeTypes.DestinationType), + context); + var exception = Parameter(typeof(Exception), "ex"); + fullExpression = + TryCatch(ToType(map, destinationType), + MakeCatchBlock(typeof(Exception), exception, Block( + Throw(New(ExceptionConstructor, Constant("Error mapping types."), exception, Constant(mapRequest.RequestedTypes))), + Default(destination.Type)), null)); + } + var nullCheckSource = NullCheckSource(Configuration, source, destination, fullExpression); + return Lambda(nullCheckSource, source, destination, context); + } + + public TypeMap[] GetAllTypeMaps() => _typeMapRegistry.TypeMaps.ToArray(); + + public TypeMap FindTypeMapFor(Type sourceType, Type destinationType) => FindTypeMapFor(new TypePair(sourceType, destinationType)); + + public TypeMap FindTypeMapFor() => FindTypeMapFor(new TypePair(typeof(TSource), typeof(TDestination))); + + public TypeMap FindTypeMapFor(TypePair typePair) => _typeMapRegistry.GetTypeMap(typePair); + + public TypeMap ResolveTypeMap(Type sourceType, Type destinationType) + { + var typePair = new TypePair(sourceType, destinationType); + + return ResolveTypeMap(typePair, new DefaultTypeMapConfig(typePair)); + } + + public TypeMap ResolveTypeMap(Type sourceType, Type destinationType, ITypeMapConfiguration inlineConfiguration) + { + var typePair = new TypePair(sourceType, destinationType); + + return ResolveTypeMap(typePair, inlineConfiguration); + } + + public TypeMap ResolveTypeMap(TypePair typePair) + => ResolveTypeMap(typePair, new DefaultTypeMapConfig(typePair)); + + public TypeMap ResolveTypeMap(TypePair typePair, ITypeMapConfiguration inlineConfiguration) + { + var typeMap = _typeMapPlanCache.GetOrAdd(typePair); + // if it's a dynamically created type map, we need to seal it outside GetTypeMap to handle recursion + if (typeMap != null && typeMap.MapExpression == null && _typeMapRegistry.GetTypeMap(typePair) == null) + { + lock (typeMap) + { + inlineConfiguration.Configure(typeMap); + typeMap.Seal(this); + } + } + return typeMap; + } + + private TypeMap GetTypeMap(TypePair initialTypes) + { + var doesNotHaveMapper = FindMapper(initialTypes) == null; + + foreach (var types in initialTypes.GetRelatedTypePairs()) + { + if (types != initialTypes && _typeMapPlanCache.TryGetValue(types, out var typeMap)) + { + if (typeMap != null) + { + return typeMap; + } + } + typeMap = FindTypeMapFor(types); + if (typeMap != null) + { + return typeMap; + } + typeMap = FindClosedGenericTypeMapFor(types); + if (typeMap != null) + { + return typeMap; + } + if (doesNotHaveMapper) + { + typeMap = FindConventionTypeMapFor(types); + if (typeMap != null) + { + return typeMap; + } + } + } + + if (doesNotHaveMapper + && Configuration.CreateMissingTypeMaps + && !(initialTypes.SourceType.IsAbstract() && initialTypes.SourceType.IsClass()) + && !(initialTypes.DestinationType.IsAbstract() && initialTypes.DestinationType.IsClass()) + && !ExcludedTypes.Contains(initialTypes.SourceType) + && !ExcludedTypes.Contains(initialTypes.DestinationType)) + { + lock (this) + { + return Configuration.CreateInlineMap(_typeMapRegistry, initialTypes); + } + } + + return null; + } + + public void AssertConfigurationIsValid(TypeMap typeMap) + { + _validator.AssertConfigurationIsValid(Enumerable.Repeat(typeMap, 1)); + } + + public void AssertConfigurationIsValid(string profileName) + { + _validator.AssertConfigurationIsValid(_typeMapRegistry.TypeMaps.Where(typeMap => typeMap.Profile.Name == profileName)); + } + + public void AssertConfigurationIsValid() + where TProfile : Profile, new() + { + AssertConfigurationIsValid(new TProfile().ProfileName); + } + + public void AssertConfigurationIsValid() + { + _expressionValidator.AssertConfigurationExpressionIsValid(); + + _validator.AssertConfigurationIsValid(_typeMapRegistry.TypeMaps.Where(tm => !tm.SourceType.IsGenericTypeDefinition() && !tm.DestinationType.IsGenericTypeDefinition())); + } + + public IMapper CreateMapper() => new Mapper(this); + + public IMapper CreateMapper(Func serviceCtor) => new Mapper(this, serviceCtor); + + public IEnumerable GetMappers() => _mappers; + + private static MapperConfigurationExpression Build(Action configure) + { + var expr = new MapperConfigurationExpression(); + configure(expr); + return expr; + } + + private void Seal() + { + var derivedMaps = new List>(); + var redirectedTypes = new List>(); + + foreach (var profile in Profiles) + { + profile.Register(_typeMapRegistry); + } + + foreach (var profile in Profiles) + { + profile.Configure(_typeMapRegistry); + } + + foreach (var typeMap in _typeMapRegistry.TypeMaps) + { + _typeMapPlanCache[typeMap.Types] = typeMap; + + if (typeMap.DestinationTypeOverride != null) + { + redirectedTypes.Add(Tuple.Create(typeMap.Types, new TypePair(typeMap.SourceType, typeMap.DestinationTypeOverride))); + } + derivedMaps.AddRange(GetDerivedTypeMaps(typeMap).Select(derivedMap => Tuple.Create(new TypePair(derivedMap.SourceType, typeMap.DestinationType), derivedMap))); + } + foreach (var redirectedType in redirectedTypes) + { + var derivedMap = FindTypeMapFor(redirectedType.Item2); + if (derivedMap != null) + { + _typeMapPlanCache[redirectedType.Item1] = derivedMap; + } + } + foreach (var derivedMap in derivedMaps.Where(derivedMap => !_typeMapPlanCache.ContainsKey(derivedMap.Item1))) + { + _typeMapPlanCache[derivedMap.Item1] = derivedMap.Item2; + } + + foreach (var typeMap in _typeMapRegistry.TypeMaps) + { + typeMap.Seal(this); + } + } + + private IEnumerable GetDerivedTypeMaps(TypeMap typeMap) + { + foreach (var derivedTypes in typeMap.IncludedDerivedTypes) + { + var derivedMap = FindTypeMapFor(derivedTypes); + if (derivedMap == null) + { + throw QueryMapperHelper.MissingMapException(derivedTypes.SourceType, derivedTypes.DestinationType); + } + yield return derivedMap; + foreach (var derivedTypeMap in GetDerivedTypeMaps(derivedMap)) + { + yield return derivedTypeMap; + } + } + } + + private TypeMap FindConventionTypeMapFor(TypePair typePair) + { + var profile = Profiles.FirstOrDefault(p => p.IsConventionMap(typePair)); + if(profile == null) + { + return null; + } + TypeMap typeMap; + lock(this) + { + typeMap = profile.CreateConventionTypeMap(_typeMapRegistry, typePair); + } + return typeMap; + } + + private TypeMap FindClosedGenericTypeMapFor(TypePair typePair) + { + if(typePair.GetOpenGenericTypePair() == null) + { + return null; + } + var mapInfo = Profiles.Select(p => new { GenericMap = p.GetGenericMap(typePair), Profile = p }).FirstOrDefault(p => p.GenericMap != null); + if(mapInfo == null) + { + return null; + } + TypeMap typeMap; + lock(this) + { + typeMap = mapInfo.Profile.CreateClosedGenericTypeMap(mapInfo.GenericMap, _typeMapRegistry, typePair); + } + return typeMap; + } + + public IObjectMapper FindMapper(TypePair types) =>_mappers.FirstOrDefault(m => m.IsMatch(types)); + + internal struct MapperFuncs + { + public Delegate Typed { get; } + + public UntypedMapperFunc Untyped { get; } + + public MapperFuncs(MapRequest mapRequest, LambdaExpression typedExpression) + { + Typed = typedExpression.Compile(); + Untyped = Wrap(mapRequest, Typed).Compile(); + } + + private static Expression Wrap(MapRequest mapRequest, Delegate typedDelegate) + { + var sourceParameter = Parameter(typeof(object), "source"); + var destinationParameter = Parameter(typeof(object), "destination"); + var contextParameter = Parameter(typeof(ResolutionContext), "context"); + var requestedSourceType = mapRequest.RequestedTypes.SourceType; + var requestedDestinationType = mapRequest.RequestedTypes.DestinationType; + + var destination = requestedDestinationType.IsValueType() ? Coalesce(destinationParameter, New(requestedDestinationType)) : (Expression)destinationParameter; + // Invoking a delegate here + return Lambda( + ToType( + Invoke(Constant(typedDelegate), ToType(sourceParameter, requestedSourceType), ToType(destination, requestedDestinationType), contextParameter) + , typeof(object)), + sourceParameter, destinationParameter, contextParameter); + } + } + + internal class DefaultTypeMapConfig : ITypeMapConfiguration + { + public DefaultTypeMapConfig(TypePair types) + { + Types = types; + } + + public void Configure(TypeMap typeMap) { } + + public Type SourceType => Types.SourceType; + public Type DestinationType => Types.DestinationType; + public bool IsOpenGeneric => false; + public TypePair Types { get; } + public ITypeMapConfiguration ReverseTypeMap => null; + } + } + + public struct ValidationContext + { + public IObjectMapper ObjectMapper { get; } + public PropertyMap PropertyMap { get; } + public TypeMap TypeMap { get; } + public TypePair Types { get; } + + public ValidationContext(TypePair types, PropertyMap propertyMap, IObjectMapper objectMapper) : this(types, propertyMap, objectMapper, null) + { + } + + public ValidationContext(TypePair types, PropertyMap propertyMap, TypeMap typeMap) : this(types, propertyMap, null, typeMap) + { + } + + private ValidationContext(TypePair types, PropertyMap propertyMap, IObjectMapper objectMapper, TypeMap typeMap) + { + ObjectMapper = objectMapper; + TypeMap = typeMap; + Types = types; + PropertyMap = propertyMap; + } + } +} diff --git a/tools/AutoMapper/MapperConfigurationExpressionValidator.cs b/tools/AutoMapper/MapperConfigurationExpressionValidator.cs new file mode 100644 index 0000000000..01e5f2b0e3 --- /dev/null +++ b/tools/AutoMapper/MapperConfigurationExpressionValidator.cs @@ -0,0 +1,34 @@ +using System.Linq; +using AutoMapper.Configuration; + +namespace AutoMapper +{ + public class MapperConfigurationExpressionValidator + { + private readonly MapperConfigurationExpression _expression; + + public MapperConfigurationExpressionValidator(MapperConfigurationExpression expression) + { + _expression = expression; + } + + public void AssertConfigurationExpressionIsValid() + { + if (_expression.Advanced.AllowAdditiveTypeMapCreation) + return; + + var duplicateTypeMapConfigs = Enumerable.Concat(new [] {_expression}, _expression.Profiles) + .SelectMany(p => p.TypeMapConfigs, (profile, typeMap) => new {profile, typeMap}) + .GroupBy(x => x.typeMap.Types) + .Where(g => g.Count() > 1) + .Select(g => new { TypePair = g.Key, ProfileNames = g.Select(tmc => tmc.profile.ProfileName).ToArray() }) + .Select(g => new DuplicateTypeMapConfigurationException.TypeMapConfigErrors(g.TypePair, g.ProfileNames)) + .ToArray(); + + if (duplicateTypeMapConfigs.Any()) + { + throw new DuplicateTypeMapConfigurationException(duplicateTypeMapConfigs); + } + } + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Mappers/ArrayMapper.cs b/tools/AutoMapper/Mappers/ArrayMapper.cs new file mode 100644 index 0000000000..303148644a --- /dev/null +++ b/tools/AutoMapper/Mappers/ArrayMapper.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using AutoMapper.Configuration; +using AutoMapper.Internal; +using AutoMapper.Mappers.Internal; + +namespace AutoMapper.Mappers +{ + using static Expression; + using static ExpressionFactory; + using static CollectionMapperExpressionFactory; + + public class ArrayMapper : EnumerableMapperBase + { + public override bool IsMatch(TypePair context) => context.DestinationType.IsArray && context.SourceType.IsEnumerableType(); + + public override Expression MapExpression(IConfigurationProvider configurationProvider, ProfileMap profileMap, PropertyMap propertyMap, Expression sourceExpression, Expression destExpression, Expression contextExpression) + { + var sourceElementType = ElementTypeHelper.GetElementType(sourceExpression.Type); + var destElementType = ElementTypeHelper.GetElementType(destExpression.Type); + + var itemExpr = MapItemExpr(configurationProvider, profileMap, sourceExpression.Type, destExpression.Type, contextExpression, out ParameterExpression itemParam); + + //var count = source.Count(); + //var array = new TDestination[count]; + + //int i = 0; + //foreach (var item in source) + // array[i++] = newItemFunc(item, context); + //return array; + + var countParam = Parameter(typeof(int), "count"); + var arrayParam = Parameter(destExpression.Type, "destinationArray"); + var indexParam = Parameter(typeof(int), "destinationArrayIndex"); + + var actions = new List(); + var parameters = new List { countParam, arrayParam, indexParam }; + + var countMethod = typeof(Enumerable) + .GetTypeInfo() + .DeclaredMethods + .Single(mi => mi.Name == "Count" && mi.GetParameters().Length == 1) + .MakeGenericMethod(sourceElementType); + actions.Add(Assign(countParam, Call(countMethod, sourceExpression))); + actions.Add(Assign(arrayParam, NewArrayBounds(destElementType, countParam))); + actions.Add(Assign(indexParam, Constant(0))); + actions.Add(ForEach(sourceExpression, itemParam, + Assign(ArrayAccess(arrayParam, PostIncrementAssign(indexParam)), itemExpr) + )); + actions.Add(arrayParam); + + return Block(parameters, actions); + } + } +} diff --git a/tools/AutoMapper/Mappers/AssignableMapper.cs b/tools/AutoMapper/Mappers/AssignableMapper.cs new file mode 100644 index 0000000000..b7928e720e --- /dev/null +++ b/tools/AutoMapper/Mappers/AssignableMapper.cs @@ -0,0 +1,12 @@ +using System.Linq.Expressions; + +namespace AutoMapper.Mappers +{ + public class AssignableMapper : IObjectMapper + { + public bool IsMatch(TypePair context) => context.DestinationType.IsAssignableFrom(context.SourceType); + + public Expression MapExpression(IConfigurationProvider configurationProvider, ProfileMap profileMap, PropertyMap propertyMap, Expression sourceExpression, Expression destExpression, Expression contextExpression) + => sourceExpression; + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Mappers/CollectionMapper.cs b/tools/AutoMapper/Mappers/CollectionMapper.cs new file mode 100644 index 0000000000..0c38bf0add --- /dev/null +++ b/tools/AutoMapper/Mappers/CollectionMapper.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using System.Linq.Expressions; +using AutoMapper.Configuration; +using AutoMapper.Mappers.Internal; + +namespace AutoMapper.Mappers +{ + using static CollectionMapperExpressionFactory; + + public class CollectionMapper : EnumerableMapperBase + { + public override bool IsMatch(TypePair context) => context.SourceType.IsEnumerableType() && context.DestinationType.IsCollectionType(); + + public override Expression MapExpression(IConfigurationProvider configurationProvider, ProfileMap profileMap, PropertyMap propertyMap, Expression sourceExpression, Expression destExpression, Expression contextExpression) + => MapCollectionExpression(configurationProvider, profileMap, propertyMap, sourceExpression, destExpression, contextExpression, typeof(List<>), MapItemExpr); + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Mappers/ConvertMapper.cs b/tools/AutoMapper/Mappers/ConvertMapper.cs new file mode 100644 index 0000000000..86575a163f --- /dev/null +++ b/tools/AutoMapper/Mappers/ConvertMapper.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using AutoMapper.Internal; + +namespace AutoMapper.Mappers +{ + using LazyExpression = Lazy; + using static Expression; + using static ExpressionFactory; + + public class ConvertMapper : IObjectMapper + { + private readonly Dictionary _converters = GetConverters(); + + private static Dictionary GetConverters() + { + var primitiveTypes = new[] + { + typeof(string), typeof(bool), typeof(byte), typeof(short), typeof(int), typeof(long), typeof(float), + typeof(double), typeof(decimal), typeof(sbyte), typeof(ushort), typeof(uint), typeof(ulong) + }; + return + (from sourceType in primitiveTypes + from destinationType in primitiveTypes + select new + { + Key = new TypePair(sourceType, destinationType), + Value = new LazyExpression(() => ConvertExpression(sourceType, destinationType), isThreadSafe: false) + }) + .ToDictionary(i => i.Key, i => i.Value); + } + + static LambdaExpression ConvertExpression(Type sourceType, Type destinationType) + { + var convertMethod = typeof(Convert).GetDeclaredMethod("To" + destinationType.Name, new[] { sourceType }); + var sourceParameter = Parameter(sourceType, "source"); + return Lambda(Call(convertMethod, sourceParameter), sourceParameter); + } + + public bool IsMatch(TypePair types) => _converters.ContainsKey(types); + + public Expression MapExpression(IConfigurationProvider configurationProvider, ProfileMap profileMap, PropertyMap propertyMap, Expression sourceExpression, Expression destExpression, Expression contextExpression) + { + var typeMap = new TypePair(sourceExpression.Type, destExpression.Type); + return _converters[typeMap].Value.ReplaceParameters(sourceExpression); + } + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Mappers/CreateMapBasedOnCriteriaMapper.cs b/tools/AutoMapper/Mappers/CreateMapBasedOnCriteriaMapper.cs new file mode 100644 index 0000000000..8f815c5268 --- /dev/null +++ b/tools/AutoMapper/Mappers/CreateMapBasedOnCriteriaMapper.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; + +namespace AutoMapper.Mappers +{ + public interface IConditionalObjectMapper + { + ICollection> Conventions { get; } + bool IsMatch(TypePair context); + } + + public class ConditionalObjectMapper : IConditionalObjectMapper + { + public bool IsMatch(TypePair typePair) + { + return Conventions.All(c => c(typePair)); + } + + public ICollection> Conventions { get; } = new Collection>(); + } + + public static class ConventionGeneratorExtensions + { + public static IConditionalObjectMapper Where(this IConditionalObjectMapper self, Func condition) + { + self.Conventions.Add(rc => condition(rc.SourceType, rc.DestinationType)); + return self; + } + + } + +} \ No newline at end of file diff --git a/tools/AutoMapper/Mappers/DictionaryMapper.cs b/tools/AutoMapper/Mappers/DictionaryMapper.cs new file mode 100644 index 0000000000..73d043edb6 --- /dev/null +++ b/tools/AutoMapper/Mappers/DictionaryMapper.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using System.Linq.Expressions; +using AutoMapper.Configuration; +using AutoMapper.Mappers.Internal; + +namespace AutoMapper.Mappers +{ + using static CollectionMapperExpressionFactory; + + public class DictionaryMapper : IObjectMapper + { + public bool IsMatch(TypePair context) => context.SourceType.IsDictionaryType() && context.DestinationType.IsDictionaryType(); + + public Expression MapExpression(IConfigurationProvider configurationProvider, ProfileMap profileMap, PropertyMap propertyMap, Expression sourceExpression, Expression destExpression, Expression contextExpression) + => MapCollectionExpression(configurationProvider, profileMap, propertyMap, sourceExpression, destExpression, contextExpression, typeof(Dictionary<,>), MapKeyPairValueExpr); + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Mappers/EnumToEnumMapper.cs b/tools/AutoMapper/Mappers/EnumToEnumMapper.cs new file mode 100644 index 0000000000..f53131f269 --- /dev/null +++ b/tools/AutoMapper/Mappers/EnumToEnumMapper.cs @@ -0,0 +1,49 @@ +using System; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using AutoMapper.Mappers.Internal; +using static System.Linq.Expressions.Expression; + +namespace AutoMapper.Mappers +{ + public class EnumToEnumMapper : IObjectMapper + { + public static TDestination Map(TSource source) + { + var sourceEnumType = ElementTypeHelper.GetEnumerationType(typeof(TSource)); + var destEnumType = ElementTypeHelper.GetEnumerationType(typeof(TDestination)); + + if (!Enum.IsDefined(sourceEnumType, source)) + { + return (TDestination)Enum.ToObject(destEnumType, source); + } + + if (!Enum.GetNames(destEnumType).Contains(source.ToString())) + { + var underlyingSourceType = Enum.GetUnderlyingType(sourceEnumType); + var underlyingSourceValue = System.Convert.ChangeType(source, underlyingSourceType); + + return (TDestination)Enum.ToObject(destEnumType, underlyingSourceValue); + } + + return (TDestination)Enum.Parse(destEnumType, Enum.GetName(sourceEnumType, source), true); + } + + private static readonly MethodInfo MapMethodInfo = typeof(EnumToEnumMapper).GetAllMethods().First(_ => _.IsStatic); + + public bool IsMatch(TypePair context) + { + var sourceEnumType = ElementTypeHelper.GetEnumerationType(context.SourceType); + var destEnumType = ElementTypeHelper.GetEnumerationType(context.DestinationType); + return sourceEnumType != null && destEnumType != null; + } + + public Expression MapExpression(IConfigurationProvider configurationProvider, ProfileMap profileMap, + PropertyMap propertyMap, Expression sourceExpression, Expression destExpression, + Expression contextExpression) => + Call(null, + MapMethodInfo.MakeGenericMethod(sourceExpression.Type, destExpression.Type), + sourceExpression); + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Mappers/EnumToStringMapper.cs b/tools/AutoMapper/Mappers/EnumToStringMapper.cs new file mode 100644 index 0000000000..c13ba4b18b --- /dev/null +++ b/tools/AutoMapper/Mappers/EnumToStringMapper.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Runtime.Serialization; +using AutoMapper.Internal; +using AutoMapper.Mappers.Internal; + +namespace AutoMapper.Mappers +{ + public class EnumToStringMapper : IObjectMapper + { + public bool IsMatch(TypePair context) => context.DestinationType == typeof(string) && + ElementTypeHelper.GetEnumerationType(context.SourceType) != null; + + public Expression MapExpression(IConfigurationProvider configurationProvider, ProfileMap profileMap, + PropertyMap propertyMap, Expression sourceExpression, Expression destExpression, + Expression contextExpression) + { + var sourceType = sourceExpression.Type; + var sourceTypeEnum = ElementTypeHelper.GetEnumerationType(sourceType); + var toStringCall = Expression.Call(sourceExpression, typeof(object).GetDeclaredMethod("ToString")); + var switchCases = new List(); + var enumNames = sourceTypeEnum.GetDeclaredMembers(); + foreach (var memberInfo in enumNames.Where(x => x.IsStatic())) + { + var attribute = memberInfo.GetCustomAttribute(typeof(EnumMemberAttribute)) as EnumMemberAttribute; + if (attribute?.Value != null) + { + var switchCase = Expression.SwitchCase(Expression.Constant(attribute.Value), + Expression.Constant(Enum.ToObject(sourceTypeEnum, memberInfo.GetMemberValue(null)))); + switchCases.Add(switchCase); + } + } + return switchCases.Count > 0 + ? (Expression) Expression.Switch(sourceExpression, toStringCall, switchCases.ToArray()) + : toStringCall; + } + } +} diff --git a/tools/AutoMapper/Mappers/EnumToUnderlyingTypeMapper.cs b/tools/AutoMapper/Mappers/EnumToUnderlyingTypeMapper.cs new file mode 100644 index 0000000000..3873456b68 --- /dev/null +++ b/tools/AutoMapper/Mappers/EnumToUnderlyingTypeMapper.cs @@ -0,0 +1,34 @@ +using System; +using System.Linq.Expressions; +using System.Reflection; +using AutoMapper.Internal; +using AutoMapper.Mappers.Internal; +using static System.Linq.Expressions.Expression; +using Convert = System.Convert; + +namespace AutoMapper.Mappers +{ + using static ExpressionFactory; + + public class EnumToUnderlyingTypeMapper : IObjectMapper + { + private static readonly MethodInfo ChangeTypeMethod = Method(() => Convert.ChangeType(null, typeof(object))); + + public bool IsMatch(TypePair context) + { + var sourceEnumType = ElementTypeHelper.GetEnumerationType(context.SourceType); + + return sourceEnumType != null && context.DestinationType.IsAssignableFrom(Enum.GetUnderlyingType(sourceEnumType)); + } + + public Expression MapExpression(IConfigurationProvider configurationProvider, ProfileMap profileMap, + PropertyMap propertyMap, Expression sourceExpression, Expression destExpression, + Expression contextExpression) => + ToType( + Call(ChangeTypeMethod, ToObject(sourceExpression), + Constant(destExpression.Type)), + destExpression.Type + ); + } + +} \ No newline at end of file diff --git a/tools/AutoMapper/Mappers/EnumerableMapper.cs b/tools/AutoMapper/Mappers/EnumerableMapper.cs new file mode 100644 index 0000000000..f3edc2c684 --- /dev/null +++ b/tools/AutoMapper/Mappers/EnumerableMapper.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq.Expressions; +using AutoMapper.Configuration; +using AutoMapper.Mappers.Internal; + +namespace AutoMapper.Mappers +{ + using static Expression; + using static CollectionMapperExpressionFactory; + + public class EnumerableMapper : EnumerableMapperBase + { + public override bool IsMatch(TypePair context) => (context.DestinationType.IsInterface() && context.DestinationType.IsEnumerableType() || + context.DestinationType.IsListType()) + && context.SourceType.IsEnumerableType(); + + public override Expression MapExpression(IConfigurationProvider configurationProvider, ProfileMap profileMap, PropertyMap propertyMap, Expression sourceExpression, Expression destExpression, Expression contextExpression) + { + if(destExpression.Type.IsInterface()) + { + var listType = typeof(IList<>).MakeGenericType(ElementTypeHelper.GetElementType(destExpression.Type)); + destExpression = Convert(destExpression, listType); + } + return MapCollectionExpression(configurationProvider, profileMap, propertyMap, sourceExpression, + destExpression, contextExpression, typeof(List<>), MapItemExpr); + } + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Mappers/EnumerableMapperBase.cs b/tools/AutoMapper/Mappers/EnumerableMapperBase.cs new file mode 100644 index 0000000000..4e889b93bd --- /dev/null +++ b/tools/AutoMapper/Mappers/EnumerableMapperBase.cs @@ -0,0 +1,18 @@ +using System.Linq.Expressions; +using AutoMapper.Mappers.Internal; + +namespace AutoMapper.Mappers +{ + public abstract class EnumerableMapperBase : IObjectMapperInfo + { + public TypePair GetAssociatedTypes(TypePair initialTypes) + { + var sourceElementType = ElementTypeHelper.GetElementType(initialTypes.SourceType); + var destElementType = ElementTypeHelper.GetElementType(initialTypes.DestinationType); + return new TypePair(sourceElementType, destElementType); + } + + public abstract bool IsMatch(TypePair context); + public abstract Expression MapExpression(IConfigurationProvider configurationProvider, ProfileMap profileMap, PropertyMap propertyMap, Expression sourceExpression, Expression destExpression, Expression contextExpression); + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Mappers/EnumerableToDictionaryMapper.cs b/tools/AutoMapper/Mappers/EnumerableToDictionaryMapper.cs new file mode 100644 index 0000000000..c995daea25 --- /dev/null +++ b/tools/AutoMapper/Mappers/EnumerableToDictionaryMapper.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Linq.Expressions; +using AutoMapper.Configuration; +using AutoMapper.Mappers.Internal; + +namespace AutoMapper.Mappers +{ + using static CollectionMapperExpressionFactory; + + public class EnumerableToDictionaryMapper : IObjectMapper + { + public bool IsMatch(TypePair context) => context.DestinationType.IsDictionaryType() + && context.SourceType.IsEnumerableType() + && !context.SourceType.IsDictionaryType(); + + public Expression MapExpression(IConfigurationProvider configurationProvider, ProfileMap profileMap, PropertyMap propertyMap, Expression sourceExpression, Expression destExpression, Expression contextExpression) + => + MapCollectionExpression(configurationProvider, profileMap, propertyMap, sourceExpression, destExpression, + contextExpression, typeof(Dictionary<,>), MapItemExpr); + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Mappers/ExplicitConversionOperatorMapper.cs b/tools/AutoMapper/Mappers/ExplicitConversionOperatorMapper.cs new file mode 100644 index 0000000000..9501ecff89 --- /dev/null +++ b/tools/AutoMapper/Mappers/ExplicitConversionOperatorMapper.cs @@ -0,0 +1,35 @@ +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; + +namespace AutoMapper.Mappers +{ + public class ExplicitConversionOperatorMapper : IObjectMapper + { + public bool IsMatch(TypePair context) + { + var methodInfo = GetExplicitConversionOperator(context); + + return methodInfo != null; + } + + private static MethodInfo GetExplicitConversionOperator(TypePair context) + { + var sourceTypeMethod = context.SourceType + .GetDeclaredMethods() + .Where(mi => mi.IsPublic && mi.IsStatic) + .Where(mi => mi.Name == "op_Explicit") + .FirstOrDefault(mi => mi.ReturnType == context.DestinationType); + + var destTypeMethod = context.DestinationType.GetDeclaredMethod("op_Explicit", new[] {context.SourceType}); + + return sourceTypeMethod ?? destTypeMethod; + } + + public Expression MapExpression(IConfigurationProvider configurationProvider, ProfileMap profileMap, PropertyMap propertyMap, Expression sourceExpression, Expression destExpression, Expression contextExpression) + { + var implicitOperator = GetExplicitConversionOperator(new TypePair(sourceExpression.Type, destExpression.Type)); + return Expression.Call(null, implicitOperator, sourceExpression); + } + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Mappers/ExpressionMapper.cs b/tools/AutoMapper/Mappers/ExpressionMapper.cs new file mode 100644 index 0000000000..0249753887 --- /dev/null +++ b/tools/AutoMapper/Mappers/ExpressionMapper.cs @@ -0,0 +1,281 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using AutoMapper.Configuration; +using AutoMapper.XpressionMapper.Extensions; +using static System.Linq.Expressions.Expression; + +namespace AutoMapper.Mappers +{ + public class ExpressionMapper : IObjectMapper + { + private static TDestination Map(TSource expression, ResolutionContext context) + where TSource : LambdaExpression + where TDestination : LambdaExpression => context.Mapper.MapExpression(expression); + + private static readonly MethodInfo MapMethodInfo = typeof(ExpressionMapper).GetDeclaredMethod(nameof(Map)); + + public bool IsMatch(TypePair context) => typeof(LambdaExpression).IsAssignableFrom(context.SourceType) + && context.SourceType != typeof(LambdaExpression) + && typeof(LambdaExpression).IsAssignableFrom(context.DestinationType) + && context.DestinationType != typeof(LambdaExpression); + + public Expression MapExpression(IConfigurationProvider configurationProvider, ProfileMap profileMap, PropertyMap propertyMap, Expression sourceExpression, Expression destExpression, Expression contextExpression) => + Call(null, + MapMethodInfo.MakeGenericMethod(sourceExpression.Type, destExpression.Type), + sourceExpression, + contextExpression); + + internal class MappingVisitor : ExpressionVisitor + { + private IList _destSubTypes = new Type[0]; + + private readonly IConfigurationProvider _configurationProvider; + private readonly TypeMap _typeMap; + private readonly Expression _oldParam; + private readonly Expression _newParam; + private readonly MappingVisitor _parentMappingVisitor; + + public MappingVisitor(IConfigurationProvider configurationProvider, IList destSubTypes) + : this(configurationProvider, null, Parameter(typeof(Nullable)), Parameter(typeof(Nullable)), null, destSubTypes) + { + } + + internal MappingVisitor(IConfigurationProvider configurationProvider, TypeMap typeMap, Expression oldParam, Expression newParam, MappingVisitor parentMappingVisitor = null, IList destSubTypes = null) + { + _configurationProvider = configurationProvider; + _typeMap = typeMap; + _oldParam = oldParam; + _newParam = newParam; + _parentMappingVisitor = parentMappingVisitor; + if (destSubTypes != null) + _destSubTypes = destSubTypes; + } + + protected override Expression VisitConstant(ConstantExpression node) => ReferenceEquals(node, _oldParam) ? _newParam : node; + + protected override Expression VisitParameter(ParameterExpression node) => ReferenceEquals(node, _oldParam) ? _newParam : node; + + protected override Expression VisitMethodCall(MethodCallExpression node) => base.VisitMethodCall(GetConvertedMethodCall(node)); + + protected override Expression VisitExtension(Expression node) => (int)node.NodeType == 10000 ? node : base.VisitExtension(node); + + private MethodCallExpression GetConvertedMethodCall(MethodCallExpression node) + { + if (!node.Method.IsGenericMethod) + return node; + var convertedArguments = Visit(node.Arguments); + var convertedMethodArgumentTypes = node.Method.GetGenericArguments().Select(t => GetConvertingTypeIfExists(node.Arguments, t, convertedArguments)).ToArray(); + var convertedMethodCall = node.Method.GetGenericMethodDefinition().MakeGenericMethod(convertedMethodArgumentTypes); + return Call(convertedMethodCall, convertedArguments); + } + + private static Type GetConvertingTypeIfExists(IList args, Type t, IList arguments) + { + var matchingArgument = args.Where(a => !a.Type.IsGenericType()).FirstOrDefault(a => a.Type == t); + if (matchingArgument != null) + { + var index = args.IndexOf(matchingArgument); + return index < 0 ? t : arguments[index].Type; + } + + var matchingEnumerableArgument = args.Where(a => a.Type.IsGenericType()).FirstOrDefault(a => a.Type.GetTypeInfo().GenericTypeArguments[0] == t); + var index2 = args.IndexOf(matchingEnumerableArgument); + return index2 < 0 ? t : arguments[index2].Type.GetTypeInfo().GenericTypeArguments[0]; + } + + protected override Expression VisitBinary(BinaryExpression node) + { + var newLeft = Visit(node.Left); + var newRight = Visit(node.Right); + + if (newLeft.Type != newRight.Type && newRight.Type == typeof(string)) + newLeft = Call(newLeft, typeof(object).GetDeclaredMethod("ToString")); + if (newRight.Type != newLeft.Type && newLeft.Type == typeof(string)) + newRight = Call(newRight, typeof(object).GetDeclaredMethod("ToString")); + CheckNullableToNonNullableChanges(node.Left, node.Right, ref newLeft, ref newRight); + CheckNullableToNonNullableChanges(node.Right, node.Left, ref newRight, ref newLeft); + return MakeBinary(node.NodeType, newLeft, newRight); + } + + private static void CheckNullableToNonNullableChanges(Expression left, Expression right, ref Expression newLeft, ref Expression newRight) + { + if (GoingFromNonNullableToNullable(left, newLeft)) + if (BothAreNonNullable(right, newRight)) + UpdateToNullableExpression(right, out newRight); + else if (BothAreNullable(right, newRight)) + UpdateToNonNullableExpression(right, out newRight); + + if (GoingFromNonNullableToNullable(newLeft, left)) + if (BothAreNonNullable(right, newRight)) + UpdateToNullableExpression(right, out newRight); + else if (BothAreNullable(right, newRight)) + UpdateToNonNullableExpression(right, out newRight); + } + + private static void UpdateToNullableExpression(Expression right, out Expression newRight) + { + newRight = right is ConstantExpression expression + ? Constant(expression.Value, + typeof(Nullable<>).MakeGenericType(right.Type)) + : throw new AutoMapperMappingException( + "Mapping a BinaryExpression where one side is nullable and the other isn't"); + } + + private static void UpdateToNonNullableExpression(Expression right, out Expression newRight) + { + if (right is ConstantExpression expression) + { + var t = right.Type.IsNullableType() + ? right.Type.GetGenericArguments()[0] + : right.Type; + newRight = Constant(expression.Value, t); + } + else if (right is UnaryExpression) + newRight = ((UnaryExpression) right).Operand; + else + throw new AutoMapperMappingException( + "Mapping a BinaryExpression where one side is nullable and the other isn't"); + } + + private static bool GoingFromNonNullableToNullable(Expression node, Expression newLeft) + => !node.Type.IsNullableType() && newLeft.Type.IsNullableType(); + + private static bool BothAreNullable(Expression node, Expression newLeft) + => node.Type.IsNullableType() && newLeft.Type.IsNullableType(); + + private static bool BothAreNonNullable(Expression node, Expression newLeft) + => !node.Type.IsNullableType() && !newLeft.Type.IsNullableType(); + + protected override Expression VisitLambda(Expression expression) + { + return expression.Parameters.Any(b => b.Type == _oldParam.Type) + ? VisitLambdaExpression(expression) + : VisitAllParametersExpression(expression); + } + + private Expression VisitLambdaExpression(Expression expression) + { + var convertedBody = Visit(expression.Body); + var convertedArguments = expression.Parameters.Select(e => Visit(e) as ParameterExpression).ToList(); + return Lambda(convertedBody, convertedArguments); + } + + private Expression VisitAllParametersExpression(Expression expression) + { + var visitors = ( + from t in expression.Parameters + let sourceParamType = t.Type + from destParamType in _destSubTypes.Where(dt => dt != sourceParamType) + let a = destParamType.IsGenericType() ? destParamType.GetTypeInfo().GenericTypeArguments[0] : destParamType + let typeMap = _configurationProvider.ResolveTypeMap(a, sourceParamType) + where typeMap != null + let oldParam = t + let newParam = Parameter(a, oldParam.Name) + select new MappingVisitor(_configurationProvider, typeMap, oldParam, newParam, this)) + .Cast() + .ToList(); + + return visitors.Aggregate(expression as Expression, (e, v) => v.Visit(e)); + } + + protected override Expression VisitMember(MemberExpression node) + { + if (node == _oldParam) + return _newParam; + var propertyMap = PropertyMap(node); + + if (propertyMap == null) + { + if (node.Expression is MemberExpression) + return GetConvertedSubMemberCall(node); + return node; + } + + var constantVisitor = new IsConstantExpressionVisitor(); + constantVisitor.Visit(node); + if (constantVisitor.IsConstant) + return node; + + SetSorceSubTypes(propertyMap); + + var replacedExpression = Visit(node.Expression); + if (replacedExpression == node.Expression) + replacedExpression = _parentMappingVisitor.Visit(node.Expression); + + if (propertyMap.CustomExpression != null) + return propertyMap.CustomExpression.ReplaceParameters(replacedExpression); + + Func getExpression = MakeMemberAccess; + + return propertyMap.SourceMembers + .Aggregate(replacedExpression, getExpression); + } + + private class IsConstantExpressionVisitor : ExpressionVisitor + { + public bool IsConstant { get; private set; } + + protected override Expression VisitConstant(ConstantExpression node) + { + IsConstant = true; + + return base.VisitConstant(node); + } + } + + private Expression GetConvertedSubMemberCall(MemberExpression node) + { + var baseExpression = Visit(node.Expression); + var propertyMap = FindPropertyMapOfExpression(node.Expression as MemberExpression); + if (propertyMap == null) + return node; + var sourceType = GetSourceType(propertyMap); + var destType = propertyMap.DestinationPropertyType; + if (sourceType == destType) + return MakeMemberAccess(baseExpression, node.Member); + var typeMap = _configurationProvider.ResolveTypeMap(sourceType, destType); + var subVisitor = new MappingVisitor(_configurationProvider, typeMap, node.Expression, baseExpression, this); + var newExpression = subVisitor.Visit(node); + _destSubTypes = _destSubTypes.Concat(subVisitor._destSubTypes).ToArray(); + return newExpression; + } + + private Type GetSourceType(PropertyMap propertyMap) => + propertyMap.SourceType ?? + throw new AutoMapperMappingException( + "Could not determine source property type. Make sure the property is mapped.", + null, + new TypePair(null, propertyMap.DestinationPropertyType), + propertyMap.TypeMap, + propertyMap); + + private PropertyMap FindPropertyMapOfExpression(MemberExpression expression) + { + var propertyMap = PropertyMap(expression); + return propertyMap == null && expression.Expression is MemberExpression + ? FindPropertyMapOfExpression((MemberExpression) expression.Expression) + : propertyMap; + } + + private PropertyMap PropertyMap(MemberExpression node) + => _typeMap == null + ? null + : (node.Member.IsStatic() + ? null + : (!node.Member.DeclaringType.IsAssignableFrom(_typeMap.DestinationType) + ? null + : _typeMap.GetExistingPropertyMapFor(node.Member))); + + private void SetSorceSubTypes(PropertyMap propertyMap) + { + if (propertyMap.SourceMember is PropertyInfo info) + _destSubTypes = info.PropertyType.GetTypeInfo().GenericTypeArguments.Concat(new[] { info.PropertyType }).ToList(); + else if (propertyMap.SourceMember is FieldInfo fInfo) + _destSubTypes = fInfo.FieldType.GetTypeInfo().GenericTypeArguments; + } + } + } +} diff --git a/tools/AutoMapper/Mappers/FlagsEnumMapper.cs b/tools/AutoMapper/Mappers/FlagsEnumMapper.cs new file mode 100644 index 0000000000..741e9ced87 --- /dev/null +++ b/tools/AutoMapper/Mappers/FlagsEnumMapper.cs @@ -0,0 +1,40 @@ +using System; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using AutoMapper.Internal; +using AutoMapper.Mappers.Internal; + +namespace AutoMapper.Mappers +{ + using static Expression; + using static ExpressionFactory; + + public class FlagsEnumMapper : IObjectMapper + { + private static readonly MethodInfo EnumParseMethod = Method(() => Enum.Parse(null, null, true)); + + public bool IsMatch(TypePair context) + { + var sourceEnumType = ElementTypeHelper.GetEnumerationType(context.SourceType); + var destEnumType = ElementTypeHelper.GetEnumerationType(context.DestinationType); + + return sourceEnumType != null + && destEnumType != null + && sourceEnumType.GetCustomAttributes(typeof (FlagsAttribute), false).Any() + && destEnumType.GetCustomAttributes(typeof (FlagsAttribute), false).Any(); + } + + public Expression MapExpression(IConfigurationProvider configurationProvider, ProfileMap profileMap, + PropertyMap propertyMap, Expression sourceExpression, Expression destExpression, + Expression contextExpression) => + ToType( + Call(EnumParseMethod, + Constant(destExpression.Type), + Call(sourceExpression, sourceExpression.Type.GetDeclaredMethod("ToString")), + Constant(true) + ), + destExpression.Type + ); + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Mappers/FromDynamicMapper.cs b/tools/AutoMapper/Mappers/FromDynamicMapper.cs new file mode 100644 index 0000000000..8be145de2e --- /dev/null +++ b/tools/AutoMapper/Mappers/FromDynamicMapper.cs @@ -0,0 +1,62 @@ +using System; +using System.Linq.Expressions; +using System.Reflection; +using System.Runtime.CompilerServices; +using AutoMapper.Execution; +using AutoMapper.Internal; +using Microsoft.CSharp.RuntimeBinder; +using Binder = Microsoft.CSharp.RuntimeBinder.Binder; + +namespace AutoMapper.Mappers +{ + using static Expression; + using static ExpressionFactory; + + public class FromDynamicMapper : IObjectMapper + { + private static TDestination Map(TSource source, TDestination destination, ResolutionContext context, ProfileMap profileMap) + { + object boxedDestination = destination; + var destinationTypeDetails = profileMap.CreateTypeDetails(typeof(TDestination)); + foreach (var member in destinationTypeDetails.PublicWriteAccessors) + { + object sourceMemberValue; + try + { + sourceMemberValue = GetDynamically(member.Name, source); + } + catch (RuntimeBinderException) + { + continue; + } + var destinationMemberValue = context.MapMember(member, sourceMemberValue, boxedDestination); + member.SetMemberValue(boxedDestination, destinationMemberValue); + } + return (TDestination) boxedDestination; + } + + private static object GetDynamically(string memberName, object target) + { + var binder = Binder.GetMember(CSharpBinderFlags.None, memberName, null, + new[] { CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null) }); + var callsite = CallSite>.Create(binder); + return callsite.Target(callsite, target); + } + + private static readonly MethodInfo MapMethodInfo = typeof(FromDynamicMapper).GetDeclaredMethod(nameof(Map)); + + public bool IsMatch(TypePair context) => context.SourceType.IsDynamic() && !context.DestinationType.IsDynamic(); + + public Expression MapExpression(IConfigurationProvider configurationProvider, ProfileMap profileMap, + PropertyMap propertyMap, Expression sourceExpression, Expression destExpression, + Expression contextExpression) => + Call(null, + MapMethodInfo.MakeGenericMethod(sourceExpression.Type, destExpression.Type), + sourceExpression, + ToType( + Coalesce(ToObject(destExpression), + DelegateFactory.GenerateConstructorExpression(destExpression.Type)), destExpression.Type), + contextExpression, + Constant(profileMap)); + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Mappers/FromStringDictionaryMapper.cs b/tools/AutoMapper/Mappers/FromStringDictionaryMapper.cs new file mode 100644 index 0000000000..b3e6a9e400 --- /dev/null +++ b/tools/AutoMapper/Mappers/FromStringDictionaryMapper.cs @@ -0,0 +1,48 @@ +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using AutoMapper.Execution; +using AutoMapper.Internal; +using static System.Linq.Expressions.Expression; +using StringDictionary = System.Collections.Generic.IDictionary; + +namespace AutoMapper.Mappers +{ + using static ExpressionFactory; + + public class FromStringDictionaryMapper : IObjectMapper + { + private static readonly MethodInfo MapMethodInfo = + typeof(FromStringDictionaryMapper).GetDeclaredMethod(nameof(Map)); + + public bool IsMatch(TypePair context) => typeof(StringDictionary).IsAssignableFrom(context.SourceType); + + public Expression MapExpression(IConfigurationProvider configurationProvider, ProfileMap profileMap, + PropertyMap propertyMap, Expression sourceExpression, Expression destExpression, + Expression contextExpression) => + Call(null, + MapMethodInfo.MakeGenericMethod(destExpression.Type), + sourceExpression, + Condition( + Equal(ToObject(destExpression), Constant(null)), + DelegateFactory.GenerateConstructorExpression(destExpression.Type), + destExpression), + contextExpression, + Constant(profileMap)); + + private static TDestination Map(StringDictionary source, TDestination destination, ResolutionContext context, ProfileMap profileMap) + { + var destTypeDetails = profileMap.CreateTypeDetails(typeof(TDestination)); + var members = from name in source.Keys + join member in destTypeDetails.PublicWriteAccessors on name equals member.Name + select member; + object boxedDestination = destination; + foreach (var member in members) + { + var value = context.MapMember(member, source[member.Name], boxedDestination); + member.SetMemberValue(boxedDestination, value); + } + return (TDestination) boxedDestination; + } + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Mappers/HashSetMapper.cs b/tools/AutoMapper/Mappers/HashSetMapper.cs new file mode 100644 index 0000000000..4f13626a6f --- /dev/null +++ b/tools/AutoMapper/Mappers/HashSetMapper.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using AutoMapper.Configuration; +using AutoMapper.Mappers.Internal; + +namespace AutoMapper.Mappers +{ + using static CollectionMapperExpressionFactory; + + public class HashSetMapper : IObjectMapper + { + public bool IsMatch(TypePair context) + => context.SourceType.IsEnumerableType() && IsSetType(context.DestinationType); + + public Expression MapExpression(IConfigurationProvider configurationProvider, ProfileMap profileMap, PropertyMap propertyMap, Expression sourceExpression, Expression destExpression, Expression contextExpression) + => MapCollectionExpression(configurationProvider, profileMap, propertyMap, sourceExpression, destExpression, contextExpression, typeof(HashSet<>), MapItemExpr); + + private static bool IsSetType(Type type) => type.IsSetType(); + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Mappers/ImplicitConversionOperatorMapper.cs b/tools/AutoMapper/Mappers/ImplicitConversionOperatorMapper.cs new file mode 100644 index 0000000000..24e40219b4 --- /dev/null +++ b/tools/AutoMapper/Mappers/ImplicitConversionOperatorMapper.cs @@ -0,0 +1,34 @@ +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using AutoMapper.Configuration; + +namespace AutoMapper.Mappers +{ + public class ImplicitConversionOperatorMapper : IObjectMapper + { + public bool IsMatch(TypePair context) + { + var methodInfo = GetImplicitConversionOperator(context); + + return methodInfo != null; + } + + private static MethodInfo GetImplicitConversionOperator(TypePair context) + { + var destinationType = context.DestinationType; + var sourceTypeMethod = context.SourceType + .GetDeclaredMethods() + .FirstOrDefault(mi => mi.IsPublic && mi.IsStatic && mi.Name == "op_Implicit" && mi.ReturnType == destinationType); + + return sourceTypeMethod ?? destinationType.GetDeclaredMethod("op_Implicit", new[] { context.SourceType }); + } + + + public Expression MapExpression(IConfigurationProvider configurationProvider, ProfileMap profileMap, PropertyMap propertyMap, Expression sourceExpression, Expression destExpression, Expression contextExpression) + { + var implicitOperator = GetImplicitConversionOperator(new TypePair(sourceExpression.Type, destExpression.Type)); + return Expression.Call(null, implicitOperator, sourceExpression); + } + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Mappers/Internal/CollectionMapperExpressionFactory.cs b/tools/AutoMapper/Mappers/Internal/CollectionMapperExpressionFactory.cs new file mode 100644 index 0000000000..1b02e60a8b --- /dev/null +++ b/tools/AutoMapper/Mappers/Internal/CollectionMapperExpressionFactory.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using AutoMapper.Execution; +using AutoMapper.Internal; + +namespace AutoMapper.Mappers.Internal +{ + using static Expression; + using static ExpressionBuilder; + using static ExpressionFactory; + using static ElementTypeHelper; + + public static class CollectionMapperExpressionFactory + { + public delegate Expression MapItem(IConfigurationProvider configurationProvider, ProfileMap profileMap, + Type sourceType, Type destType, Expression contextParam, + out ParameterExpression itemParam); + + public static Expression MapCollectionExpression(IConfigurationProvider configurationProvider, ProfileMap profileMap, PropertyMap propertyMap, Expression sourceExpression, Expression destExpression, Expression contextExpression, Type ifInterfaceType, MapItem mapItem) + { + var passedDestination = Variable(destExpression.Type, "passedDestination"); + var newExpression = Variable(passedDestination.Type, "collectionDestination"); + var sourceElementType = GetElementType(sourceExpression.Type); + + var itemExpr = mapItem(configurationProvider, profileMap, sourceExpression.Type, passedDestination.Type, + contextExpression, out ParameterExpression itemParam); + + var destinationElementType = itemExpr.Type; + var destinationCollectionType = typeof(ICollection<>).MakeGenericType(destinationElementType); + if (!destinationCollectionType.IsAssignableFrom(destExpression.Type)) + destinationCollectionType = typeof(IList); + var addMethod = destinationCollectionType.GetDeclaredMethod("Add"); + + Expression destination, assignNewExpression; + + UseDestinationValue(); + + var addItems = ForEach(sourceExpression, itemParam, Call(destination, addMethod, itemExpr)); + var mapExpr = Block(addItems, destination); + + var clearMethod = destinationCollectionType.GetDeclaredMethod("Clear"); + var checkNull = + Block(new[] { newExpression, passedDestination }, + Assign(passedDestination, destExpression), + assignNewExpression, + Call(destination, clearMethod), + mapExpr + ); + if (propertyMap != null) + return checkNull; + var elementTypeMap = configurationProvider.ResolveTypeMap(sourceElementType, destinationElementType); + if (elementTypeMap == null) + return checkNull; + var checkContext = CheckContext(elementTypeMap, contextExpression); + if (checkContext == null) + return checkNull; + return Block(checkContext, checkNull); + void UseDestinationValue() + { + if(propertyMap?.UseDestinationValue == true) + { + destination = passedDestination; + assignNewExpression = Empty(); + } + else + { + destination = newExpression; + Expression createInstance = passedDestination.Type.NewExpr(ifInterfaceType); + var isReadOnly = Property(ToType(passedDestination, destinationCollectionType), "IsReadOnly"); + assignNewExpression = Assign(newExpression, + Condition(OrElse(Equal(passedDestination, Constant(null)), isReadOnly), ToType(createInstance, passedDestination.Type), passedDestination)); + } + } + } + + private static Expression NewExpr(this Type baseType, Type ifInterfaceType) + { + var newExpr = baseType.IsInterface() + ? New( + ifInterfaceType.MakeGenericType(GetElementTypes(baseType, + ElementTypeFlags.BreakKeyValuePair))) + : DelegateFactory.GenerateConstructorExpression(baseType); + return newExpr; + } + + public static Expression MapItemExpr(IConfigurationProvider configurationProvider, ProfileMap profileMap, Type sourceType, Type destType, Expression contextParam, out ParameterExpression itemParam) + { + var sourceElementType = GetElementType(sourceType); + var destElementType = GetElementType(destType); + itemParam = Parameter(sourceElementType, "item"); + + var typePair = new TypePair(sourceElementType, destElementType); + + var itemExpr = MapExpression(configurationProvider, profileMap, typePair, itemParam, contextParam); + return ToType(itemExpr, destElementType); + } + + public static Expression MapKeyPairValueExpr(IConfigurationProvider configurationProvider, ProfileMap profileMap, Type sourceType, Type destType, Expression contextParam, out ParameterExpression itemParam) + { + var sourceElementTypes = GetElementTypes(sourceType, ElementTypeFlags.BreakKeyValuePair); + var destElementTypes = GetElementTypes(destType, ElementTypeFlags.BreakKeyValuePair); + + var typePairKey = new TypePair(sourceElementTypes[0], destElementTypes[0]); + var typePairValue = new TypePair(sourceElementTypes[1], destElementTypes[1]); + + var sourceElementType = typeof(KeyValuePair<,>).MakeGenericType(sourceElementTypes); + itemParam = Parameter(sourceElementType, "item"); + var destElementType = typeof(KeyValuePair<,>).MakeGenericType(destElementTypes); + + var keyExpr = MapExpression(configurationProvider, profileMap, typePairKey, + Property(itemParam, "Key"), contextParam); + var valueExpr = MapExpression(configurationProvider, profileMap, typePairValue, + Property(itemParam, "Value"), contextParam); + var keyPair = New(destElementType.GetDeclaredConstructors().First(), keyExpr, valueExpr); + return keyPair; + } + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Mappers/Internal/ElementTypeHelper.cs b/tools/AutoMapper/Mappers/Internal/ElementTypeHelper.cs new file mode 100644 index 0000000000..f9cf05ca68 --- /dev/null +++ b/tools/AutoMapper/Mappers/Internal/ElementTypeHelper.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using AutoMapper.Configuration; + +namespace AutoMapper.Mappers.Internal +{ + public static class ElementTypeHelper + { + public static Type GetElementType(Type enumerableType) => GetElementTypes(enumerableType, null)[0]; + + public static Type[] GetElementTypes(Type enumerableType, ElementTypeFlags flags = ElementTypeFlags.None) => + GetElementTypes(enumerableType, null, flags); + + public static Type GetElementType(Type enumerableType, IEnumerable enumerable) => GetElementTypes(enumerableType, enumerable)[0]; + + public static Type[] GetElementTypes(Type enumerableType, IEnumerable enumerable, + ElementTypeFlags flags = ElementTypeFlags.None) + { + if (enumerableType.HasElementType) + { + return new[] {enumerableType.GetElementType()}; + } + + var idictionaryType = enumerableType.GetDictionaryType(); + if (idictionaryType != null && flags.HasFlag(ElementTypeFlags.BreakKeyValuePair)) + { + return idictionaryType.GetTypeInfo().GenericTypeArguments; + } + + var ienumerableType = enumerableType.GetIEnumerableType(); + if (ienumerableType != null) + { + return ienumerableType.GetTypeInfo().GenericTypeArguments; + } + + if (typeof(IEnumerable).IsAssignableFrom(enumerableType)) + { + var first = enumerable?.Cast().FirstOrDefault(); + + return new[] {first?.GetType() ?? typeof(object)}; + } + + throw new ArgumentException($"Unable to find the element type for type '{enumerableType}'.", + nameof(enumerableType)); + } + + public static Type GetEnumerationType(Type enumType) + { + return !enumType.IsEnum() ? null : enumType; + } + + internal static IEnumerable GetStaticMethods(this Type type) + { + return type.GetRuntimeMethods().Where(m => m.IsStatic); + } + } + + public enum ElementTypeFlags + { + None = 0, + BreakKeyValuePair = 1 + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Mappers/MapperRegistry.cs b/tools/AutoMapper/Mappers/MapperRegistry.cs new file mode 100644 index 0000000000..4f1d758241 --- /dev/null +++ b/tools/AutoMapper/Mappers/MapperRegistry.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; + +namespace AutoMapper.Mappers +{ + internal static class MapperRegistry + { + public static IList Mappers() => new List + { + new NullableSourceMapper(), + new NullableDestinationMapper(), + new ExpressionMapper(), + new FlagsEnumMapper(), + new StringToEnumMapper(), + new EnumToStringMapper(), + new EnumToEnumMapper(), + new EnumToUnderlyingTypeMapper(), + new UnderlyingTypeToEnumMapper(), + new MultidimensionalArrayMapper(), + new ArrayMapper(), + new EnumerableToDictionaryMapper(), +#if NETSTANDARD1_3 || NET45 || NET40 + new NameValueCollectionMapper(), +#endif + new DictionaryMapper(), + new ReadOnlyCollectionMapper(), + new HashSetMapper(), + new CollectionMapper(), + new EnumerableMapper(), + new AssignableMapper(), + new ConvertMapper(), + new StringMapper(), +#if NETSTANDARD1_3 || NET45 || NET40 + new TypeConverterMapper(), +#endif + new ImplicitConversionOperatorMapper(), + new ExplicitConversionOperatorMapper(), + new FromStringDictionaryMapper(), + new ToStringDictionaryMapper(), + new FromDynamicMapper(), + new ToDynamicMapper() + }; + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Mappers/MultidimensionalArrayMapper.cs b/tools/AutoMapper/Mappers/MultidimensionalArrayMapper.cs new file mode 100644 index 0000000000..507f2d3afb --- /dev/null +++ b/tools/AutoMapper/Mappers/MultidimensionalArrayMapper.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using AutoMapper.Configuration; +using AutoMapper.Mappers.Internal; + +namespace AutoMapper.Mappers +{ + using static Expression; + + public class MultidimensionalArrayMapper : IObjectMapper + { + private static Array Map(TSource source, ResolutionContext context, ProfileMap profileMap) + where TSource : IEnumerable + { + var destElementType = ElementTypeHelper.GetElementType(typeof(TDestination)); + + if (typeof(TDestination).IsAssignableFrom(typeof(TSource))) + { + var elementTypeMap = context.ConfigurationProvider.ResolveTypeMap(typeof(TSourceElement), destElementType); + if (elementTypeMap == null) + return source as Array; + } + + var sourceList = (IEnumerable)source; + var sourceArray = source as Array; + var destinationArray = sourceArray == null + ? Array.CreateInstance(destElementType, sourceList.Cast().Count()) + : Array.CreateInstance(destElementType, Enumerable.Range(0, sourceArray.Rank).Select(sourceArray.GetLength).ToArray()); + + var filler = new MultidimensionalArrayFiller(destinationArray); + foreach (var item in sourceList) + { + filler.NewValue(context.Map(item, null, typeof(TSourceElement), destElementType)); + } + return destinationArray; + } + + private static readonly MethodInfo MapMethodInfo = typeof(MultidimensionalArrayMapper).GetDeclaredMethod(nameof(Map)); + + public bool IsMatch(TypePair context) => + context.DestinationType.IsArray + && context.DestinationType.GetArrayRank() > 1 + && context.SourceType.IsEnumerableType(); + + public Expression MapExpression(IConfigurationProvider configurationProvider, ProfileMap profileMap, + PropertyMap propertyMap, Expression sourceExpression, Expression destExpression, + Expression contextExpression) => + Call(null, + MapMethodInfo.MakeGenericMethod(destExpression.Type, sourceExpression.Type, + ElementTypeHelper.GetElementType(sourceExpression.Type)), + sourceExpression, + contextExpression, + Constant(profileMap)); + + public class MultidimensionalArrayFiller + { + private readonly int[] _indices; + private readonly Array _destination; + + public MultidimensionalArrayFiller(Array destination) + { + _indices = new int[destination.Rank]; + _destination = destination; + } + + public void NewValue(object value) + { + var dimension = _destination.Rank - 1; + var changedDimension = false; + while (_indices[dimension] == _destination.GetLength(dimension)) + { + _indices[dimension] = 0; + dimension--; + if (dimension < 0) + { + throw new InvalidOperationException("Not enough room in destination array " + _destination); + } + _indices[dimension]++; + changedDimension = true; + } + _destination.SetValue(value, _indices); + if (changedDimension) + { + _indices[dimension + 1]++; + } + else + { + _indices[dimension]++; + } + } + } + } + +} \ No newline at end of file diff --git a/tools/AutoMapper/Mappers/NameValueCollectionMapper.cs b/tools/AutoMapper/Mappers/NameValueCollectionMapper.cs new file mode 100644 index 0000000000..e761545501 --- /dev/null +++ b/tools/AutoMapper/Mappers/NameValueCollectionMapper.cs @@ -0,0 +1,29 @@ +using System.Collections.Specialized; +using System.Linq.Expressions; +using static System.Linq.Expressions.Expression; +using System.Reflection; + +#if !NETSTANDARD1_1 +namespace AutoMapper.Mappers +{ + public class NameValueCollectionMapper : IObjectMapper + { + private static NameValueCollection Map(NameValueCollection source) + { + var nvc = new NameValueCollection(); + foreach (var s in source.AllKeys) + nvc.Add(s, source[s]); + + return nvc; + } + + private static readonly MethodInfo MapMethodInfo = typeof(NameValueCollectionMapper).GetDeclaredMethod(nameof(Map)); + + public bool IsMatch(TypePair context) => context.SourceType == typeof (NameValueCollection) && + context.DestinationType == typeof (NameValueCollection); + + public Expression MapExpression(IConfigurationProvider configurationProvider, ProfileMap profileMap, PropertyMap propertyMap, Expression sourceExpression, Expression destExpression, Expression contextExpression) => + Call(null, MapMethodInfo, sourceExpression); + } +} +#endif \ No newline at end of file diff --git a/tools/AutoMapper/Mappers/NullableDestinationMapper.cs b/tools/AutoMapper/Mappers/NullableDestinationMapper.cs new file mode 100644 index 0000000000..63e504d424 --- /dev/null +++ b/tools/AutoMapper/Mappers/NullableDestinationMapper.cs @@ -0,0 +1,28 @@ +using System; +using System.Linq.Expressions; +using AutoMapper.Configuration; +using AutoMapper.Execution; + +namespace AutoMapper.Mappers +{ + public class NullableDestinationMapper : IObjectMapperInfo + { + public bool IsMatch(TypePair context) => context.DestinationType.IsNullableType(); + + public Expression MapExpression(IConfigurationProvider configurationProvider, ProfileMap profileMap, + PropertyMap propertyMap, Expression sourceExpression, Expression destExpression, + Expression contextExpression) => + ExpressionBuilder.MapExpression(configurationProvider, profileMap, + new TypePair(sourceExpression.Type, Nullable.GetUnderlyingType(destExpression.Type)), + sourceExpression, + contextExpression, + propertyMap, + Expression.Property(destExpression, destExpression.Type.GetDeclaredProperty("Value")) + ); + + public TypePair GetAssociatedTypes(TypePair initialTypes) + { + return new TypePair(initialTypes.SourceType, Nullable.GetUnderlyingType(initialTypes.DestinationType)); + } + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Mappers/NullableSourceMapper.cs b/tools/AutoMapper/Mappers/NullableSourceMapper.cs new file mode 100644 index 0000000000..73c8cab62f --- /dev/null +++ b/tools/AutoMapper/Mappers/NullableSourceMapper.cs @@ -0,0 +1,30 @@ +using System; +using System.Linq.Expressions; +using AutoMapper.Configuration; +using AutoMapper.Execution; + +namespace AutoMapper.Mappers +{ + using static Expression; + + public class NullableSourceMapper : IObjectMapperInfo + { + public bool IsMatch(TypePair context) => context.SourceType.IsNullableType(); + + public Expression MapExpression(IConfigurationProvider configurationProvider, ProfileMap profileMap, + PropertyMap propertyMap, Expression sourceExpression, Expression destExpression, + Expression contextExpression) => + ExpressionBuilder.MapExpression(configurationProvider, profileMap, + new TypePair(Nullable.GetUnderlyingType(sourceExpression.Type), destExpression.Type), + Property(sourceExpression, sourceExpression.Type.GetDeclaredProperty("Value")), + contextExpression, + propertyMap, + destExpression + ); + + public TypePair GetAssociatedTypes(TypePair initialTypes) + { + return new TypePair(Nullable.GetUnderlyingType(initialTypes.SourceType), initialTypes.DestinationType); + } + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Mappers/ReadOnlyCollectionMapper.cs b/tools/AutoMapper/Mappers/ReadOnlyCollectionMapper.cs new file mode 100644 index 0000000000..b3b89ad281 --- /dev/null +++ b/tools/AutoMapper/Mappers/ReadOnlyCollectionMapper.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Linq.Expressions; +using AutoMapper.Configuration; +using AutoMapper.Mappers.Internal; + +namespace AutoMapper.Mappers +{ + using static Expression; + using static CollectionMapperExpressionFactory; + + public class ReadOnlyCollectionMapper : IObjectMapper + { + public bool IsMatch(TypePair context) + { + if (!(context.SourceType.IsEnumerableType() && context.DestinationType.IsGenericType())) + return false; + + var genericType = context.DestinationType.GetGenericTypeDefinition(); + + return genericType == typeof (ReadOnlyCollection<>); + } + + public Expression MapExpression(IConfigurationProvider configurationProvider, ProfileMap profileMap, PropertyMap propertyMap, Expression sourceExpression, Expression destExpression, Expression contextExpression) + { + var listType = typeof(List<>).MakeGenericType(ElementTypeHelper.GetElementType(destExpression.Type)); + var list = MapCollectionExpression(configurationProvider, profileMap, propertyMap, sourceExpression, Default(listType), contextExpression, typeof(List<>), MapItemExpr); + var dest = Variable(listType, "dest"); + + return Block(new[] { dest }, Assign(dest, list), Condition(NotEqual(dest, Default(listType)), New(destExpression.Type.GetDeclaredConstructors().First(), dest), Default(destExpression.Type))); + } + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Mappers/StringMapper.cs b/tools/AutoMapper/Mappers/StringMapper.cs new file mode 100644 index 0000000000..58846a2852 --- /dev/null +++ b/tools/AutoMapper/Mappers/StringMapper.cs @@ -0,0 +1,16 @@ +using System.Linq.Expressions; + +namespace AutoMapper.Mappers +{ + using static Expression; + + public class StringMapper : IObjectMapper + { + public bool IsMatch(TypePair context) => context.DestinationType == typeof(string) && context.SourceType != typeof(string); + + public Expression MapExpression(IConfigurationProvider configurationProvider, ProfileMap profileMap, PropertyMap propertyMap, Expression sourceExpression, Expression destExpression, Expression contextExpression) + { + return Call(sourceExpression, typeof(object).GetDeclaredMethod("ToString")); + } + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Mappers/StringToEnumMapper.cs b/tools/AutoMapper/Mappers/StringToEnumMapper.cs new file mode 100644 index 0000000000..1c5198bc17 --- /dev/null +++ b/tools/AutoMapper/Mappers/StringToEnumMapper.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Runtime.Serialization; +using AutoMapper.Internal; +using AutoMapper.Mappers.Internal; + +namespace AutoMapper.Mappers +{ + using static ExpressionFactory; + + public class StringToEnumMapper : IObjectMapper + { + public bool IsMatch(TypePair context) => context.SourceType == typeof(string) && + ElementTypeHelper.GetEnumerationType(context.DestinationType) != null; + + public Expression MapExpression(IConfigurationProvider configurationProvider, ProfileMap profileMap, + PropertyMap propertyMap, Expression sourceExpression, Expression destExpression, + Expression contextExpression) + { + var destinationType = destExpression.Type; + var destinationEnumType = ElementTypeHelper.GetEnumerationType(destinationType); + var enumParse = Expression.Call(typeof(Enum), "Parse", null, Expression.Constant(destinationEnumType), + sourceExpression, Expression.Constant(true)); + var switchCases = new List(); + var enumNames = destinationEnumType.GetDeclaredMembers(); + foreach (var memberInfo in enumNames.Where(x => x.IsStatic())) + { + var attribute = memberInfo.GetCustomAttribute(typeof(EnumMemberAttribute)) as EnumMemberAttribute; + if (attribute?.Value != null) + { + var switchCase = Expression.SwitchCase( + ToType(Expression.Constant(Enum.ToObject(destinationEnumType, memberInfo.GetMemberValue(null))), + destinationType), Expression.Constant(attribute.Value)); + switchCases.Add(switchCase); + } + } + var equalsMethodInfo = Method(() => StringCompareOrdinalIgnoreCase(null, null)); + var switchTable = switchCases.Count > 0 + ? Expression.Switch(sourceExpression, ToType(enumParse, destinationType), equalsMethodInfo, switchCases) + : ToType(enumParse, destinationType); + var isNullOrEmpty = Expression.Call(typeof(string), "IsNullOrEmpty", null, sourceExpression); + return Expression.Condition(isNullOrEmpty, Expression.Default(destinationType), switchTable); + } + + private static bool StringCompareOrdinalIgnoreCase(string x, string y) + { + return StringComparer.OrdinalIgnoreCase.Equals(x, y); + } + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Mappers/ToDynamicMapper.cs b/tools/AutoMapper/Mappers/ToDynamicMapper.cs new file mode 100644 index 0000000000..5249924d05 --- /dev/null +++ b/tools/AutoMapper/Mappers/ToDynamicMapper.cs @@ -0,0 +1,64 @@ +using System; +using System.Linq.Expressions; +using System.Reflection; +using System.Runtime.CompilerServices; +using AutoMapper.Execution; +using AutoMapper.Internal; +using Microsoft.CSharp.RuntimeBinder; +using Binder = Microsoft.CSharp.RuntimeBinder.Binder; + +namespace AutoMapper.Mappers +{ + using static Expression; + using static ExpressionFactory; + + public class ToDynamicMapper : IObjectMapper + { + public static TDestination Map(TSource source, TDestination destination, ResolutionContext context, ProfileMap profileMap) + { + var sourceTypeDetails = profileMap.CreateTypeDetails(typeof(TSource)); + foreach (var member in sourceTypeDetails.PublicReadAccessors) + { + object sourceMemberValue; + try + { + sourceMemberValue = member.GetMemberValue(source); + } + catch (RuntimeBinderException) + { + continue; + } + var destinationMemberValue = context.MapMember(member, sourceMemberValue); + SetDynamically(member.Name, destination, destinationMemberValue); + } + return destination; + } + + private static void SetDynamically(string memberName, object target, object value) + { + var binder = Binder.SetMember(CSharpBinderFlags.None, memberName, null, + new[]{ + CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null), + CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null) + }); + var callsite = CallSite>.Create(binder); + callsite.Target(callsite, target, value); + } + + private static readonly MethodInfo MapMethodInfo = typeof(ToDynamicMapper).GetDeclaredMethod(nameof(Map)); + + public bool IsMatch(TypePair context) => context.DestinationType.IsDynamic() && !context.SourceType.IsDynamic(); + + public Expression MapExpression(IConfigurationProvider configurationProvider, ProfileMap profileMap, + PropertyMap propertyMap, Expression sourceExpression, Expression destExpression, + Expression contextExpression) => + Call(null, + MapMethodInfo.MakeGenericMethod(sourceExpression.Type, destExpression.Type), + sourceExpression, + ToType( + Coalesce(ToObject(destExpression), + DelegateFactory.GenerateConstructorExpression(destExpression.Type)), destExpression.Type), + contextExpression, + Constant(profileMap)); + } +} diff --git a/tools/AutoMapper/Mappers/ToStringDictionaryMapper.cs b/tools/AutoMapper/Mappers/ToStringDictionaryMapper.cs new file mode 100644 index 0000000000..95b5bc45aa --- /dev/null +++ b/tools/AutoMapper/Mappers/ToStringDictionaryMapper.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using AutoMapper.Mappers.Internal; + +namespace AutoMapper.Mappers +{ + using static Expression; + using static CollectionMapperExpressionFactory; + + public class ToStringDictionaryMapper : IObjectMapper + { + private static readonly MethodInfo MembersDictionaryMethodInfo = + typeof(ToStringDictionaryMapper).GetDeclaredMethod(nameof(MembersDictionary)); + + public bool IsMatch(TypePair context) => typeof(IDictionary).IsAssignableFrom(context.DestinationType); + + public Expression MapExpression(IConfigurationProvider configurationProvider, ProfileMap profileMap, PropertyMap propertyMap, Expression sourceExpression, Expression destExpression, Expression contextExpression) + => MapCollectionExpression(configurationProvider, profileMap, propertyMap, + Call(MembersDictionaryMethodInfo, sourceExpression, Constant(profileMap)), destExpression, contextExpression, typeof(Dictionary<,>), + MapKeyPairValueExpr); + + private static Dictionary MembersDictionary(object source, ProfileMap profileMap) + { + var sourceTypeDetails = profileMap.CreateTypeDetails(source.GetType()); + var membersDictionary = sourceTypeDetails.PublicReadAccessors.ToDictionary(p => p.Name, p => p.GetMemberValue(source)); + return membersDictionary; + } + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Mappers/TypeConverterMapper.cs b/tools/AutoMapper/Mappers/TypeConverterMapper.cs new file mode 100644 index 0000000000..0218a8ac5e --- /dev/null +++ b/tools/AutoMapper/Mappers/TypeConverterMapper.cs @@ -0,0 +1,51 @@ +using System; +using System.ComponentModel; +using System.Linq.Expressions; +using System.Reflection; +using AutoMapper.Configuration; +using static System.Linq.Expressions.Expression; + +namespace AutoMapper.Mappers +{ +#if NETSTANDARD1_3 || NET45 || NET40 + public class TypeConverterMapper : IObjectMapper + { + private static TDestination Map(TSource source) + { + var typeConverter = GetTypeConverter(typeof(TSource)); + + if (typeConverter.CanConvertTo(typeof(TDestination))) + { + return (TDestination)typeConverter.ConvertTo(source, typeof(TDestination)); + } + + typeConverter = GetTypeConverter(typeof(TDestination)); + if (typeConverter.CanConvertFrom(typeof(TSource))) + { + return (TDestination)typeConverter.ConvertFrom(source); + } + + return default(TDestination); + } + + private static readonly MethodInfo MapMethodInfo = typeof(TypeConverterMapper).GetDeclaredMethod(nameof(Map)); + + public bool IsMatch(TypePair context) + { + var sourceTypeConverter = GetTypeConverter(context.SourceType); + var destTypeConverter = GetTypeConverter(context.DestinationType); + + return sourceTypeConverter.CanConvertTo(context.DestinationType) || destTypeConverter.CanConvertFrom(context.SourceType); + } + + public Expression MapExpression(IConfigurationProvider configurationProvider, ProfileMap profileMap, + PropertyMap propertyMap, Expression sourceExpression, Expression destExpression, + Expression contextExpression) => + Call(null, + MapMethodInfo.MakeGenericMethod(sourceExpression.Type, destExpression.Type), + sourceExpression); + + private static TypeConverter GetTypeConverter(Type type) => TypeDescriptor.GetConverter(type); + } +#endif +} diff --git a/tools/AutoMapper/Mappers/UnderlyingTypeToEnumMapper.cs b/tools/AutoMapper/Mappers/UnderlyingTypeToEnumMapper.cs new file mode 100644 index 0000000000..344336d35f --- /dev/null +++ b/tools/AutoMapper/Mappers/UnderlyingTypeToEnumMapper.cs @@ -0,0 +1,32 @@ +using System; +using System.Linq.Expressions; +using System.Reflection; +using AutoMapper.Internal; +using AutoMapper.Mappers.Internal; + +namespace AutoMapper.Mappers +{ + using static Expression; + using static ExpressionFactory; + + public class UnderlyingTypeToEnumMapper : IObjectMapper + { + private static readonly MethodInfo EnumToObject = Method(() => Enum.ToObject(typeof(object), null)); + + public bool IsMatch(TypePair context) + { + var destEnumType = ElementTypeHelper.GetEnumerationType(context.DestinationType); + + return destEnumType != null && context.SourceType.IsAssignableFrom(Enum.GetUnderlyingType(destEnumType)); + } + + public Expression MapExpression(IConfigurationProvider configurationProvider, ProfileMap profileMap, + PropertyMap propertyMap, Expression sourceExpression, Expression destExpression, + Expression contextExpression) => + ToType( + Call(EnumToObject, Constant(destExpression.Type), + ToObject(sourceExpression)), + destExpression.Type + ); + } +} \ No newline at end of file diff --git a/tools/AutoMapper/MappingOperationOptions.cs b/tools/AutoMapper/MappingOperationOptions.cs new file mode 100644 index 0000000000..e82d8d5ace --- /dev/null +++ b/tools/AutoMapper/MappingOperationOptions.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using AutoMapper.Configuration; + +namespace AutoMapper +{ + using StringDictionary = Dictionary; + + public class MappingOperationOptions : IMappingOperationOptions + { + private StringDictionary _items; + private static readonly Action Empty = (_, __) => { }; + + public MappingOperationOptions(Func serviceCtor) + { + BeforeMapAction = AfterMapAction = Empty; + ServiceCtor = serviceCtor; + } + + public Func ServiceCtor { get; private set; } + public IDictionary Items => _items ?? (_items = new StringDictionary()); + public Action BeforeMapAction { get; protected set; } + public Action AfterMapAction { get; protected set; } + public ITypeMapConfiguration InlineConfiguration { get; protected set; } = new MappingExpression(MemberList.Destination); + + public void BeforeMap(Action beforeFunction) => BeforeMapAction = beforeFunction; + + public void AfterMap(Action afterFunction) => AfterMapAction = afterFunction; + + public IMappingExpression ConfigureMap() + => ConfigureMap(MemberList.Destination); + + public IMappingExpression ConfigureMap(MemberList memberList) + { + var typeMapConfiguration = new MappingExpression(memberList); + + InlineConfiguration = typeMapConfiguration; + + return typeMapConfiguration; + } + + public T CreateInstance() + { + var service = ServiceCtor(typeof(T)); + if(service == null) + { + throw new AutoMapperMappingException("Cannot create an instance of type " + typeof(T)); + } + return (T) service; + } + + public void ConstructServicesUsing(Func constructor) + { + var ctor = ServiceCtor; + ServiceCtor = t => constructor(t) ?? ctor(t); + } + + void IMappingOperationOptions.BeforeMap(Action beforeFunction) => BeforeMapAction = (s, d) => beforeFunction(s, d); + + void IMappingOperationOptions.AfterMap(Action afterFunction) => AfterMapAction = (s, d) => afterFunction(s, d); + } +} \ No newline at end of file diff --git a/tools/AutoMapper/MemberList.cs b/tools/AutoMapper/MemberList.cs new file mode 100644 index 0000000000..3d42418353 --- /dev/null +++ b/tools/AutoMapper/MemberList.cs @@ -0,0 +1,23 @@ +namespace AutoMapper +{ + /// + /// Member list to check for configuration validation + /// + public enum MemberList + { + /// + /// Check that all destination members are mapped + /// + Destination = 0, + + /// + /// Check that all source members are mapped + /// + Source = 1, + + /// + /// Check neither source nor destination members, skipping validation + /// + None = 2 + } +} diff --git a/tools/AutoMapper/PascalCaseNamingConvention.cs b/tools/AutoMapper/PascalCaseNamingConvention.cs new file mode 100644 index 0000000000..4ccf09ad40 --- /dev/null +++ b/tools/AutoMapper/PascalCaseNamingConvention.cs @@ -0,0 +1,12 @@ +using System.Text.RegularExpressions; + +namespace AutoMapper +{ + public class PascalCaseNamingConvention : INamingConvention + { + public Regex SplittingExpression { get; } = new Regex(@"(\p{Lu}+(?=$|\p{Lu}[\p{Ll}0-9])|\p{Lu}?[\p{Ll}0-9]+)"); + + public string SeparatorCharacter => string.Empty; + public string ReplaceValue(Match match) => match.Value[0].ToString().ToUpper() + match.Value.Substring(1); + } +} \ No newline at end of file diff --git a/tools/AutoMapper/PathMap.cs b/tools/AutoMapper/PathMap.cs new file mode 100644 index 0000000000..269c8d7b2e --- /dev/null +++ b/tools/AutoMapper/PathMap.cs @@ -0,0 +1,28 @@ +using System.Diagnostics; +using System.Linq.Expressions; +using System.Reflection; + +namespace AutoMapper +{ + using System; + using Internal; + + [DebuggerDisplay("{DestinationExpression}")] + public class PathMap + { + public PathMap(LambdaExpression destinationExpression, MemberPath memberPath, TypeMap typeMap) + { + MemberPath = memberPath; + TypeMap = typeMap; + DestinationExpression = destinationExpression; + } + + public TypeMap TypeMap { get; } + public LambdaExpression DestinationExpression { get; } + public LambdaExpression SourceExpression { get; set; } + public MemberPath MemberPath { get; } + public MemberInfo DestinationMember => MemberPath.Last; + public bool Ignored { get; set; } + public LambdaExpression Condition { get; set; } + } +} \ No newline at end of file diff --git a/tools/AutoMapper/Profile.cs b/tools/AutoMapper/Profile.cs new file mode 100644 index 0000000000..c7f8c63ba0 --- /dev/null +++ b/tools/AutoMapper/Profile.cs @@ -0,0 +1,199 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Runtime.CompilerServices; +using AutoMapper.Configuration; +using AutoMapper.Configuration.Conventions; +using AutoMapper.Mappers; + +namespace AutoMapper +{ + /// + /// Provides a named configuration for maps. Naming conventions become scoped per profile. + /// + public abstract class Profile : IProfileExpression, IProfileConfiguration + { + private readonly List> _allPropertyMapActions = + new List>(); + + private readonly List> _allTypeMapActions = + new List>(); + + private readonly List _globalIgnore = new List(); + private readonly IList _memberConfigurations = new List(); + private readonly List _openTypeMapConfigs = new List(); + private readonly List _sourceExtensionMethods = new List(); + private readonly IList _typeConfigurations = new List(); + private readonly List _typeMapConfigs = new List(); + private readonly List _valueTransformerConfigs = new List(); + + protected Profile(string profileName) + : this() => ProfileName = profileName; + + protected Profile() + { + ProfileName = GetType().FullName; + + AddMemberConfiguration() + .AddMember() + .AddName(_ => _.AddStrings(p => p.Prefixes, "Get")); + + SourceMemberNamingConvention = new PascalCaseNamingConvention(); + DestinationMemberNamingConvention = new PascalCaseNamingConvention(); + } + + protected Profile(string profileName, Action configurationAction) + : this(profileName) + { + configurationAction(this); + } + + public IMemberConfiguration DefaultMemberConfig => _memberConfigurations.First(); + public bool? ConstructorMappingEnabled { get; private set; } + public bool? CreateMissingTypeMaps { get; set; } + public bool? ValidateInlineMaps { get; set; } + + IEnumerable> IProfileConfiguration.AllPropertyMapActions + => _allPropertyMapActions; + + IEnumerable> IProfileConfiguration.AllTypeMapActions => _allTypeMapActions; + IEnumerable IProfileConfiguration.GlobalIgnores => _globalIgnore; + IEnumerable IProfileConfiguration.MemberConfigurations => _memberConfigurations; + IEnumerable IProfileConfiguration.SourceExtensionMethods => _sourceExtensionMethods; + IEnumerable IProfileConfiguration.TypeConfigurations => _typeConfigurations; + IEnumerable IProfileConfiguration.TypeMapConfigs => _typeMapConfigs; + IEnumerable IProfileConfiguration.OpenTypeMapConfigs => _openTypeMapConfigs; + IEnumerable IProfileConfiguration.ValueTransformers => _valueTransformerConfigs; + + public virtual string ProfileName { get; } + + public bool? AllowNullDestinationValues { get; set; } + public bool? AllowNullCollections { get; set; } + public bool? EnableNullPropagationForQueryMapping { get; set; } + public Func ShouldMapProperty { get; set; } + public Func ShouldMapField { get; set; } + + public INamingConvention SourceMemberNamingConvention { get; set; } + public INamingConvention DestinationMemberNamingConvention { get; set; } + + public IList ValueTransformers => _valueTransformerConfigs; + + public void DisableConstructorMapping() + { + ConstructorMappingEnabled = false; + } + + public void ForAllMaps(Action configuration) + { + _allTypeMapActions.Add(configuration); + } + + public void ForAllPropertyMaps(Func condition, + Action configuration) + { + _allPropertyMapActions.Add((pm, cfg) => + { + if (condition(pm)) configuration(pm, cfg); + }); + } + + public IMappingExpression CreateMap() => + CreateMap(MemberList.Destination); + + public IMappingExpression CreateMap(MemberList memberList) => + CreateMappingExpression(memberList); + + public IMappingExpression CreateMap(Type sourceType, Type destinationType) => + CreateMap(sourceType, destinationType, MemberList.Destination); + + public IMappingExpression CreateMap(Type sourceType, Type destinationType, MemberList memberList) + { + var map = new MappingExpression(new TypePair(sourceType, destinationType), memberList); + + _typeMapConfigs.Add(map); + + if (sourceType.IsGenericTypeDefinition() || destinationType.IsGenericTypeDefinition()) + _openTypeMapConfigs.Add(map); + + return map; + } + + public void ClearPrefixes() + { + DefaultMemberConfig.AddName(_ => _.Prefixes.Clear()); + } + + public void RecognizeAlias(string original, string alias) + { + DefaultMemberConfig.AddName(_ => _.AddReplace(original, alias)); + } + + public void ReplaceMemberName(string original, string newValue) + { + DefaultMemberConfig.AddName(_ => _.AddReplace(original, newValue)); + } + + public void RecognizePrefixes(params string[] prefixes) + { + DefaultMemberConfig.AddName(_ => _.AddStrings(p => p.Prefixes, prefixes)); + } + + public void RecognizePostfixes(params string[] postfixes) + { + DefaultMemberConfig.AddName(_ => _.AddStrings(p => p.Postfixes, postfixes)); + } + + public void RecognizeDestinationPrefixes(params string[] prefixes) + { + DefaultMemberConfig.AddName(_ => _.AddStrings(p => p.DestinationPrefixes, prefixes)); + } + + public void RecognizeDestinationPostfixes(params string[] postfixes) + { + DefaultMemberConfig.AddName(_ => _.AddStrings(p => p.DestinationPostfixes, postfixes)); + } + + public void AddGlobalIgnore(string propertyNameStartingWith) + { + _globalIgnore.Add(propertyNameStartingWith); + } + + public IMemberConfiguration AddMemberConfiguration() + { + var condition = new MemberConfiguration(); + _memberConfigurations.Add(condition); + return condition; + } + + public IConditionalObjectMapper AddConditionalObjectMapper() + { + var condition = new ConditionalObjectMapper(); + + _typeConfigurations.Add(condition); + + return condition; + } + + public void IncludeSourceExtensionMethods(Type type) + { + _sourceExtensionMethods.AddRange( + type.GetDeclaredMethods() + .Where( + m => + m.IsStatic && m.IsDefined(typeof(ExtensionAttribute), false) && + m.GetParameters().Length == 1)); + } + + private IMappingExpression CreateMappingExpression( + MemberList memberList) + { + var mappingExp = new MappingExpression(memberList); + + _typeMapConfigs.Add(mappingExp); + + return mappingExp; + } + } +} \ No newline at end of file diff --git a/tools/AutoMapper/ProfileMap.cs b/tools/AutoMapper/ProfileMap.cs new file mode 100644 index 0000000000..34349a357d --- /dev/null +++ b/tools/AutoMapper/ProfileMap.cs @@ -0,0 +1,259 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using AutoMapper.Configuration; +using AutoMapper.Configuration.Conventions; +using AutoMapper.Mappers; + +namespace AutoMapper +{ +#if NETSTANDARD1_1 + struct IConvertible{} +#endif + + [DebuggerDisplay("{Name}")] + public class ProfileMap + { + private readonly TypeMapFactory _typeMapFactory = new TypeMapFactory(); + private readonly IEnumerable _typeMapConfigs; + private readonly IEnumerable _openTypeMapConfigs; + private readonly LockingConcurrentDictionary _typeDetails; + + public ProfileMap(IProfileConfiguration profile) + : this(profile, null) + { + } + + public ProfileMap(IProfileConfiguration profile, IConfiguration configuration) + { + _typeDetails = new LockingConcurrentDictionary(TypeDetailsFactory); + + Name = profile.ProfileName; + AllowNullCollections = profile.AllowNullCollections ?? configuration?.AllowNullCollections ?? false; + AllowNullDestinationValues = profile.AllowNullDestinationValues ?? configuration?.AllowNullDestinationValues ?? true; + EnableNullPropagationForQueryMapping = profile.EnableNullPropagationForQueryMapping ?? configuration?.EnableNullPropagationForQueryMapping ?? false; + ConstructorMappingEnabled = profile.ConstructorMappingEnabled ?? configuration?.ConstructorMappingEnabled ?? true; + ShouldMapField = profile.ShouldMapField ?? configuration?.ShouldMapField ?? (p => p.IsPublic()); + ShouldMapProperty = profile.ShouldMapProperty ?? configuration?.ShouldMapProperty ?? (p => p.IsPublic()); + CreateMissingTypeMaps = profile.CreateMissingTypeMaps ?? configuration?.CreateMissingTypeMaps ?? true; + ValidateInlineMaps = profile.ValidateInlineMaps ?? configuration?.ValidateInlineMaps ?? true; + + TypeConfigurations = profile.TypeConfigurations + .Concat(configuration?.TypeConfigurations ?? Enumerable.Empty()) + .ToArray(); + + ValueTransformers = profile.ValueTransformers.Concat(configuration?.ValueTransformers ?? Enumerable.Empty()).ToArray(); + + MemberConfigurations = profile.MemberConfigurations.ToArray(); + + MemberConfigurations.FirstOrDefault()?.AddMember(_ => _.SourceMemberNamingConvention = profile.SourceMemberNamingConvention); + MemberConfigurations.FirstOrDefault()?.AddMember(_ => _.DestinationMemberNamingConvention = profile.DestinationMemberNamingConvention); + + GlobalIgnores = profile.GlobalIgnores.Concat(configuration?.GlobalIgnores ?? Enumerable.Empty()).ToArray(); + SourceExtensionMethods = profile.SourceExtensionMethods.Concat(configuration?.SourceExtensionMethods ?? Enumerable.Empty()).ToArray(); + AllPropertyMapActions = profile.AllPropertyMapActions.Concat(configuration?.AllPropertyMapActions ?? Enumerable.Empty>()).ToArray(); + AllTypeMapActions = profile.AllTypeMapActions.Concat(configuration?.AllTypeMapActions ?? Enumerable.Empty>()).ToArray(); + + Prefixes = + profile.MemberConfigurations + .Select(m => m.NameMapper) + .SelectMany(m => m.NamedMappers) + .OfType() + .SelectMany(m => m.Prefixes) + .ToArray(); + + Postfixes = + profile.MemberConfigurations + .Select(m => m.NameMapper) + .SelectMany(m => m.NamedMappers) + .OfType() + .SelectMany(m => m.Postfixes) + .ToArray(); + + _typeMapConfigs = profile.TypeMapConfigs.ToArray(); + _openTypeMapConfigs = profile.OpenTypeMapConfigs.ToArray(); + } + + + public bool AllowNullCollections { get; } + public bool AllowNullDestinationValues { get; } + public bool ConstructorMappingEnabled { get; } + public bool CreateMissingTypeMaps { get; } + public bool ValidateInlineMaps { get; } + public bool EnableNullPropagationForQueryMapping { get; } + public string Name { get; } + public Func ShouldMapField { get; } + public Func ShouldMapProperty { get; } + + public IEnumerable> AllPropertyMapActions { get; } + public IEnumerable> AllTypeMapActions { get; } + public IEnumerable GlobalIgnores { get; } + public IEnumerable MemberConfigurations { get; } + public IEnumerable SourceExtensionMethods { get; } + public IEnumerable TypeConfigurations { get; } + public IEnumerable Prefixes { get; } + public IEnumerable Postfixes { get; } + public IEnumerable ValueTransformers { get; } + + public TypeDetails CreateTypeDetails(Type type) => _typeDetails.GetOrAdd(type); + + private TypeDetails TypeDetailsFactory(Type type) => new TypeDetails(type, this); + + public void Register(TypeMapRegistry typeMapRegistry) + { + foreach (var config in _typeMapConfigs.Where(c => !c.IsOpenGeneric)) + { + BuildTypeMap(typeMapRegistry, config); + + if (config.ReverseTypeMap != null) + { + BuildTypeMap(typeMapRegistry, config.ReverseTypeMap); + } + } + } + + public void Configure(TypeMapRegistry typeMapRegistry) + { + foreach (var typeMapConfiguration in _typeMapConfigs.Where(c => !c.IsOpenGeneric)) + { + Configure(typeMapRegistry, typeMapConfiguration); + if (typeMapConfiguration.ReverseTypeMap != null) + { + Configure(typeMapRegistry, typeMapConfiguration.ReverseTypeMap); + } + } + } + + private void BuildTypeMap(TypeMapRegistry typeMapRegistry, ITypeMapConfiguration config) + { + var typeMap = _typeMapFactory.CreateTypeMap(config.SourceType, config.DestinationType, this); + + config.Configure(typeMap); + + typeMapRegistry.RegisterTypeMap(typeMap); + } + + private void Configure(TypeMapRegistry typeMapRegistry, ITypeMapConfiguration typeMapConfiguration) + { + var typeMap = typeMapRegistry.GetTypeMap(typeMapConfiguration.Types); + Configure(typeMapRegistry, typeMap); + } + + private void Configure(TypeMapRegistry typeMapRegistry, TypeMap typeMap) + { + foreach (var action in AllTypeMapActions) + { + var expression = new MappingExpression(typeMap.Types, typeMap.ConfiguredMemberList); + + action(typeMap, expression); + + expression.Configure(typeMap); + } + + foreach (var action in AllPropertyMapActions) + { + foreach (var propertyMap in typeMap.GetPropertyMaps()) + { + var memberExpression = new MappingExpression.MemberConfigurationExpression(propertyMap.DestinationProperty, typeMap.SourceType); + + action(propertyMap, memberExpression); + + memberExpression.Configure(typeMap); + } + } + + ApplyBaseMaps(typeMapRegistry, typeMap, typeMap); + ApplyDerivedMaps(typeMapRegistry, typeMap, typeMap); + } + + public bool IsConventionMap(TypePair types) + { + return TypeConfigurations.Any(c => c.IsMatch(types)); + } + + public TypeMap CreateConventionTypeMap(TypeMapRegistry typeMapRegistry, TypePair types) + { + var typeMap = _typeMapFactory.CreateTypeMap(types.SourceType, types.DestinationType, this); + + typeMap.IsConventionMap = true; + + var config = new MappingExpression(typeMap.Types, typeMap.ConfiguredMemberList); + + config.Configure(typeMap); + + Configure(typeMapRegistry, typeMap); + + return typeMap; + } + + public TypeMap CreateInlineMap(TypeMapRegistry typeMapRegistry, TypePair types) + { + var typeMap = _typeMapFactory.CreateTypeMap(types.SourceType, types.DestinationType, this); + + typeMap.IsConventionMap = true; + + Configure(typeMapRegistry, typeMap); + + return typeMap; + } + + public TypeMap CreateClosedGenericTypeMap(ITypeMapConfiguration openMapConfig, TypeMapRegistry typeMapRegistry, TypePair closedTypes) + { + var closedMap = _typeMapFactory.CreateTypeMap(closedTypes.SourceType, closedTypes.DestinationType, this); + + openMapConfig.Configure(closedMap); + + Configure(typeMapRegistry, closedMap); + + if(closedMap.TypeConverterType != null) + { + var typeParams = + (openMapConfig.SourceType.IsGenericTypeDefinition() ? closedTypes.SourceType.GetGenericArguments() : new Type[0]) + .Concat + (openMapConfig.DestinationType.IsGenericTypeDefinition() ? closedTypes.DestinationType.GetGenericArguments() : new Type[0]); + + var neededParameters = closedMap.TypeConverterType.GetGenericParameters().Length; + closedMap.TypeConverterType = closedMap.TypeConverterType.MakeGenericType(typeParams.Take(neededParameters).ToArray()); + } + if(closedMap.DestinationTypeOverride?.IsGenericTypeDefinition() == true) + { + var neededParameters = closedMap.DestinationTypeOverride.GetGenericParameters().Length; + closedMap.DestinationTypeOverride = closedMap.DestinationTypeOverride.MakeGenericType(closedTypes.DestinationType.GetGenericArguments().Take(neededParameters).ToArray()); + } + return closedMap; + } + + public ITypeMapConfiguration GetGenericMap(TypePair closedTypes) + { + return _openTypeMapConfigs + .SelectMany(tm => tm.ReverseTypeMap == null ? new[] { tm } : new[] { tm, tm.ReverseTypeMap }) + .Where(tm => + tm.Types.SourceType.GetGenericTypeDefinitionIfGeneric() == closedTypes.SourceType.GetGenericTypeDefinitionIfGeneric() && + tm.Types.DestinationType.GetGenericTypeDefinitionIfGeneric() == closedTypes.DestinationType.GetGenericTypeDefinitionIfGeneric()) + .OrderByDescending(tm => tm.DestinationType == closedTypes.DestinationType) // Favor more specific destination matches, + .ThenByDescending(tm => tm.SourceType == closedTypes.SourceType) // then more specific source matches + .FirstOrDefault(); + } + + private static void ApplyBaseMaps(TypeMapRegistry typeMapRegistry, TypeMap derivedMap, TypeMap currentMap) + { + foreach (var baseMap in currentMap.IncludedBaseTypes.Select(typeMapRegistry.GetTypeMap).Where(baseMap => baseMap != null)) + { + baseMap.IncludeDerivedTypes(currentMap.SourceType, currentMap.DestinationType); + derivedMap.AddInheritedMap(baseMap); + ApplyBaseMaps(typeMapRegistry, derivedMap, baseMap); + } + } + + private void ApplyDerivedMaps(TypeMapRegistry typeMapRegistry, TypeMap baseMap, TypeMap typeMap) + { + foreach (var inheritedTypeMap in typeMap.IncludedDerivedTypes.Select(typeMapRegistry.GetTypeMap).Where(map => map != null)) + { + inheritedTypeMap.AddInheritedMap(baseMap); + ApplyDerivedMaps(typeMapRegistry, baseMap, inheritedTypeMap); + } + } + } +} \ No newline at end of file diff --git a/tools/AutoMapper/PropertyMap.cs b/tools/AutoMapper/PropertyMap.cs new file mode 100644 index 0000000000..b80fbb02f6 --- /dev/null +++ b/tools/AutoMapper/PropertyMap.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using AutoMapper.Configuration; + +namespace AutoMapper +{ + using static Expression; + + [DebuggerDisplay("{DestinationProperty.Name}")] + public class PropertyMap + { + private readonly List _memberChain = new List(); + private readonly List _valueTransformerConfigs = new List(); + + public PropertyMap(PathMap pathMap) + { + Condition = pathMap.Condition; + DestinationProperty = pathMap.DestinationMember; + CustomExpression = pathMap.SourceExpression; + TypeMap = pathMap.TypeMap; + } + + public PropertyMap(MemberInfo destinationProperty, TypeMap typeMap) + { + TypeMap = typeMap; + DestinationProperty = destinationProperty; + } + + public PropertyMap(PropertyMap inheritedMappedProperty, TypeMap typeMap) + : this(inheritedMappedProperty.DestinationProperty, typeMap) + { + ApplyInheritedPropertyMap(inheritedMappedProperty); + } + + public TypeMap TypeMap { get; } + public MemberInfo DestinationProperty { get; } + + public Type DestinationPropertyType => DestinationProperty.GetMemberType(); + + public ICollection SourceMembers => _memberChain; + + public bool Inline { get; set; } = true; + public bool Ignored { get; set; } + public bool AllowNull { get; set; } + public int? MappingOrder { get; set; } + public LambdaExpression CustomResolver { get; set; } + public LambdaExpression Condition { get; set; } + public LambdaExpression PreCondition { get; set; } + public LambdaExpression CustomExpression { get; set; } + public bool UseDestinationValue { get; set; } + public bool ExplicitExpansion { get; set; } + public object NullSubstitute { get; set; } + public ValueResolverConfiguration ValueResolverConfig { get; set; } + public IEnumerable ValueTransformers => _valueTransformerConfigs; + public string CustomSourceMemberName { get; set; } + + public MemberInfo SourceMember + { + get + { + if (CustomSourceMemberName != null) + return TypeMap.SourceType.GetFieldOrProperty(CustomSourceMemberName); + + if (CustomExpression != null) + { + var finder = new MemberFinderVisitor(); + finder.Visit(CustomExpression); + + if (finder.Member != null) + { + return finder.Member.Member; + } + } + + return _memberChain.LastOrDefault(); + } + } + + public Type SourceType + { + get + { + if (CustomExpression != null) + return CustomExpression.ReturnType; + if (CustomResolver != null) + return CustomResolver.ReturnType; + if(ValueResolverConfig != null) + return typeof(object); + return SourceMember?.GetMemberType(); + } + } + + + public void ChainMembers(IEnumerable members) + { + var getters = members as IList ?? members.ToList(); + _memberChain.AddRange(getters); + } + + public void ApplyInheritedPropertyMap(PropertyMap inheritedMappedProperty) + { + if(inheritedMappedProperty.Ignored && !ResolveConfigured()) + { + Ignored = true; + } + CustomExpression = CustomExpression ?? inheritedMappedProperty.CustomExpression; + CustomResolver = CustomResolver ?? inheritedMappedProperty.CustomResolver; + Condition = Condition ?? inheritedMappedProperty.Condition; + PreCondition = PreCondition ?? inheritedMappedProperty.PreCondition; + NullSubstitute = NullSubstitute ?? inheritedMappedProperty.NullSubstitute; + MappingOrder = MappingOrder ?? inheritedMappedProperty.MappingOrder; + ValueResolverConfig = ValueResolverConfig ?? inheritedMappedProperty.ValueResolverConfig; + CustomSourceMemberName = CustomSourceMemberName ?? inheritedMappedProperty.CustomSourceMemberName; + } + + public bool IsMapped() => HasSource() || Ignored; + + public bool CanResolveValue() => HasSource() && !Ignored; + + public bool HasSource() => _memberChain.Count > 0 || ResolveConfigured(); + + public bool ResolveConfigured() => ValueResolverConfig != null || CustomResolver != null || CustomExpression != null || CustomSourceMemberName != null; + + public void MapFrom(LambdaExpression sourceMember) + { + CustomExpression = sourceMember; + Ignored = false; + } + + public void AddValueTransformation(ValueTransformerConfiguration valueTransformerConfiguration) + { + _valueTransformerConfigs.Add(valueTransformerConfiguration); + } + + private class MemberFinderVisitor : ExpressionVisitor + { + public MemberExpression Member { get; private set; } + + protected override Expression VisitMember(MemberExpression node) + { + Member = node; + + return base.VisitMember(node); + } + } + } +} diff --git a/tools/AutoMapper/QueryableExtensions/ExpressionBuilder.cs b/tools/AutoMapper/QueryableExtensions/ExpressionBuilder.cs new file mode 100644 index 0000000000..ea0207f0cf --- /dev/null +++ b/tools/AutoMapper/QueryableExtensions/ExpressionBuilder.cs @@ -0,0 +1,565 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Runtime.CompilerServices; +using AutoMapper.Configuration; +using AutoMapper.Internal; +using AutoMapper.Execution; +using AutoMapper.QueryableExtensions.Impl; +using static System.Linq.Expressions.Expression; + +namespace AutoMapper.QueryableExtensions +{ + using static ExpressionFactory; + using TypePairCount = IDictionary; + using ParameterBag = IDictionary; + + public interface IExpressionBuilder + { + LambdaExpression[] GetMapExpression(Type sourceType, Type destinationType, ParameterBag parameters, MemberInfo[] membersToExpand); + LambdaExpression[] CreateMapExpression(ExpressionRequest request, TypePairCount typePairCount, LetPropertyMaps letPropertyMaps); + Expression CreateMapExpression(ExpressionRequest request, Expression instanceParameter, TypePairCount typePairCount, LetPropertyMaps letPropertyMaps); + } + + public class ExpressionBuilder : IExpressionBuilder + { + private static readonly IExpressionResultConverter[] ExpressionResultConverters = + { + new MemberResolverExpressionResultConverter(), + new MemberGetterExpressionResultConverter() + }; + + private static readonly IExpressionBinder[] Binders = + { + new CustomProjectionExpressionBinder(), + new NullableDestinationExpressionBinder(), + new NullableSourceExpressionBinder(), + new AssignableExpressionBinder(), + new EnumerableExpressionBinder(), + new MappedTypeExpressionBinder(), + new StringExpressionBinder() + }; + + private readonly LockingConcurrentDictionary _expressionCache; + private readonly IConfigurationProvider _configurationProvider; + + public ExpressionBuilder(IConfigurationProvider configurationProvider) + { + _configurationProvider = configurationProvider; + _expressionCache = new LockingConcurrentDictionary(CreateMapExpression); + } + + public LambdaExpression[] GetMapExpression(Type sourceType, Type destinationType, ParameterBag parameters, + MemberInfo[] membersToExpand) + { + if (sourceType == null) + { + throw new ArgumentNullException(nameof(sourceType)); + } + if (destinationType == null) + { + throw new ArgumentNullException(nameof(destinationType)); + } + if (parameters == null) + { + throw new ArgumentNullException(nameof(parameters)); + } + if (membersToExpand == null) + { + throw new ArgumentNullException(nameof(membersToExpand)); + } + + var cachedExpressions = _expressionCache.GetOrAdd(new ExpressionRequest(sourceType, destinationType, membersToExpand, null)); + + return cachedExpressions.Select(e => Prepare(e, parameters)).Cast().ToArray(); + } + + private Expression Prepare(Expression cachedExpression, ParameterBag parameters) + { + Expression result; + if(parameters.Any()) + { + var visitor = new ConstantExpressionReplacementVisitor(parameters); + result = visitor.Visit(cachedExpression); + } + else + { + result = cachedExpression; + } + // perform null-propagation if this feature is enabled. + if(_configurationProvider.EnableNullPropagationForQueryMapping) + { + var nullVisitor = new NullsafeQueryRewriter(); + return nullVisitor.Visit(result); + } + return result; + } + + private LambdaExpression[] CreateMapExpression(ExpressionRequest request) => CreateMapExpression(request, new Dictionary(), new FirstPassLetPropertyMaps(_configurationProvider)); + + public LambdaExpression[] CreateMapExpression(ExpressionRequest request, TypePairCount typePairCount, LetPropertyMaps letPropertyMaps) + { + // this is the input parameter of this expression with name + var instanceParameter = Parameter(request.SourceType, "dto"); + var expressions = new QueryExpressions(CreateMapExpressionCore(request, instanceParameter, typePairCount, letPropertyMaps, out var typeMap)); + if(letPropertyMaps.Count > 0) + { + expressions = letPropertyMaps.GetSubQueryExpression(this, expressions.First, typeMap, request, instanceParameter, typePairCount); + } + if(expressions.First == null) + { + return null; + } + var firstLambda = Lambda(expressions.First, instanceParameter); + if(expressions.Second == null) + { + return new[] { firstLambda }; + } + return new[] { firstLambda, Lambda(expressions.Second, expressions.SecondParameter) }; + } + + public Expression CreateMapExpression(ExpressionRequest request, Expression instanceParameter, TypePairCount typePairCount, LetPropertyMaps letPropertyMaps) + { + return CreateMapExpressionCore(request, instanceParameter, typePairCount, letPropertyMaps, out var _); + } + + private Expression CreateMapExpressionCore(ExpressionRequest request, Expression instanceParameter, TypePairCount typePairCount, LetPropertyMaps letPropertyMaps, out TypeMap typeMap) + { + typeMap = _configurationProvider.ResolveTypeMap(request.SourceType, request.DestinationType); + + if(typeMap == null) + { + throw QueryMapperHelper.MissingMapException(request.SourceType, request.DestinationType); + } + + if(typeMap.CustomProjection != null) + { + return typeMap.CustomProjection.ReplaceParameters(instanceParameter); + } + return CreateMapExpressionCore(request, instanceParameter, typePairCount, typeMap, letPropertyMaps); + } + + private Expression CreateMapExpressionCore(ExpressionRequest request, Expression instanceParameter, TypePairCount typePairCount, TypeMap typeMap, LetPropertyMaps letPropertyMaps) + { + var bindings = new List(); + var depth = GetDepth(request, typePairCount); + if(typeMap.MaxDepth > 0 && depth >= typeMap.MaxDepth) + { + if(typeMap.Profile.AllowNullDestinationValues) + { + return null; + } + } + else + { + bindings = CreateMemberBindings(request, typeMap, instanceParameter, typePairCount, letPropertyMaps); + } + Expression constructorExpression = DestinationConstructorExpression(typeMap, instanceParameter); + if(instanceParameter is ParameterExpression) + constructorExpression = ((LambdaExpression)constructorExpression).ReplaceParameters(instanceParameter); + var visitor = new NewFinderVisitor(); + visitor.Visit(constructorExpression); + + var expression = MemberInit( + visitor.NewExpression, + bindings.ToArray() + ); + return expression; + } + + private static int GetDepth(ExpressionRequest request, TypePairCount typePairCount) + { + if (typePairCount.TryGetValue(request, out int visitCount)) + { + visitCount = visitCount + 1; + } + typePairCount[request] = visitCount; + return visitCount; + } + + private LambdaExpression DestinationConstructorExpression(TypeMap typeMap, Expression instanceParameter) + { + var ctorExpr = typeMap.ConstructExpression; + if (ctorExpr != null) + { + return ctorExpr; + } + var newExpression = typeMap.ConstructorMap?.CanResolve == true + ? typeMap.ConstructorMap.NewExpression(instanceParameter) + : New(typeMap.DestinationTypeToUse); + + return Lambda(newExpression); + } + + private class NewFinderVisitor : ExpressionVisitor + { + public NewExpression NewExpression { get; private set; } + + protected override Expression VisitNew(NewExpression node) + { + NewExpression = node; + return base.VisitNew(node); + } + } + + private List CreateMemberBindings(ExpressionRequest request, + TypeMap typeMap, + Expression instanceParameter, TypePairCount typePairCount, LetPropertyMaps letPropertyMaps) + { + var bindings = new List(); + + foreach (var propertyMap in typeMap.GetPropertyMaps().Where(pm => pm.CanResolveValue() && ReflectionHelper.CanBeSet(pm.DestinationProperty))) + { + letPropertyMaps.Push(propertyMap); + + CreateMemberBinding(propertyMap); + + letPropertyMaps.Pop(); + } + return bindings; + void CreateMemberBinding(PropertyMap propertyMap) + { + var result = ResolveExpression(propertyMap, request.SourceType, instanceParameter, letPropertyMaps); + + if(propertyMap.ExplicitExpansion && !request.MembersToExpand.Contains(propertyMap.DestinationProperty)) + { + return; + } + var propertyTypeMap = _configurationProvider.ResolveTypeMap(result.Type, + propertyMap.DestinationPropertyType); + var propertyRequest = new ExpressionRequest(result.Type, propertyMap.DestinationPropertyType, request.MembersToExpand, request); + + if(!propertyRequest.AlreadyExists) + { + var binder = Binders.FirstOrDefault(b => b.IsMatch(propertyMap, propertyTypeMap, result)); + + if(binder == null) + { + var message = + $"Unable to create a map expression from {propertyMap.SourceMember?.DeclaringType?.Name}.{propertyMap.SourceMember?.Name} ({result.Type}) to {propertyMap.DestinationProperty.DeclaringType?.Name}.{propertyMap.DestinationProperty.Name} ({propertyMap.DestinationPropertyType})"; + + throw new AutoMapperMappingException(message, null, typeMap.Types, typeMap, propertyMap); + } + + var bindExpression = binder.Build(_configurationProvider, propertyMap, propertyTypeMap, + propertyRequest, result, typePairCount, letPropertyMaps); + + + if (bindExpression != null) + { + var rhs = propertyMap.ValueTransformers + .Concat(typeMap.ValueTransformers) + .Concat(typeMap.Profile.ValueTransformers) + .Where(vt => vt.IsMatch(propertyMap)) + .Aggregate(bindExpression.Expression, (current, vtConfig) => ToType(ReplaceParameters(vtConfig.TransformerExpression, ToType(current, vtConfig.ValueType)), propertyMap.DestinationPropertyType)); + + bindExpression = bindExpression.Update(rhs); + + bindings.Add(bindExpression); + } + } + } + } + + private static ExpressionResolutionResult ResolveExpression(PropertyMap propertyMap, Type currentType, + Expression instanceParameter, LetPropertyMaps letPropertyMaps) + { + var result = new ExpressionResolutionResult(instanceParameter, currentType); + + var matchingExpressionConverter = + ExpressionResultConverters.FirstOrDefault(c => c.CanGetExpressionResolutionResult(result, propertyMap)); + result = matchingExpressionConverter?.GetExpressionResolutionResult(result, propertyMap, letPropertyMaps) + ?? throw new Exception("Can't resolve this to Queryable Expression"); + + if(propertyMap.NullSubstitute != null && result.Type.IsNullableType()) + { + var currentChild = result.ResolutionExpression; + var currentChildType = result.Type; + var nullSubstitute = propertyMap.NullSubstitute; + + var newParameter = result.ResolutionExpression; + var converter = new NullSubstitutionConversionVisitor(newParameter, nullSubstitute); + + currentChild = converter.Visit(currentChild); + currentChildType = currentChildType.GetTypeOfNullable(); + + return new ExpressionResolutionResult(currentChild, currentChildType); + } + + return result; + } + + private class NullSubstitutionConversionVisitor : ExpressionVisitor + { + private readonly Expression _newParameter; + private readonly object _nullSubstitute; + + public NullSubstitutionConversionVisitor(Expression newParameter, object nullSubstitute) + { + _newParameter = newParameter; + _nullSubstitute = nullSubstitute; + } + + protected override Expression VisitMember(MemberExpression node) => node == _newParameter ? NullCheck(node) : node; + + private Expression NullCheck(Expression input) + { + var underlyingType = input.Type.GetTypeOfNullable(); + var nullSubstitute = ToType(Constant(_nullSubstitute), underlyingType); + return Condition(Property(input, "HasValue"), Property(input, "Value"), nullSubstitute, underlyingType); + } + } + + internal class ConstantExpressionReplacementVisitor : ExpressionVisitor + { + private readonly ParameterBag _paramValues; + + public ConstantExpressionReplacementVisitor( + ParameterBag paramValues) => _paramValues = paramValues; + + protected override Expression VisitMember(MemberExpression node) + { + if (!node.Member.DeclaringType.Has()) + { + return base.VisitMember(node); + } + var parameterName = node.Member.Name; + if (!_paramValues.TryGetValue(parameterName, out object parameterValue)) + { + const string vbPrefix = "$VB$Local_"; + if (!parameterName.StartsWith(vbPrefix, StringComparison.Ordinal) || !_paramValues.TryGetValue(parameterName.Substring(vbPrefix.Length), out parameterValue)) + { + return base.VisitMember(node); + } + } + return Convert(Constant(parameterValue), node.Member.GetMemberType()); + } + } + + /// + /// Expression visitor for making member access null-safe. + /// + /// + /// Use to make a query null-safe. + /// copied from NeinLinq (MIT License): https://github.com/axelheer/nein-linq/blob/master/src/NeinLinq/NullsafeQueryRewriter.cs + /// + internal class NullsafeQueryRewriter : ExpressionVisitor + { + static readonly LockingConcurrentDictionary Cache = new LockingConcurrentDictionary(NodeFallback); + + /// + protected override Expression VisitMember(MemberExpression node) => + node?.Expression != null + ? MakeNullsafe(node, node.Expression) + : base.VisitMember(node); + + /// + protected override Expression VisitMethodCall(MethodCallExpression node) => + node?.Object != null + ? MakeNullsafe(node, node.Object) + : base.VisitMethodCall(node); + + private Expression MakeNullsafe(Expression node, Expression value) + { + // cache "fallback expression" for performance reasons + var fallback = Cache.GetOrAdd(node.Type); + + // check value and insert additional coalesce, if fallback is not default + return Condition( + NotEqual(Visit(value), Default(value.Type)), + fallback.NodeType != ExpressionType.Default ? Coalesce(node, fallback) : node, + fallback); + } + + private static Expression NodeFallback(Type type) + { + // default values for generic collections + if (type.GetIsConstructedGenericType() && type.GetTypeInfo().GenericTypeArguments.Length == 1) + { + return GenericCollectionFallback(typeof(List<>), type) + ?? GenericCollectionFallback(typeof(HashSet<>), type) + ?? Default(type); + } + + // default value for arrays + if (type.IsArray) + { + return NewArrayInit(type.GetElementType()); + } + + // default value + return Default(type); + } + + private static Expression GenericCollectionFallback(Type collectionDefinition, Type type) + { + var collectionType = collectionDefinition.MakeGenericType(type.GetTypeInfo().GenericTypeArguments); + + // try if an instance of this collection would suffice + return type.GetTypeInfo().IsAssignableFrom(collectionType.GetTypeInfo()) ? Convert(New(collectionType), type) : null; + } + } + + + public class FirstPassLetPropertyMaps : LetPropertyMaps + { + Stack _currentPath = new Stack(); + List _savedPaths = new List(); + IConfigurationProvider _configurationProvider; + + public FirstPassLetPropertyMaps(IConfigurationProvider configurationProvider) => + _configurationProvider = configurationProvider; + + public override Expression GetSubQueryMarker() + { + var propertyMap = _currentPath.Peek(); + var mapFrom = propertyMap.CustomExpression; + if(!IsSubQuery() || _configurationProvider.ResolveTypeMap(propertyMap.SourceType, propertyMap.DestinationPropertyType) == null) + { + return null; + } + var type = mapFrom.Body.Type; + var marker = Parameter(type, "marker" + propertyMap.DestinationProperty.Name); + _savedPaths.Add(new PropertyPath(_currentPath.Reverse().ToArray(), marker)); + return marker; + bool IsSubQuery() + { + if(!(mapFrom.Body is MethodCallExpression methodCall)) + { + return false; + } + var method = methodCall.Method; + return method.IsStatic && method.DeclaringType == typeof(Enumerable); + } + } + + public override void Push(PropertyMap propertyMap) => _currentPath.Push(propertyMap); + + public override void Pop() => _currentPath.Pop(); + + public override int Count => _savedPaths.Count; + + public override LetPropertyMaps New() => new FirstPassLetPropertyMaps(_configurationProvider); + + public override QueryExpressions GetSubQueryExpression(ExpressionBuilder builder, Expression projection, TypeMap typeMap, ExpressionRequest request, Expression instanceParameter, TypePairCount typePairCount) + { + var letMapInfos = _savedPaths.Select(path => new + { + MapFrom = path.Last.CustomExpression, + MapFromSource = path.PropertyMaps.Take(path.PropertyMaps.Length - 1).Select(pm=>pm.SourceMember).MemberAccesses(instanceParameter), + Property = new PropertyDescription + ( + string.Join("_", path.PropertyMaps.Select(pm => pm.DestinationProperty.Name)), + path.Last.SourceType + ), + Marker = path.Marker + }).ToArray(); + + var letProperties = letMapInfos.Select(m => m.Property); + var letType = ProxyGenerator.GetSimilarType(request.SourceType, letProperties); + var typeMapFactory = new TypeMapFactory(); + TypeMap firstTypeMap; + lock(_configurationProvider) + { + firstTypeMap = typeMapFactory.CreateTypeMap(request.SourceType, letType, typeMap.Profile); + } + var secondParameter = Parameter(letType, "dto"); + + ReplaceSubQueries(); + + var firstExpression = builder.CreateMapExpressionCore(request, instanceParameter, typePairCount, firstTypeMap, LetPropertyMaps.Default); + return new QueryExpressions(firstExpression, projection, secondParameter); + + void ReplaceSubQueries() + { + foreach(var letMapInfo in letMapInfos) + { + var letProperty = letType.GetDeclaredProperty(letMapInfo.Property.Name); + var letPropertyMap = firstTypeMap.FindOrCreatePropertyMapFor(letProperty); + letPropertyMap.CustomExpression = + Lambda(letMapInfo.MapFrom.ReplaceParameters(letMapInfo.MapFromSource), (ParameterExpression)instanceParameter); + projection = projection.Replace(letMapInfo.Marker, MakeMemberAccess(secondParameter, letProperty)); + } + projection = new ReplaceMemberAccessesVisitor(instanceParameter, secondParameter).Visit(projection); + } + } + + class ReplaceMemberAccessesVisitor : ExpressionVisitor + { + Expression _oldObject, _newObject; + + public ReplaceMemberAccessesVisitor(Expression oldObject, Expression newObject) + { + _oldObject = oldObject; + _newObject = newObject; + } + + protected override Expression VisitMember(MemberExpression node) + { + if(node.Expression != _oldObject) + { + return base.VisitMember(node); + } + return MakeMemberAccess(_newObject, _newObject.Type.GetFieldOrProperty(node.Member.Name)); + } + } + } + } + + public class LetPropertyMaps + { + public static readonly LetPropertyMaps Default = new LetPropertyMaps(); + + protected LetPropertyMaps() { } + + public virtual Expression GetSubQueryMarker() => null; + + public virtual void Push(PropertyMap propertyMap) {} + + public virtual void Pop() {} + + public virtual int Count => 0; + + public virtual LetPropertyMaps New() => Default; + + public virtual QueryExpressions GetSubQueryExpression(ExpressionBuilder builder, Expression projection, TypeMap typeMap, ExpressionRequest request, Expression instanceParameter, TypePairCount typePairCount) + => throw new NotImplementedException(); + + public struct PropertyPath + { + public PropertyPath(PropertyMap[] propertyMaps, Expression marker) + { + PropertyMaps = propertyMaps; + Marker = marker; + } + public PropertyMap[] PropertyMaps { get; } + public Expression Marker { get; } + public PropertyMap Last => PropertyMaps[PropertyMaps.Length - 1]; + } + } + + public struct QueryExpressions + { + public QueryExpressions(Expression first, Expression second = null, ParameterExpression secondParameter = null) + { + First = first; + Second = second; + SecondParameter = secondParameter; + } + public Expression First { get; } + public Expression Second { get; } + public ParameterExpression SecondParameter { get; } + } + + public static class ExpressionBuilderExtensions + { + public static Expression> GetMapExpression( + this IExpressionBuilder expressionBuilder) + { + return (Expression>) expressionBuilder.GetMapExpression(typeof(TSource), + typeof(TDestination), new Dictionary(), new MemberInfo[0])[0]; + } + } +} \ No newline at end of file diff --git a/tools/AutoMapper/QueryableExtensions/ExpressionRequest.cs b/tools/AutoMapper/QueryableExtensions/ExpressionRequest.cs new file mode 100644 index 0000000000..ead5cfe217 --- /dev/null +++ b/tools/AutoMapper/QueryableExtensions/ExpressionRequest.cs @@ -0,0 +1,67 @@ + + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; + +namespace AutoMapper.QueryableExtensions +{ + [DebuggerDisplay("{SourceType.Name}, {DestinationType.Name}")] + public class ExpressionRequest : IEquatable + { + public Type SourceType { get; } + + public Type DestinationType { get; } + + public MemberInfo[] MembersToExpand { get; } + + internal ICollection PreviousRequests { get; } + + internal IEnumerable GetPreviousRequestsAndSelf() => PreviousRequests.Concat(new[] { this }); + + internal bool AlreadyExists => PreviousRequests.Contains(this); + + public ExpressionRequest(Type sourceType, Type destinationType, MemberInfo[] membersToExpand, ExpressionRequest parentRequest) + { + SourceType = sourceType; + DestinationType = destinationType; + MembersToExpand = membersToExpand.OrderBy(p => p.Name).ToArray(); + + PreviousRequests = parentRequest == null + ? new HashSet() + : new HashSet(parentRequest.GetPreviousRequestsAndSelf()); + } + + public bool Equals(ExpressionRequest other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return MembersToExpand.SequenceEqual(other.MembersToExpand) && + SourceType == other.SourceType && DestinationType == other.DestinationType; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals((ExpressionRequest) obj); + } + + public override int GetHashCode() + { + var hashCode = HashCodeCombiner.Combine(SourceType, DestinationType); + foreach(var member in MembersToExpand) + { + hashCode = HashCodeCombiner.CombineCodes(hashCode, member.GetHashCode()); + } + return hashCode; + } + + public static bool operator ==(ExpressionRequest left, ExpressionRequest right) => Equals(left, right); + + public static bool operator !=(ExpressionRequest left, ExpressionRequest right) => !Equals(left, right); + } +} \ No newline at end of file diff --git a/tools/AutoMapper/QueryableExtensions/ExpressionResolutionResult.cs b/tools/AutoMapper/QueryableExtensions/ExpressionResolutionResult.cs new file mode 100644 index 0000000000..c51fb9ca44 --- /dev/null +++ b/tools/AutoMapper/QueryableExtensions/ExpressionResolutionResult.cs @@ -0,0 +1,17 @@ +using System; +using System.Linq.Expressions; + +namespace AutoMapper.QueryableExtensions +{ + public class ExpressionResolutionResult + { + public Expression ResolutionExpression { get; } + public Type Type { get; } + + public ExpressionResolutionResult(Expression resolutionExpression, Type type) + { + ResolutionExpression = resolutionExpression; + Type = type; + } + } +} \ No newline at end of file diff --git a/tools/AutoMapper/QueryableExtensions/Extensions.cs b/tools/AutoMapper/QueryableExtensions/Extensions.cs new file mode 100644 index 0000000000..0e4485df03 --- /dev/null +++ b/tools/AutoMapper/QueryableExtensions/Extensions.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using AutoMapper.QueryableExtensions.Impl; + +namespace AutoMapper.QueryableExtensions +{ + /// + /// Queryable extensions for AutoMapper + /// + public static class Extensions + { + /// + /// Maps a queryable expression of a source type to a queryable expression of a destination type + /// + /// Source type + /// Destination type + /// Source queryable + /// Destination queryable + /// Mapped destination queryable + public static IQueryable Map(this IQueryable sourceQuery, IQueryable destQuery) + => sourceQuery.Map(destQuery, Mapper.Configuration); + + /// + /// Maps a queryable expression of a source type to a queryable expression of a destination type + /// + /// Source type + /// Destination type + /// Source queryable + /// Destination queryable + /// + /// Mapped destination queryable + public static IQueryable Map(this IQueryable sourceQuery, IQueryable destQuery, IConfigurationProvider config) + => QueryMapperVisitor.Map(sourceQuery, destQuery, config); + + [Obsolete("Uses static API internally (Mapper.Configuration) - will be dropped in v5")] + public static IQueryDataSourceInjection UseAsDataSource(this IQueryable dataSource) + => dataSource.UseAsDataSource(Mapper.Configuration?.CreateMapper()); + + public static IQueryDataSourceInjection UseAsDataSource(this IQueryable dataSource, IConfigurationProvider config) + => dataSource.UseAsDataSource(config.CreateMapper()); + + public static IQueryDataSourceInjection UseAsDataSource(this IQueryable dataSource, IMapper mapper) + => new QueryDataSourceInjection(dataSource, mapper); + + /// + /// Extension method to project from a queryable using the provided mapping engine + /// + /// Projections are only calculated once and cached + /// Destination type + /// Queryable source + /// Optional parameter object for parameterized mapping expressions + /// Explicit members to expand + /// Expression to project into + public static IQueryable ProjectTo(this IQueryable source, object parameters, params Expression>[] membersToExpand) + => source.ProjectTo(Mapper.Configuration, parameters, membersToExpand); + + /// + /// Extension method to project from a queryable using the provided mapping engine + /// + /// Projections are only calculated once and cached + /// Destination type + /// Queryable source + /// Mapper configuration + /// Optional parameter object for parameterized mapping expressions + /// Explicit members to expand + /// Expression to project into + public static IQueryable ProjectTo(this IQueryable source, IConfigurationProvider configuration, object parameters, params Expression>[] membersToExpand) + => new ProjectionExpression(source, configuration.ExpressionBuilder).To(parameters, membersToExpand); + + /// + /// Extension method to project from a queryable using the provided mapping engine + /// + /// Projections are only calculated once and cached + /// Destination type + /// Queryable source + /// Mapper configuration + /// Explicit members to expand + /// Expression to project into + public static IQueryable ProjectTo( + this IQueryable source, + IConfigurationProvider configuration, + params Expression>[] membersToExpand + ) + => source.ProjectTo(configuration, null, membersToExpand); + + /// + /// Extension method to project from a queryable using the provided mapping engine + /// + /// Projections are only calculated once and cached + /// Destination type + /// Queryable source + /// Explicit members to expand + /// Expression to project into + public static IQueryable ProjectTo( + this IQueryable source, + params Expression>[] membersToExpand + ) + => source.ProjectTo(Mapper.Configuration, null, membersToExpand); + + /// + /// Projects the source type to the destination type given the mapping configuration + /// + /// Destination type to map to + /// Queryable source + /// Optional parameter object for parameterized mapping expressions + /// Explicit members to expand + /// Queryable result, use queryable extension methods to project and execute result + public static IQueryable ProjectTo(this IQueryable source, + IDictionary parameters, params string[] membersToExpand) + => source.ProjectTo(Mapper.Configuration, parameters, membersToExpand); + + /// + /// Projects the source type to the destination type given the mapping configuration + /// + /// Destination type to map to + /// Queryable source + /// Mapper configuration + /// Optional parameter object for parameterized mapping expressions + /// Explicit members to expand + /// Queryable result, use queryable extension methods to project and execute result + public static IQueryable ProjectTo(this IQueryable source, IConfigurationProvider configuration, IDictionary parameters, params string[] membersToExpand) + => new ProjectionExpression(source, configuration.ExpressionBuilder).To(parameters, membersToExpand); + } +} diff --git a/tools/AutoMapper/QueryableExtensions/IExpressionBinder.cs b/tools/AutoMapper/QueryableExtensions/IExpressionBinder.cs new file mode 100644 index 0000000000..0bedbf1bed --- /dev/null +++ b/tools/AutoMapper/QueryableExtensions/IExpressionBinder.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using System.Linq.Expressions; + +namespace AutoMapper.QueryableExtensions +{ + public interface IExpressionBinder + { + bool IsMatch(PropertyMap propertyMap, TypeMap propertyTypeMap, ExpressionResolutionResult result); + + MemberAssignment Build(IConfigurationProvider configuration, PropertyMap propertyMap, TypeMap propertyTypeMap, ExpressionRequest request, ExpressionResolutionResult result, IDictionary typePairCount, LetPropertyMaps letPropertyMaps); + } +} \ No newline at end of file diff --git a/tools/AutoMapper/QueryableExtensions/IExpressionResultConverter.cs b/tools/AutoMapper/QueryableExtensions/IExpressionResultConverter.cs new file mode 100644 index 0000000000..4b2956b14f --- /dev/null +++ b/tools/AutoMapper/QueryableExtensions/IExpressionResultConverter.cs @@ -0,0 +1,10 @@ +namespace AutoMapper.QueryableExtensions +{ + public interface IExpressionResultConverter + { + ExpressionResolutionResult GetExpressionResolutionResult(ExpressionResolutionResult expressionResolutionResult, PropertyMap propertyMap, LetPropertyMaps letPropertyMaps); + ExpressionResolutionResult GetExpressionResolutionResult(ExpressionResolutionResult expressionResolutionResult, ConstructorParameterMap propertyMap); + bool CanGetExpressionResolutionResult(ExpressionResolutionResult expressionResolutionResult, PropertyMap propertyMap); + bool CanGetExpressionResolutionResult(ExpressionResolutionResult expressionResolutionResult, ConstructorParameterMap propertyMap); + } +} \ No newline at end of file diff --git a/tools/AutoMapper/QueryableExtensions/IProjectionExpression.cs b/tools/AutoMapper/QueryableExtensions/IProjectionExpression.cs new file mode 100644 index 0000000000..7b5564c51f --- /dev/null +++ b/tools/AutoMapper/QueryableExtensions/IProjectionExpression.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; + +namespace AutoMapper.QueryableExtensions +{ + /// + /// Continuation to execute projection + /// + public interface IProjectionExpression + { + /// + /// Projects the source type to the destination type given the mapping configuration + /// + /// Destination type to map to + /// Optional parameter object for parameterized mapping expressions + /// Queryable result, use queryable extension methods to project and execute result + IQueryable To(object parameters = null); + + /// + /// Projects the source type to the destination type given the mapping configuration + /// + /// Destination type to map to + /// Optional parameter object for parameterized mapping expressions + /// Queryable result, use queryable extension methods to project and execute result + IQueryable To(IDictionary parameters); + + /// + /// Projects the source type to the destination type given the mapping configuration + /// + /// Destination type to map to + /// Optional parameter object for parameterized mapping expressions + /// Explicit members to expand + /// Queryable result, use queryable extension methods to project and execute result + IQueryable To(object parameters = null, params string[] membersToExpand); + + /// + /// Projects the source type to the destination type given the mapping configuration + /// + /// Destination type to map to + /// Parameters for parameterized mapping expressions + /// Explicit members to expand + /// Queryable result, use queryable extension methods to project and execute result + IQueryable To(IDictionary parameters, params string[] membersToExpand); + + /// + /// Projects the source type to the destination type given the mapping configuration + /// + /// Destination type to map to + /// >Explicit members to expand + /// Optional parameter object for parameterized mapping expressions + /// Queryable result, use queryable extension methods to project and execute result + IQueryable To(object parameters = null, params Expression>[] membersToExpand); + + /// + /// Projects the source type to the destination type given the mapping configuration + /// + /// Destination type to map to + /// >Explicit members to expand + /// Parameters for parameterized mapping expressions + /// Queryable result, use queryable extension methods to project and execute result + IQueryable To(IDictionary parameters, params Expression>[] membersToExpand); + } +} \ No newline at end of file diff --git a/tools/AutoMapper/QueryableExtensions/Impl/AssignableExpressionBinder.cs b/tools/AutoMapper/QueryableExtensions/Impl/AssignableExpressionBinder.cs new file mode 100644 index 0000000000..2d94ab6217 --- /dev/null +++ b/tools/AutoMapper/QueryableExtensions/Impl/AssignableExpressionBinder.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using System.Linq.Expressions; + +namespace AutoMapper.QueryableExtensions.Impl +{ + public class AssignableExpressionBinder : IExpressionBinder + { + public bool IsMatch(PropertyMap propertyMap, TypeMap propertyTypeMap, ExpressionResolutionResult result) + => propertyMap.DestinationPropertyType.IsAssignableFrom(result.Type) && propertyTypeMap == null; + + public MemberAssignment Build(IConfigurationProvider configuration, PropertyMap propertyMap, TypeMap propertyTypeMap, ExpressionRequest request, ExpressionResolutionResult result, IDictionary typePairCount, LetPropertyMaps letPropertyMaps) + => BindAssignableExpression(propertyMap, result); + + private static MemberAssignment BindAssignableExpression(PropertyMap propertyMap, ExpressionResolutionResult result) + => Expression.Bind(propertyMap.DestinationProperty, result.ResolutionExpression); + } +} \ No newline at end of file diff --git a/tools/AutoMapper/QueryableExtensions/Impl/CustomProjectionExpressionBinder.cs b/tools/AutoMapper/QueryableExtensions/Impl/CustomProjectionExpressionBinder.cs new file mode 100644 index 0000000000..4b2c283005 --- /dev/null +++ b/tools/AutoMapper/QueryableExtensions/Impl/CustomProjectionExpressionBinder.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using System.Linq.Expressions; + +namespace AutoMapper.QueryableExtensions.Impl +{ + public class CustomProjectionExpressionBinder : IExpressionBinder + { + public bool IsMatch(PropertyMap propertyMap, TypeMap propertyTypeMap, ExpressionResolutionResult result) + => propertyTypeMap?.CustomProjection != null; + + public MemberAssignment Build(IConfigurationProvider configuration, PropertyMap propertyMap, TypeMap propertyTypeMap, ExpressionRequest request, ExpressionResolutionResult result, IDictionary typePairCount, LetPropertyMaps letPropertyMaps) + => BindCustomProjectionExpression(propertyMap, propertyTypeMap, result); + + private static MemberAssignment BindCustomProjectionExpression(PropertyMap propertyMap, TypeMap propertyTypeMap, ExpressionResolutionResult result) + => Expression.Bind(propertyMap.DestinationProperty, propertyTypeMap.CustomProjection.ReplaceParameters(result.ResolutionExpression)); + } +} \ No newline at end of file diff --git a/tools/AutoMapper/QueryableExtensions/Impl/EnumerableExpressionBinder.cs b/tools/AutoMapper/QueryableExtensions/Impl/EnumerableExpressionBinder.cs new file mode 100644 index 0000000000..109912cea5 --- /dev/null +++ b/tools/AutoMapper/QueryableExtensions/Impl/EnumerableExpressionBinder.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using AutoMapper.Configuration; +using AutoMapper.Mappers; +using AutoMapper.Mappers.Internal; + +namespace AutoMapper.QueryableExtensions.Impl +{ + public class EnumerableExpressionBinder : IExpressionBinder + { + public bool IsMatch(PropertyMap propertyMap, TypeMap propertyTypeMap, ExpressionResolutionResult result) => + propertyMap.DestinationPropertyType.IsEnumerableType() && propertyMap.SourceType.IsEnumerableType(); + + public MemberAssignment Build(IConfigurationProvider configuration, PropertyMap propertyMap, TypeMap propertyTypeMap, ExpressionRequest request, ExpressionResolutionResult result, IDictionary typePairCount, LetPropertyMaps letPropertyMaps) + => BindEnumerableExpression(configuration, propertyMap, request, result, typePairCount, letPropertyMaps); + + private static MemberAssignment BindEnumerableExpression(IConfigurationProvider configuration, PropertyMap propertyMap, ExpressionRequest request, ExpressionResolutionResult result, IDictionary typePairCount, LetPropertyMaps letPropertyMaps) + { + var destinationListType = ElementTypeHelper.GetElementType(propertyMap.DestinationPropertyType); + var sourceListType = ElementTypeHelper.GetElementType(propertyMap.SourceType); + var expression = result.ResolutionExpression; + + if (sourceListType != destinationListType) + { + var listTypePair = new ExpressionRequest(sourceListType, destinationListType, request.MembersToExpand, request); + var transformedExpressions = configuration.ExpressionBuilder.CreateMapExpression(listTypePair, typePairCount, letPropertyMaps.New()); + if(transformedExpressions == null) + { + return null; + } + expression = transformedExpressions.Aggregate(expression, (source, lambda) => Select(source, lambda)); + } + + expression = Expression.Call(typeof(Enumerable), propertyMap.DestinationPropertyType.IsArray ? "ToArray" : "ToList", new[] { destinationListType }, expression); + + return Expression.Bind(propertyMap.DestinationProperty, expression); + } + + private static Expression Select(Expression source, LambdaExpression lambda) + { + return Expression.Call(typeof(Enumerable), "Select", new[] { lambda.Parameters[0].Type, lambda.ReturnType }, source, lambda); + } + } +} \ No newline at end of file diff --git a/tools/AutoMapper/QueryableExtensions/Impl/ISourceInjectedQueryable.cs b/tools/AutoMapper/QueryableExtensions/Impl/ISourceInjectedQueryable.cs new file mode 100644 index 0000000000..d21a9dc2a1 --- /dev/null +++ b/tools/AutoMapper/QueryableExtensions/Impl/ISourceInjectedQueryable.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace AutoMapper.QueryableExtensions.Impl +{ + public interface ISourceInjectedQueryable : IQueryable + { + /// + /// Called when [enumerated]. + /// + /// The enumeration handler. + IQueryable OnEnumerated(Action> enumerationHandler); + + /// + /// Casts itself to IQueryable<T> so no explicit casting is necessary + /// + /// + IQueryable AsQueryable(); + } +} \ No newline at end of file diff --git a/tools/AutoMapper/QueryableExtensions/Impl/MappedTypeExpressionBinder.cs b/tools/AutoMapper/QueryableExtensions/Impl/MappedTypeExpressionBinder.cs new file mode 100644 index 0000000000..78fa007789 --- /dev/null +++ b/tools/AutoMapper/QueryableExtensions/Impl/MappedTypeExpressionBinder.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using System.Linq.Expressions; + +namespace AutoMapper.QueryableExtensions.Impl +{ + public class MappedTypeExpressionBinder : IExpressionBinder + { + public bool IsMatch(PropertyMap propertyMap, TypeMap propertyTypeMap, ExpressionResolutionResult result) => + propertyTypeMap != null && propertyTypeMap.CustomProjection == null; + + public MemberAssignment Build(IConfigurationProvider configuration, PropertyMap propertyMap, TypeMap propertyTypeMap, ExpressionRequest request, ExpressionResolutionResult result, IDictionary typePairCount, LetPropertyMaps letPropertyMaps) + => BindMappedTypeExpression(configuration, propertyMap, request, result, typePairCount, letPropertyMaps); + + private static MemberAssignment BindMappedTypeExpression(IConfigurationProvider configuration, PropertyMap propertyMap, ExpressionRequest request, ExpressionResolutionResult result, IDictionary typePairCount, LetPropertyMaps letPropertyMaps) + { + var transformedExpression = configuration.ExpressionBuilder.CreateMapExpression(request, result.ResolutionExpression, typePairCount, letPropertyMaps); + if(transformedExpression == null) + { + return null; + } + // Handles null source property so it will not create an object with possible non-nullable propeerties + // which would result in an exception. + if (propertyMap.TypeMap.Profile.AllowNullDestinationValues && !propertyMap.AllowNull) + { + var expressionNull = Expression.Constant(null, propertyMap.DestinationPropertyType); + transformedExpression = + Expression.Condition(Expression.NotEqual(result.ResolutionExpression, Expression.Constant(null)), + transformedExpression, expressionNull); + } + + return Expression.Bind(propertyMap.DestinationProperty, transformedExpression); + } + } +} \ No newline at end of file diff --git a/tools/AutoMapper/QueryableExtensions/Impl/MemberAccessQueryMapperVisitor.cs b/tools/AutoMapper/QueryableExtensions/Impl/MemberAccessQueryMapperVisitor.cs new file mode 100644 index 0000000000..b2b66bcc45 --- /dev/null +++ b/tools/AutoMapper/QueryableExtensions/Impl/MemberAccessQueryMapperVisitor.cs @@ -0,0 +1,31 @@ +using System.Linq.Expressions; + +namespace AutoMapper.QueryableExtensions.Impl +{ + public class MemberAccessQueryMapperVisitor : ExpressionVisitor + { + private readonly ExpressionVisitor _rootVisitor; + private readonly IConfigurationProvider _config; + + public MemberAccessQueryMapperVisitor(ExpressionVisitor rootVisitor, IConfigurationProvider config) + { + _rootVisitor = rootVisitor; + _config = config; + } + + protected override Expression VisitMember(MemberExpression node) + { + var parentExpr = _rootVisitor.Visit(node.Expression); + if (parentExpr != null) + { + var propertyMap = _config.GetPropertyMap(node.Member, parentExpr.Type); + + var newMember = Expression.MakeMemberAccess(parentExpr, propertyMap.DestinationProperty); + + return newMember; + } + return node; + } + + } +} \ No newline at end of file diff --git a/tools/AutoMapper/QueryableExtensions/Impl/MemberGetterExpressionResultConverter.cs b/tools/AutoMapper/QueryableExtensions/Impl/MemberGetterExpressionResultConverter.cs new file mode 100644 index 0000000000..696ac15cd2 --- /dev/null +++ b/tools/AutoMapper/QueryableExtensions/Impl/MemberGetterExpressionResultConverter.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; + +namespace AutoMapper.QueryableExtensions.Impl +{ + public class MemberGetterExpressionResultConverter : IExpressionResultConverter + { + public ExpressionResolutionResult GetExpressionResolutionResult(ExpressionResolutionResult expressionResolutionResult, PropertyMap propertyMap, LetPropertyMaps letPropertyMaps) + => ExpressionResolutionResult(expressionResolutionResult, propertyMap.SourceMembers); + + public ExpressionResolutionResult GetExpressionResolutionResult(ExpressionResolutionResult expressionResolutionResult, + ConstructorParameterMap propertyMap) + => ExpressionResolutionResult(expressionResolutionResult, propertyMap.SourceMembers); + + private static ExpressionResolutionResult ExpressionResolutionResult( + ExpressionResolutionResult expressionResolutionResult, IEnumerable sourceMembers) + => sourceMembers.Aggregate(expressionResolutionResult, ExpressionResolutionResult); + + private static ExpressionResolutionResult ExpressionResolutionResult( + ExpressionResolutionResult expressionResolutionResult, MemberInfo getter) + { + var member = Expression.MakeMemberAccess(expressionResolutionResult.ResolutionExpression, getter); + return new ExpressionResolutionResult(member, member.Type); + } + + public bool CanGetExpressionResolutionResult(ExpressionResolutionResult expressionResolutionResult, PropertyMap propertyMap) + => propertyMap.SourceMembers.Any(); + + public bool CanGetExpressionResolutionResult(ExpressionResolutionResult expressionResolutionResult, ConstructorParameterMap propertyMap) + => propertyMap.SourceMembers.Any(); + } +} \ No newline at end of file diff --git a/tools/AutoMapper/QueryableExtensions/Impl/MemberResolverExpressionResultConverter.cs b/tools/AutoMapper/QueryableExtensions/Impl/MemberResolverExpressionResultConverter.cs new file mode 100644 index 0000000000..f5d832808c --- /dev/null +++ b/tools/AutoMapper/QueryableExtensions/Impl/MemberResolverExpressionResultConverter.cs @@ -0,0 +1,36 @@ +using System.Linq.Expressions; + +namespace AutoMapper.QueryableExtensions.Impl +{ + public class MemberResolverExpressionResultConverter : IExpressionResultConverter + { + public ExpressionResolutionResult GetExpressionResolutionResult( + ExpressionResolutionResult expressionResolutionResult, PropertyMap propertyMap, LetPropertyMaps letPropertyMaps) + { + Expression subQueryMarker; + if((subQueryMarker = letPropertyMaps.GetSubQueryMarker()) != null) + { + return new ExpressionResolutionResult(subQueryMarker, subQueryMarker.Type); + } + return ExpressionResolutionResult(expressionResolutionResult, propertyMap.CustomExpression); + } + + private static ExpressionResolutionResult ExpressionResolutionResult( + ExpressionResolutionResult expressionResolutionResult, LambdaExpression lambdaExpression) + { + var currentChild = lambdaExpression.ReplaceParameters(expressionResolutionResult.ResolutionExpression); + var currentChildType = currentChild.Type; + + return new ExpressionResolutionResult(currentChild, currentChildType); + } + + public ExpressionResolutionResult GetExpressionResolutionResult(ExpressionResolutionResult expressionResolutionResult, + ConstructorParameterMap propertyMap) => ExpressionResolutionResult(expressionResolutionResult, null); + + public bool CanGetExpressionResolutionResult(ExpressionResolutionResult expressionResolutionResult, + PropertyMap propertyMap) => propertyMap.CustomExpression != null; + + public bool CanGetExpressionResolutionResult(ExpressionResolutionResult expressionResolutionResult, + ConstructorParameterMap propertyMap) => false; + } +} \ No newline at end of file diff --git a/tools/AutoMapper/QueryableExtensions/Impl/NullableDestinationExpressionBinder.cs b/tools/AutoMapper/QueryableExtensions/Impl/NullableDestinationExpressionBinder.cs new file mode 100644 index 0000000000..fe4a1d7b12 --- /dev/null +++ b/tools/AutoMapper/QueryableExtensions/Impl/NullableDestinationExpressionBinder.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using System.Linq.Expressions; +using static System.Linq.Expressions.Expression; +using AutoMapper.Configuration; + +namespace AutoMapper.QueryableExtensions.Impl +{ + public class NullableDestinationExpressionBinder : IExpressionBinder + { + public bool IsMatch(PropertyMap propertyMap, TypeMap propertyTypeMap, ExpressionResolutionResult result) => + propertyMap.DestinationPropertyType.IsNullableType() + && !result.Type.IsNullableType(); + + public MemberAssignment Build(IConfigurationProvider configuration, PropertyMap propertyMap, TypeMap propertyTypeMap, ExpressionRequest request, ExpressionResolutionResult result, IDictionary typePairCount, LetPropertyMaps letPropertyMaps) + => BindNullableExpression(propertyMap, result); + + private static MemberAssignment BindNullableExpression(PropertyMap propertyMap, + ExpressionResolutionResult result) + { + if (result.ResolutionExpression.NodeType == ExpressionType.MemberAccess) + { + var memberExpr = (MemberExpression) result.ResolutionExpression; + if (memberExpr.Expression != null && memberExpr.Expression.NodeType == ExpressionType.MemberAccess) + { + var destType = propertyMap.DestinationPropertyType; + var parentExpr = memberExpr.Expression; + Expression expressionToBind = Convert(memberExpr, destType); + var nullExpression = Convert(Constant(null), destType); + while (parentExpr.NodeType != ExpressionType.Parameter) + { + memberExpr = (MemberExpression) memberExpr.Expression; + parentExpr = memberExpr.Expression; + expressionToBind = Condition( + Equal(memberExpr, Constant(null)), + nullExpression, + expressionToBind + ); + } + + return Bind(propertyMap.DestinationProperty, expressionToBind); + } + } + + return Bind(propertyMap.DestinationProperty, + Convert(result.ResolutionExpression, propertyMap.DestinationPropertyType)); + } + } +} \ No newline at end of file diff --git a/tools/AutoMapper/QueryableExtensions/Impl/NullableSourceExpressionBinder.cs b/tools/AutoMapper/QueryableExtensions/Impl/NullableSourceExpressionBinder.cs new file mode 100644 index 0000000000..d8c1ccb2b3 --- /dev/null +++ b/tools/AutoMapper/QueryableExtensions/Impl/NullableSourceExpressionBinder.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using AutoMapper.Configuration; + +namespace AutoMapper.QueryableExtensions +{ + using static Expression; + + internal class NullableSourceExpressionBinder : IExpressionBinder + { + public MemberAssignment Build(IConfigurationProvider configuration, PropertyMap propertyMap, TypeMap propertyTypeMap, ExpressionRequest request, ExpressionResolutionResult result, IDictionary typePairCount, LetPropertyMaps letPropertyMaps) + { + var defaultDestination = Activator.CreateInstance(propertyMap.DestinationPropertyType); + return Bind(propertyMap.DestinationProperty, Coalesce(result.ResolutionExpression, Constant(defaultDestination))); + } + + public bool IsMatch(PropertyMap propertyMap, TypeMap propertyTypeMap, ExpressionResolutionResult result) => + result.Type.IsNullableType() && !propertyMap.DestinationPropertyType.IsNullableType() && propertyMap.DestinationPropertyType.IsValueType(); + } +} \ No newline at end of file diff --git a/tools/AutoMapper/QueryableExtensions/Impl/QueryDataSourceInjection.cs b/tools/AutoMapper/QueryableExtensions/Impl/QueryDataSourceInjection.cs new file mode 100644 index 0000000000..4dad39a945 --- /dev/null +++ b/tools/AutoMapper/QueryableExtensions/Impl/QueryDataSourceInjection.cs @@ -0,0 +1,164 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using AutoMapper.Configuration; +using AutoMapper.Internal; + +namespace AutoMapper.QueryableExtensions.Impl +{ + using MemberPaths = IEnumerable>; + using IObjectDictionary = IDictionary; + + public interface IQueryDataSourceInjection + { + /// + /// Creates the mapped query with an optional inspector + /// + /// The type of the destination. + /// + ISourceInjectedQueryable For(); + ISourceInjectedQueryable For(object parameters, params Expression>[] membersToExpand); + ISourceInjectedQueryable For(params Expression>[] membersToExpand); + ISourceInjectedQueryable For(IObjectDictionary parameters, params string[] membersToExpand); + + IQueryDataSourceInjection UsingInspector(SourceInjectedQueryInspector inspector); + + /// + /// ExpressionVisitors called before MappingVisitor itself is executed + /// + /// The visitors. + /// + IQueryDataSourceInjection BeforeProjection(params ExpressionVisitor[] visitors); + + /// + /// ExpressionVisitors called after the MappingVisitor itself is executed + /// + /// The visitors. + /// + IQueryDataSourceInjection AfterProjection(params ExpressionVisitor[] visitors); + + /// + /// Allows specifying a handler that will be called when the underlying QueryProvider encounters an exception. + /// This is especially useful if you expose the resulting IQueryable in e.g. a WebApi controller where + /// you do not call "ToList" yourself and therefore cannot catch exceptions + /// + /// The exception handler. + /// + IQueryDataSourceInjection OnError(Action exceptionHandler); + } + + public class QueryDataSourceInjection : IQueryDataSourceInjection + { + private readonly IQueryable _dataSource; + private readonly IMapper _mapper; + private readonly List _beforeMappingVisitors = new List(); + private readonly List _afterMappingVisitors = new List(); + private readonly ExpressionVisitor _sourceExpressionTracer = null; + private readonly ExpressionVisitor _destinationExpressionTracer = null; + private Action _exceptionHandler = x => { }; + private MemberPaths _membersToExpand; + private IObjectDictionary _parameters; + private SourceInjectedQueryInspector _inspector; + + public QueryDataSourceInjection(IQueryable dataSource, IMapper mapper) + { + _dataSource = dataSource; + _mapper = mapper; + } + + public ISourceInjectedQueryable For() => CreateQueryable(); + + public ISourceInjectedQueryable For(object parameters, params Expression>[] membersToExpand) + { + _parameters = GetParameters(parameters); + _membersToExpand = ProjectionExpression.GetMemberPaths(membersToExpand); + return CreateQueryable(); + } + + public ISourceInjectedQueryable For(params Expression>[] membersToExpand) + { + _membersToExpand = ProjectionExpression.GetMemberPaths(membersToExpand); + return CreateQueryable(); + } + + public ISourceInjectedQueryable For(IObjectDictionary parameters, params string[] membersToExpand) + { + _parameters = parameters; + _membersToExpand = ProjectionExpression.GetMemberPaths(typeof(TDestination), membersToExpand); + return CreateQueryable(); + } + + public IQueryDataSourceInjection UsingInspector(SourceInjectedQueryInspector inspector) + { + _inspector = inspector; + + if (_sourceExpressionTracer != null) + _beforeMappingVisitors.Insert(0, _sourceExpressionTracer); + if (_destinationExpressionTracer != null) + _afterMappingVisitors.Add(_destinationExpressionTracer); + + return this; + } + + /// + /// ExpressionVisitors called before MappingVisitor itself is executed + /// + /// The visitors. + /// + public IQueryDataSourceInjection BeforeProjection(params ExpressionVisitor[] visitors) + { + foreach (var visitor in visitors.Where(visitor => !_beforeMappingVisitors.Contains(visitor))) + { + _beforeMappingVisitors.Add(visitor); + } + return this; + } + + /// + /// ExpressionVisitors called after the MappingVisitor itself is executed + /// + /// The visitors. + /// + public IQueryDataSourceInjection AfterProjection(params ExpressionVisitor[] visitors) + { + foreach (var visitor in visitors.Where(visitor => !_afterMappingVisitors.Contains(visitor))) + { + _afterMappingVisitors.Add(visitor); + } + return this; + } + + /// + /// Allows specifying a handler that will be called when the underlying QueryProvider encounters an exception. + /// This is especially useful if you expose the resulting IQueryable in e.g. a WebApi controller where + /// you do not call "ToList" yourself and therefore cannot catch exceptions + /// + /// The exception handler. + /// + public IQueryDataSourceInjection OnError(Action exceptionHandler) + { + _exceptionHandler = exceptionHandler; + return this; + } + + private ISourceInjectedQueryable CreateQueryable() => + new SourceSourceInjectedQuery(_dataSource, + new TDestination[0].AsQueryable(), + _mapper, + _beforeMappingVisitors, + _afterMappingVisitors, + _exceptionHandler, + _parameters, + _membersToExpand, + _inspector); + + private static IObjectDictionary GetParameters(object parameters) + { + return (parameters ?? new object()).GetType() + .GetDeclaredProperties() + .ToDictionary(pi => pi.Name, pi => pi.GetValue(parameters, null)); + } + } +} \ No newline at end of file diff --git a/tools/AutoMapper/QueryableExtensions/Impl/QueryMapperHelper.cs b/tools/AutoMapper/QueryableExtensions/Impl/QueryMapperHelper.cs new file mode 100644 index 0000000000..aec2239e50 --- /dev/null +++ b/tools/AutoMapper/QueryableExtensions/Impl/QueryMapperHelper.cs @@ -0,0 +1,48 @@ +using System; +using System.Linq; +using System.Reflection; + +namespace AutoMapper.QueryableExtensions.Impl +{ + public static class QueryMapperHelper + { + public static PropertyMap GetPropertyMap(this IConfigurationProvider config, MemberInfo sourceMemberInfo, Type destinationMemberType) + { + var typeMap = config.CheckIfMapExists(sourceMemberInfo.DeclaringType, destinationMemberType); + + var propertyMap = typeMap.GetPropertyMaps() + .FirstOrDefault(pm => pm.CanResolveValue() && + pm.SourceMember != null && pm.SourceMember.Name == sourceMemberInfo.Name); + + if (propertyMap == null) + throw PropertyConfigurationException(typeMap, sourceMemberInfo.Name); + + return propertyMap; + } + + public static PropertyMap GetPropertyMapByDestinationProperty(this TypeMap typeMap, string destinationPropertyName) + { + var propertyMap = typeMap.GetPropertyMaps().SingleOrDefault(item => item.DestinationProperty.Name == destinationPropertyName); + if (propertyMap == null) + throw PropertyConfigurationException(typeMap, destinationPropertyName); + + return propertyMap; + } + + public static TypeMap CheckIfMapExists(this IConfigurationProvider config, Type sourceType, Type destinationType) + { + var typeMap = config.ResolveTypeMap(sourceType, destinationType); + if(typeMap == null) + { + throw MissingMapException(sourceType, destinationType); + } + return typeMap; + } + + public static Exception PropertyConfigurationException(TypeMap typeMap, params string[] unmappedPropertyNames) + => new AutoMapperConfigurationException(new[] { new AutoMapperConfigurationException.TypeMapConfigErrors(typeMap, unmappedPropertyNames, true) }); + + public static Exception MissingMapException(Type sourceType, Type destinationType) + => new InvalidOperationException($"Missing map from {sourceType} to {destinationType}. Create using Mapper.CreateMap<{sourceType.Name}, {destinationType.Name}>."); + } +} diff --git a/tools/AutoMapper/QueryableExtensions/Impl/QueryMapperVisitor.cs b/tools/AutoMapper/QueryableExtensions/Impl/QueryMapperVisitor.cs new file mode 100644 index 0000000000..e5b5d22e8e --- /dev/null +++ b/tools/AutoMapper/QueryableExtensions/Impl/QueryMapperVisitor.cs @@ -0,0 +1,165 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; + +namespace AutoMapper.QueryableExtensions.Impl +{ + public class QueryMapperVisitor : ExpressionVisitor + { + private readonly IQueryable _destQuery; + private readonly ParameterExpression _instanceParameter; + private readonly Type _sourceType; + private readonly Type _destinationType; + private readonly Stack _tree = new Stack(); + private readonly Stack _newTree = new Stack(); + private readonly MemberAccessQueryMapperVisitor _memberVisitor; + + + internal QueryMapperVisitor(Type sourceType, Type destinationType, IQueryable destQuery, IConfigurationProvider config) + { + _sourceType = sourceType; + _destinationType = destinationType; + _destQuery = destQuery; + _instanceParameter = Expression.Parameter(destinationType, "dto"); + _memberVisitor = new MemberAccessQueryMapperVisitor(this, config); + } + + public static IQueryable Map(IQueryable sourceQuery, IQueryable destQuery, IConfigurationProvider config) + { + var visitor = new QueryMapperVisitor(typeof(TSource), typeof(TDestination), destQuery, config); + var expr = visitor.Visit(sourceQuery.Expression); + + var newDestQuery = destQuery.Provider.CreateQuery(expr); + return newDestQuery; + } + + public override Expression Visit(Expression node) + { + _tree.Push(node); + // OData Client DataServiceQuery initial expression node type + if (node != null && (int)node.NodeType == 10000) + { + return node; + } + var newNode = base.Visit(node); + _newTree.Push(newNode); + return newNode; + } + + protected override Expression VisitParameter(ParameterExpression node) => _instanceParameter; + + protected override Expression VisitConstant(ConstantExpression node) + { + // It is data source of queryable object instance + if (node.Value is IQueryable query && query.ElementType == _sourceType) + return _destQuery.Expression; + return node; + } + + protected override Expression VisitBinary(BinaryExpression node) + { + var left = Visit(node.Left); + var right = Visit(node.Right); + + // Convert Right expression value to left expr type + // It is needed when PropertyMap is changing type of property + if (left.Type != right.Type && right.NodeType == ExpressionType.Constant) + { + var value = Convert.ChangeType(((ConstantExpression)right).Value, left.Type, CultureInfo.CurrentCulture); + + right = Expression.Constant(value, left.Type); + } + + return Expression.MakeBinary(node.NodeType, left, right); + } + + protected override Expression VisitLambda(Expression node) + { + var newBody = Visit(node.Body); + var newParams = node.Parameters.Select(p => (ParameterExpression)Visit(p)); + + var delegateType = ChangeLambdaArgTypeFormSourceToDest(node.Type, newBody.Type); + + var newLambda = Expression.Lambda(delegateType, newBody, newParams); + return newLambda; + } + + protected override Expression VisitMethodCall(MethodCallExpression node) + { + if (node.Method.Name == "OrderBy" || node.Method.Name == "OrderByDescending" || + node.Method.Name == "ThenBy" || node.Method.Name == "ThenByDescending") + { + return VisitOrderBy(node); + } + + var args = node.Arguments.Select(Visit).ToList(); + var newObject = Visit(node.Object); + var method = ChangeMethodArgTypeFormSourceToDest(node.Method); + + var newMethodCall = Expression.Call(newObject, method, args); + return newMethodCall; + } + + private Expression VisitOrderBy(MethodCallExpression node) + { + var query = node.Arguments[0]; + var orderByExpr = node.Arguments[1]; + + var newQuery = Visit(query); + var newOrderByExpr = Visit(orderByExpr); + var newObject = Visit(node.Object); + + + + var genericMethod = node.Method.GetGenericMethodDefinition(); + var methodArgs = node.Method.GetGenericArguments(); + methodArgs[0] = methodArgs[0].ReplaceItemType(_sourceType, _destinationType); + + // for typical orderby expression, a unaryexpression is used that contains a + // func which in turn defines the type of the field that has to be used for ordering/sorting + if (newOrderByExpr is UnaryExpression unary && unary.Operand.Type.IsGenericType()) + { + methodArgs[1] = methodArgs[1].ReplaceItemType(typeof(string), unary.Operand.Type.GetGenericArguments().Last()); + } + else + { + methodArgs[1] = methodArgs[1].ReplaceItemType(typeof(string), typeof(int)); + } + var orderByMethod = genericMethod.MakeGenericMethod(methodArgs); + + return Expression.Call(newObject, orderByMethod, newQuery, newOrderByExpr); + } + + protected override Expression VisitMember(MemberExpression node) => _memberVisitor.Visit(node); + + private MethodInfo ChangeMethodArgTypeFormSourceToDest(MethodInfo mi) + { + if (!mi.IsGenericMethod) + return mi; + var genericMethod = mi.GetGenericMethodDefinition(); + var methodArgs = mi.GetGenericArguments(); + methodArgs = methodArgs.Select(t => t.ReplaceItemType(_sourceType, _destinationType)).ToArray(); + return genericMethod.MakeGenericMethod(methodArgs); + + } + + private Type ChangeLambdaArgTypeFormSourceToDest(Type lambdaType, Type returnType) + { + if (lambdaType.IsGenericType()) + { + var genArgs = lambdaType.GetTypeInfo().GenericTypeArguments; + var newGenArgs = genArgs.Select(t => t.ReplaceItemType(_sourceType, _destinationType)).ToArray(); + var genericTypeDef = lambdaType.GetGenericTypeDefinition(); + if (genericTypeDef.FullName.StartsWith("System.Func")) + { + newGenArgs[newGenArgs.Length - 1] = returnType; + } + return genericTypeDef.MakeGenericType(newGenArgs); + } + return lambdaType; + } + } +} diff --git a/tools/AutoMapper/QueryableExtensions/Impl/SourceInjectedQuery.cs b/tools/AutoMapper/QueryableExtensions/Impl/SourceInjectedQuery.cs new file mode 100644 index 0000000000..332144691f --- /dev/null +++ b/tools/AutoMapper/QueryableExtensions/Impl/SourceInjectedQuery.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; + +namespace AutoMapper.QueryableExtensions.Impl +{ + using IObjectDictionary = IDictionary; + using MemberPaths = IEnumerable>; + + public class SourceSourceInjectedQuery : IOrderedQueryable, ISourceInjectedQueryable + { + private readonly Action _exceptionHandler; + + public SourceSourceInjectedQuery(IQueryable dataSource, + IQueryable destQuery, + IMapper mapper, + IEnumerable beforeVisitors, + IEnumerable afterVisitors, + Action exceptionHandler, + IObjectDictionary parameters, + MemberPaths membersToExpand, + SourceInjectedQueryInspector inspector) + { + Parameters = parameters; + EnumerationHandler = (x => { }); + Expression = destQuery.Expression; + ElementType = typeof(TDestination); + Provider = new SourceInjectedQueryProvider(mapper, dataSource, destQuery, beforeVisitors, afterVisitors, exceptionHandler, parameters, membersToExpand) + { + Inspector = inspector ?? new SourceInjectedQueryInspector() + }; + _exceptionHandler = exceptionHandler ?? (x => { }); + } + + internal SourceSourceInjectedQuery(IQueryProvider provider, Expression expression, Action> enumerationHandler, Action exceptionHandler) + { + _exceptionHandler = exceptionHandler ?? (x => { }); + Provider = provider; + Expression = expression; + EnumerationHandler = enumerationHandler ?? (x => { }); + ElementType = typeof(TDestination); + } + + public IQueryable OnEnumerated(Action> enumerationHandler) + { + EnumerationHandler = enumerationHandler ?? (x => { }); + ((SourceInjectedQueryProvider)Provider).EnumerationHandler = EnumerationHandler; + return this; + } + + public IQueryable AsQueryable() => this; + + internal Action> EnumerationHandler { get; set; } + internal IObjectDictionary Parameters { get; set; } + + public IEnumerator GetEnumerator() + { + try + { + var results = Provider.Execute>(Expression).Cast().ToArray(); + EnumerationHandler(results); + return results.Cast().GetEnumerator(); + } + catch (Exception x) + { + _exceptionHandler(x); + throw; + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public Type ElementType { get; } + public Expression Expression { get; } + public IQueryProvider Provider { get; } + } +} diff --git a/tools/AutoMapper/QueryableExtensions/Impl/SourceInjectedQueryInspector.cs b/tools/AutoMapper/QueryableExtensions/Impl/SourceInjectedQueryInspector.cs new file mode 100644 index 0000000000..4a7d5ea835 --- /dev/null +++ b/tools/AutoMapper/QueryableExtensions/Impl/SourceInjectedQueryInspector.cs @@ -0,0 +1,19 @@ +using System; +using System.Linq.Expressions; + +namespace AutoMapper.QueryableExtensions.Impl +{ + public class SourceInjectedQueryInspector + { + public SourceInjectedQueryInspector() + { + SourceResult = (e,o) => { }; + DestResult = o => { }; + StartQueryExecuteInterceptor = (t, e) => { }; + } + public Action SourceResult { get; set; } + public Action DestResult { get; set; } + public Action StartQueryExecuteInterceptor { get; set; } + + } +} \ No newline at end of file diff --git a/tools/AutoMapper/QueryableExtensions/Impl/SourceInjectedQueryProvider.cs b/tools/AutoMapper/QueryableExtensions/Impl/SourceInjectedQueryProvider.cs new file mode 100644 index 0000000000..9907f502b2 --- /dev/null +++ b/tools/AutoMapper/QueryableExtensions/Impl/SourceInjectedQueryProvider.cs @@ -0,0 +1,416 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using AutoMapper.Configuration; +using AutoMapper.Mappers; +using AutoMapper.Mappers.Internal; + +namespace AutoMapper.QueryableExtensions.Impl +{ + using MemberPaths = IEnumerable>; + + public class SourceInjectedQueryProvider : IQueryProvider + { + private readonly IMapper _mapper; + private readonly IQueryable _dataSource; + private readonly IQueryable _destQuery; + private readonly IEnumerable _beforeVisitors; + private readonly IEnumerable _afterVisitors; + private readonly IDictionary _parameters; + private readonly MemberPaths _membersToExpand; + private readonly Action _exceptionHandler; + + public SourceInjectedQueryProvider(IMapper mapper, + IQueryable dataSource, IQueryable destQuery, + IEnumerable beforeVisitors, + IEnumerable afterVisitors, + Action exceptionHandler, + IDictionary parameters, + MemberPaths membersToExpand) + { + _mapper = mapper; + _dataSource = dataSource; + _destQuery = destQuery; + _beforeVisitors = beforeVisitors; + _afterVisitors = afterVisitors; + _parameters = parameters ?? new Dictionary(); + _membersToExpand = membersToExpand ?? Enumerable.Empty>(); + _exceptionHandler = exceptionHandler ?? (x => { }); + } + + public SourceInjectedQueryInspector Inspector { get; set; } + internal Action> EnumerationHandler { get; set; } + + public IQueryable CreateQuery(Expression expression) + => new SourceSourceInjectedQuery(this, expression, EnumerationHandler, _exceptionHandler); + + public IQueryable CreateQuery(Expression expression) + => new SourceSourceInjectedQuery(this, expression, EnumerationHandler, _exceptionHandler); + + public object Execute(Expression expression) + { + try + { + Inspector.StartQueryExecuteInterceptor(null, expression); + + var sourceExpression = ConvertDestinationExpressionToSourceExpression(expression); + var sourceResult = InvokeSourceQuery(null, sourceExpression); + + Inspector.SourceResult(sourceExpression, sourceResult); + return sourceResult; + } + catch (Exception x) + { + _exceptionHandler(x); + throw; + } + } + + public TResult Execute(Expression expression) + { + try + { + var resultType = typeof(TResult); + Inspector.StartQueryExecuteInterceptor(resultType, expression); + + var sourceExpression = ConvertDestinationExpressionToSourceExpression(expression); + + var destResultType = typeof(TResult); + var sourceResultType = CreateSourceResultType(destResultType); + + object destResult = null; + + // case #1: query is a projection from complex Source to complex Destination + // example: users.UseAsDataSource().For().Where(x => x.Age > 20).ToList() + if (IsProjection(resultType)) + { + // in case of a projection, we need an IQueryable + var sourceResult = _dataSource.Provider.CreateQuery(sourceExpression); + Inspector.SourceResult(sourceExpression, sourceResult); + + destResult = new ProjectionExpression((IQueryable)sourceResult, _mapper.ConfigurationProvider.ExpressionBuilder).To(_parameters, _membersToExpand); + } + // case #2: query is arbitrary ("manual") projection + // exaple: users.UseAsDataSource().For().Select(user => user.Age).ToList() + // in case an arbitrary select-statement is enumerated, we do not need to map the expression at all + // and cann safely return it + else if (IsProjection(resultType, sourceExpression)) + { + var sourceResult = _dataSource.Provider.CreateQuery(sourceExpression); + var enumerator = sourceResult.GetEnumerator(); + var elementType = ElementTypeHelper.GetElementType(typeof(TResult)); + var constructorInfo = typeof(List<>).MakeGenericType(elementType).GetDeclaredConstructor(new Type[0]); + if (constructorInfo != null) + { + var listInstance = (IList)constructorInfo.Invoke(null); + while (enumerator.MoveNext()) + { + listInstance.Add(enumerator.Current); + } + destResult = listInstance; + } + } + // case #2: projection to simple type + // example: users.UseAsDataSource().For().FirstOrDefault(user => user.Age > 20) + else + { + /* + in case of an element result (so instead of IQueryable, just TResult) + we still want to support parameters. + This is e.g. the case, when the developer writes "UseAsDataSource().For().FirstOrDefault(x => ...) + To still be able to support parameters, we need to create a query from it. + That said, we need to replace the "element" operator "FirstOrDefault" with a "Where" operator, then apply our "Select" + to map from TSource to TResult and finally re-apply the "element" operator ("FirstOrDefault" in our case) so only + one element is returned by our "Execute" method. Otherwise we'd get an InvalidCastException + + * So first we visit the sourceExpression and replace "element operators" with "where" + * then we create our mapping expression from TSource to TDestination (select) and apply it + * finally, we search for the element expression overload of our replaced "element operator" that has no expression as parameter + this expression is not needed anymore as it has already been applied to the "Where" operation and can be safely omitted + * when we're done creating our new expression, we call the underlying provider to execute it + */ + + // as we need to replace the innermost element of the expression, + // we need to traverse it first in order to find the node to replace or potential caveats + // e.g. .UseAsDataSource().For().Select(e => e.Name).First() + // in the above case we cannot map anymore as the "select" operator overrides our mapping. + var searcher = new ReplaceableMethodNodeFinder(); + searcher.Visit(sourceExpression); + // provide the replacer with our found MethodNode or + var replacer = new MethodNodeReplacer(searcher.MethodNode); + + // default back to simple mapping of object instance for backwards compatibility (e.g. mapping non-nullable to nullable fields) + sourceExpression = replacer.Visit(sourceExpression); + + if (replacer.FoundElementOperator) + { + /* in case of primitive element operators (e.g. Any(), Sum()...) + we need to map the originating types (e.g. Entity to Dto) in this query + as the final value will be projected automatically + + == example 1 == + UseAsDataSource().For().Any(entity => entity.Name == "thename") + ..in that case sourceResultType and destResultType would both be "Boolean" which is not mappable for AutoMapper + + == example 2 == + UseAsDataSource().For().FirstOrDefault(entity => entity.Name == "thename") + ..in this case the sourceResultType would be Entity and the destResultType Dto, so we can map the query directly + */ + + if (sourceResultType == destResultType)// && destResultType.IsPrimitive) + { + sourceResultType = typeof(TSource); + destResultType = typeof(TDestination); + } + + var membersToExpand = _membersToExpand.SelectMany(m => m).Distinct().ToArray(); + var lambdas = _mapper.ConfigurationProvider.ExpressionBuilder.GetMapExpression(sourceResultType, destResultType, + _parameters, membersToExpand); + // add projection via "select" operator + var expr = lambdas.Aggregate(sourceExpression, (source, lambda) => Select(source, lambda)); + // in case an element operator without predicate expression was found (and thus not replaced) + var replacementMethod = replacer.ElementOperator; + // in case an element operator with predicate expression was replaced + if (replacer.ReplacedMethod != null) + { + // find replacement method that has no more predicates + replacementMethod = typeof(Queryable).GetAllMethods() + .Single(m => m.Name == replacer.ReplacedMethod.Name +#if NET45 || NET40 + && m.GetParameters().All(p => typeof(Queryable).IsAssignableFrom(p.Member.ReflectedType)) +#endif + && m.GetParameters().Length == replacer.ReplacedMethod.GetParameters().Length - 1); + } + + // reintroduce element operator that was replaced with a "where" operator to make it queryable + expr = Expression.Call(null, + replacementMethod.MakeGenericMethod(destResultType), expr); + + destResult = _dataSource.Provider.Execute(expr); + } + // If there was no element operator that needed to be replaced by "where", just map the result + else + { + var sourceResult = _dataSource.Provider.Execute(sourceExpression); + Inspector.SourceResult(sourceExpression, sourceResult); + destResult = _mapper.Map(sourceResult, sourceResultType, destResultType); + } + } + + Inspector.DestResult(destResult); + + // implicitly convert types in case of valuetypes which cannot be casted explicitly + if (typeof(TResult).IsValueType() && destResult?.GetType() != typeof(TResult)) + return (TResult)Convert.ChangeType(destResult, typeof(TResult)); + + // if it is not a valuetype, we can safely cast it + return (TResult)destResult; + } + catch (Exception x) + { + _exceptionHandler(x); + throw; + } + } + + private static Expression Select(Expression source, LambdaExpression lambda) + { + return Expression.Call( + null, + QueryableSelectMethod.MakeGenericMethod(lambda.Parameters[0].Type, lambda.ReturnType), + new[] { source, Expression.Quote(lambda) } + ); + } + + private object InvokeSourceQuery(Type sourceResultType, Expression sourceExpression) + { + var result = IsProjection(sourceResultType) + ? _dataSource.Provider.CreateQuery(sourceExpression) + : _dataSource.Provider.Execute(sourceExpression); + return result; + } + + private static bool IsProjection(Type resultType) => IsProjection(resultType) && resultType.GetGenericElementType() == typeof(T); + + private static bool IsProjection(Type resultType, Expression sourceExpression) + { + if (!IsProjection(resultType)) + { + return false; + } + + // in cases, where the query selects an IEnumerable itself, the former "IsProjection" condition is not sufficient + // as it would detect that enuerable as projection + // e.g. Source and Destination class both have a property "string[] Items{get;set;}" + // and the query was + // var result = sources.UseAsDataSource().For().Select(dest => dest.Items).First(); + // "result" would still be an IEnumerable and IsProjection would return "true" + // therefore we need to search for the existence of an "linq element operator" (an operator that returns a single element from an enumerable) + var searcher = new ElementOperatorSearcher(); + searcher.Visit(sourceExpression); + return !searcher.ContainsElementOperator; + } + + private static bool IsProjection(Type resultType) + => resultType.IsEnumerableType() && !resultType.IsQueryableType() && resultType != typeof(string); + + private static Type CreateSourceResultType(Type destResultType) + { + var sourceResultType = destResultType.ReplaceItemType(typeof(TDestination), typeof(TSource)); + return sourceResultType; + } + + private Expression ConvertDestinationExpressionToSourceExpression(Expression expression) + { + // call beforevisitors + expression = _beforeVisitors.Aggregate(expression, (current, before) => before.Visit(current)); + + var typeMap = _mapper.ConfigurationProvider.ResolveTypeMap(typeof(TDestination), typeof(TSource)); + var visitor = new ExpressionMapper.MappingVisitor(_mapper.ConfigurationProvider, typeMap, _destQuery.Expression, _dataSource.Expression, null, + new[] { typeof(TSource) }); + var sourceExpression = visitor.Visit(expression); + + // apply parameters + if (_parameters != null && _parameters.Any()) + { + var constantVisitor = new ExpressionBuilder.ConstantExpressionReplacementVisitor(_parameters); + sourceExpression = constantVisitor.Visit(sourceExpression); + } + + // apply null guards in case the feature is enabled + if (_mapper.ConfigurationProvider.EnableNullPropagationForQueryMapping) + { + var nullGuardVisitor = new ExpressionBuilder.NullsafeQueryRewriter(); + sourceExpression = nullGuardVisitor.Visit(sourceExpression); + } + // call aftervisitors + sourceExpression = _afterVisitors.Aggregate(sourceExpression, (current, after) => after.Visit(current)); + + return sourceExpression; + } + + private static readonly MethodInfo QueryableSelectMethod = FindQueryableSelectMethod(); + + private static MethodInfo FindQueryableSelectMethod() + { + Expression>> select = () => default(IQueryable).Select(default(Expression>)); + var method = ((MethodCallExpression)select.Body).Method.GetGenericMethodDefinition(); + return method; + } + } + + internal class ReplaceableMethodNodeFinder : ExpressionVisitor + { + public MethodCallExpression MethodNode { get; private set; } + private bool _ignoredMethodFound; + private static readonly string[] IgnoredMethods = { "Select" }; + + protected override Expression VisitMethodCall(MethodCallExpression node) + { + if (_ignoredMethodFound) + { + return base.VisitMethodCall(node); + } + + var isReplacableMethod = node.Method.DeclaringType == typeof(Queryable) + && !IgnoredMethods.Contains(node.Method.Name) + && !typeof(IQueryable).IsAssignableFrom(node.Method.ReturnType); + + // invalid method found => skip all (e.g. Select(entity=> (object)entity.Child1) + if (isReplacableMethod && + !node.Method.ReturnType.IsPrimitive() && node.Method.ReturnType != typeof(TDestination)) + { + return base.VisitMethodCall(node); + } + + if (isReplacableMethod) + { + MethodNode = node; + } + // in case we find an incompatible method (Select), the already found MethodNode becomes invalid + else if (IgnoredMethods.Contains(node.Method.Name)) + { + _ignoredMethodFound = true; + MethodNode = null; + } + + return base.VisitMethodCall(node); + } + } + + internal class MethodNodeReplacer : ExpressionVisitor + { + private readonly MethodCallExpression _foundExpression; + private static readonly MethodInfo QueryableWhereMethod = FindQueryableWhereMethod(); + + + private static MethodInfo FindQueryableWhereMethod() + { + Expression>> select = () => default(IQueryable).Where(default(Expression>)); + var method = ((MethodCallExpression)select.Body).Method.GetGenericMethodDefinition(); + return method; + } + + public MethodNodeReplacer(MethodCallExpression foundExpression) => _foundExpression = foundExpression; + + public MethodInfo ReplacedMethod { get; private set; } + + public MethodInfo ElementOperator { get; private set; } + + public bool FoundElementOperator { get; private set; } + + protected override Expression VisitMethodCall(MethodCallExpression node) + { + // only replace first occurrence + if (ReplacedMethod != null || _foundExpression == null) + { + return base.VisitMethodCall(node); + } + + if (node == _foundExpression) + { + // if method has invalid type + var parameters = node.Method.GetParameters(); + + if (parameters.Length > 1 && + typeof(Expression).IsAssignableFrom(parameters[1].ParameterType)) + { + FoundElementOperator = true; + ReplacedMethod = node.Method.GetGenericMethodDefinition(); + return Expression.Call(null, QueryableWhereMethod.MakeGenericMethod(typeof(TDestination)), + node.Arguments); + } + // no predicate + if (parameters.Length == 1) + { + FoundElementOperator = true; + ElementOperator = node.Method.GetGenericMethodDefinition(); + return node.Arguments[0]; + } + } + + return base.VisitMethodCall(node); + } + } + + internal class ElementOperatorSearcher : ExpressionVisitor + { + protected override Expression VisitMethodCall(MethodCallExpression node) + { + var isElementOperator = node.Method.DeclaringType == typeof(Queryable) + && !typeof(IQueryable).IsAssignableFrom(node.Method.ReturnType); + + if (!ContainsElementOperator) + { + ContainsElementOperator = isElementOperator; + } + + return base.VisitMethodCall(node); + } + + public bool ContainsElementOperator { get; private set; } + } +} \ No newline at end of file diff --git a/tools/AutoMapper/QueryableExtensions/Impl/StringExpressionBinder.cs b/tools/AutoMapper/QueryableExtensions/Impl/StringExpressionBinder.cs new file mode 100644 index 0000000000..8106b69ed3 --- /dev/null +++ b/tools/AutoMapper/QueryableExtensions/Impl/StringExpressionBinder.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using System.Linq.Expressions; + +namespace AutoMapper.QueryableExtensions.Impl +{ + public class StringExpressionBinder : IExpressionBinder + { + public bool IsMatch(PropertyMap propertyMap, TypeMap propertyTypeMap, ExpressionResolutionResult result) + => propertyMap.DestinationPropertyType == typeof(string); + + public MemberAssignment Build(IConfigurationProvider configuration, PropertyMap propertyMap, TypeMap propertyTypeMap, ExpressionRequest request, ExpressionResolutionResult result, IDictionary typePairCount, LetPropertyMaps letPropertyMaps) + => BindStringExpression(propertyMap, result); + + private static MemberAssignment BindStringExpression(PropertyMap propertyMap, ExpressionResolutionResult result) + => Expression.Bind(propertyMap.DestinationProperty, Expression.Call(result.ResolutionExpression, "ToString", null, null)); + } +} \ No newline at end of file diff --git a/tools/AutoMapper/QueryableExtensions/ProjectionExpression.cs b/tools/AutoMapper/QueryableExtensions/ProjectionExpression.cs new file mode 100644 index 0000000000..9893693c30 --- /dev/null +++ b/tools/AutoMapper/QueryableExtensions/ProjectionExpression.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using AutoMapper.Internal; +using IObjectDictionary = System.Collections.Generic.IDictionary; + +namespace AutoMapper.QueryableExtensions +{ + using MemberPaths = IEnumerable>; + + public class ProjectionExpression : IProjectionExpression + { + private static readonly MethodInfo QueryableSelectMethod = FindQueryableSelectMethod(); + + private readonly IQueryable _source; + private readonly IExpressionBuilder _builder; + + public ProjectionExpression(IQueryable source, IExpressionBuilder builder) + { + _source = source; + _builder = builder; + } + + private static MethodInfo FindQueryableSelectMethod() + { + Expression>> select = () => default(IQueryable).Select(default(Expression>)); + var method = ((MethodCallExpression)select.Body).Method.GetGenericMethodDefinition(); + return method; + } + + public IQueryable To(object parameters = null) => To(parameters, new string[0]); + + public IQueryable To(object parameters = null, params string[] membersToExpand) + { + var paramValues = GetParameters(parameters); + return To(paramValues, membersToExpand); + } + + private static IObjectDictionary GetParameters(object parameters) + { + return (parameters ?? new object()).GetType() + .GetDeclaredProperties() + .ToDictionary(pi => pi.Name, pi => pi.GetValue(parameters, null)); + } + + public IQueryable To(IObjectDictionary parameters) => To(parameters, new string[0]); + + public IQueryable To(IObjectDictionary parameters, params string[] membersToExpand) + { + var members = GetMemberPaths(typeof(TResult), membersToExpand); + return To(parameters, members); + } + + public IQueryable To(object parameters = null, params Expression>[] membersToExpand) + => To(GetParameters(parameters), GetMemberPaths(membersToExpand)); + + public static MemberPaths GetMemberPaths(Type type, string[] membersToExpand) => + membersToExpand.Select(m => ReflectionHelper.GetMemberPath(type, m)); + + public static MemberPaths GetMemberPaths(Expression>[] membersToExpand) => + membersToExpand.Select(expr => MemberVisitor.GetMemberPath(expr)); + + public IQueryable To(IObjectDictionary parameters, params Expression>[] membersToExpand) + { + var members = GetMemberPaths(membersToExpand); + return To(parameters, members); + } + + internal IQueryable To(IObjectDictionary parameters, MemberPaths memberPathsToExpand) + { + var membersToExpand = memberPathsToExpand.SelectMany(m => m).Distinct().ToArray(); + + parameters = parameters ?? new Dictionary(); + var mapExpressions = _builder.GetMapExpression(_source.ElementType, typeof(TResult), parameters, membersToExpand); + + return (IQueryable)mapExpressions.Aggregate(_source, (source, lambda)=>Select(source, lambda)); + } + + private static IQueryable Select(IQueryable source, LambdaExpression lambda) + { + return source.Provider.CreateQuery( + Expression.Call( + null, + QueryableSelectMethod.MakeGenericMethod(source.ElementType, lambda.ReturnType), + new[] { source.Expression, Expression.Quote(lambda) } + ) + ); + } + } +} \ No newline at end of file diff --git a/tools/AutoMapper/ReflectionExtensions.cs b/tools/AutoMapper/ReflectionExtensions.cs new file mode 100644 index 0000000000..c4cc871b06 --- /dev/null +++ b/tools/AutoMapper/ReflectionExtensions.cs @@ -0,0 +1,333 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Reflection; +using AutoMapper.Internal; + +namespace AutoMapper +{ + 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) + => ReflectionHelper.MapMember(context, member, value, destination); + + public static object MapMember(this ResolutionContext context, MemberInfo member, object value) + => ReflectionHelper.MapMember(context, member, value); + + public static bool IsDynamic(this object obj) + => ReflectionHelper.IsDynamic(obj); + + public static bool IsDynamic(this Type type) + => ReflectionHelper.IsDynamic(type); + + public static void SetMemberValue(this MemberInfo propertyOrField, object target, object value) + => ReflectionHelper.SetMemberValue(propertyOrField, target, value); + + public static object GetMemberValue(this MemberInfo propertyOrField, object target) + => ReflectionHelper.GetMemberValue(propertyOrField, target); + + public static IEnumerable GetMemberPath(Type type, string fullMemberName) + => ReflectionHelper.GetMemberPath(type, fullMemberName); + + public static MemberInfo GetFieldOrProperty(this LambdaExpression expression) + => ReflectionHelper.GetFieldOrProperty(expression); + + public static MemberInfo FindProperty(LambdaExpression lambdaExpression) + => ReflectionHelper.FindProperty(lambdaExpression); + + public static Type GetMemberType(this MemberInfo memberInfo) + => ReflectionHelper.GetMemberType(memberInfo); + + /// + /// if targetType is oldType, method will return newType + /// if targetType is not oldType, method will return targetType + /// if targetType is generic type with oldType arguments, method will replace all oldType arguments on newType + /// + /// + /// + /// + /// + public static Type ReplaceItemType(this Type targetType, Type oldType, Type newType) + => ReflectionHelper.ReplaceItemType(targetType, oldType, newType); + +#if NET40 + public static TypeInfo GetTypeInfo(this Type type) + { + return TypeInfo.FromType(type); + } + + public static IEnumerable GetDefinedTypes(this Assembly assembly) + { + Type[] types = assembly.GetTypes(); + TypeInfo[] array = new TypeInfo[types.Length]; + for (int i = 0; i < types.Length; i++) + { + TypeInfo typeInfo = types[i].GetTypeInfo(); + array[i] = typeInfo ?? throw new NotSupportedException(); + } + return array; + } + + public static bool GetHasDefaultValue(this ParameterInfo info) => + info.GetDefaultValue() != DBNull.Value; + + public static bool GetIsConstructedGenericType(this Type type) => + type.IsGenericType && !type.IsGenericTypeDefinition; +#else + public static IEnumerable GetDefinedTypes(this Assembly assembly) => + assembly.DefinedTypes; + + public static bool GetHasDefaultValue(this ParameterInfo info) => + info.HasDefaultValue; + + public static bool GetIsConstructedGenericType(this Type type) => + type.IsConstructedGenericType; +#endif + } +} + +#if NET40 +namespace System.Reflection +{ + using System.Collections.Concurrent; + using System.Globalization; + + [Serializable] + internal class TypeInfo + { + public static readonly ConcurrentDictionary _typeInfoCache = new ConcurrentDictionary(); + + public static TypeInfo FromType(Type type) + { + return _typeInfoCache.GetOrAdd(type, t => new TypeInfo(t)); + } + + public IEnumerable ImplementedInterfaces => _type.GetInterfaces(); + public Type[] GenericTypeParameters + { + get + { + if (_type.IsGenericTypeDefinition) + return _type.GetGenericArguments(); + return Type.EmptyTypes; + } + } + + public Type[] GenericTypeArguments + { + get + { + if (_type.IsGenericType && !_type.IsGenericTypeDefinition) + return _type.GetGenericArguments(); + return Type.EmptyTypes; + } + } + + public virtual IEnumerable DeclaredConstructors => + _type.GetConstructors(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); + + public virtual IEnumerable DeclaredMembers => + _type.GetMembers(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); + + public virtual IEnumerable DeclaredMethods => + _type.GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); + + public virtual IEnumerable DeclaredProperties => + _type.GetProperties(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); + + public virtual Guid GUID => _type.GUID; + + public virtual Module Module => _type.Module; + + public virtual Assembly Assembly => _type.Assembly; + + public virtual string FullName => _type.FullName; + + public virtual string Namespace => _type.Namespace; + + public virtual string AssemblyQualifiedName => _type.AssemblyQualifiedName; + + public virtual Type BaseType => _type.BaseType; + + public virtual Type UnderlyingSystemType => _type.UnderlyingSystemType; + + public virtual string Name => _type.Name; + + public bool IsInterface => _type.IsInterface; + + public bool IsGenericParameter => _type.IsGenericParameter; + + public bool IsValueType => _type.IsValueType; + + public bool IsGenericType => _type.IsGenericType; + + public bool IsAbstract => _type.IsAbstract; + + public bool IsClass => _type.IsClass; + + public bool IsEnum => _type.IsEnum; + + public bool IsGenericTypeDefinition => _type.IsGenericTypeDefinition; + + public bool IsSealed => _type.IsSealed; + + public bool IsPrimitive => _type.IsPrimitive; + + private readonly Type _type; + + protected TypeInfo(Type type) + { + _type = type; + } + + public Type AsType() => _type; + + public virtual PropertyInfo GetDeclaredProperty(string name) => + _type.GetProperty(name, BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); + + public virtual object InvokeMember(string name, BindingFlags invokeAttr, Binder binder, object target, object[] args, ParameterModifier[] modifiers, CultureInfo culture, string[] namedParameters) => + _type.InvokeMember(name, invokeAttr, binder, target, args, modifiers, culture, namedParameters); + + public virtual ConstructorInfo[] GetConstructors(BindingFlags bindingAttr) => + _type.GetConstructors(bindingAttr); + + public virtual MethodInfo[] GetMethods(BindingFlags bindingAttr) => + _type.GetMethods(bindingAttr); + + public virtual FieldInfo GetField(string name, BindingFlags bindingAttr) => + _type.GetField(name, bindingAttr); + + public virtual FieldInfo[] GetFields(BindingFlags bindingAttr) => + _type.GetFields(bindingAttr); + + public virtual Type GetInterface(string name, bool ignoreCase) => + _type.GetInterface(name, ignoreCase); + + public virtual Type[] GetInterfaces() => + _type.GetInterfaces(); + + public virtual EventInfo GetEvent(string name, BindingFlags bindingAttr) => + _type.GetEvent(name, bindingAttr); + + public virtual EventInfo[] GetEvents(BindingFlags bindingAttr) => + _type.GetEvents(bindingAttr); + + public virtual PropertyInfo[] GetProperties(BindingFlags bindingAttr) => + _type.GetProperties(bindingAttr); + + public virtual Type[] GetNestedTypes(BindingFlags bindingAttr) => + _type.GetNestedTypes(bindingAttr); + + public virtual Type GetNestedType(string name, BindingFlags bindingAttr) => + _type.GetNestedType(name, bindingAttr); + + public virtual MemberInfo[] GetMembers(BindingFlags bindingAttr) => + _type.GetMembers(bindingAttr); + + public virtual Type GetElementType() => + _type.GetElementType(); + + public virtual object[] GetCustomAttributes(bool inherit) => + _type.GetCustomAttributes(inherit); + + public virtual object[] GetCustomAttributes(Type attributeType, bool inherit) => + _type.GetCustomAttributes(attributeType, inherit); + + public virtual bool IsDefined(Type attributeType, bool inherit) => + _type.IsDefined(attributeType, inherit); + + public virtual bool IsSubclassOf(Type c) => + _type.IsSubclassOf(c); + + public virtual Type[] GetGenericParameterConstraints() => + _type.GetGenericParameterConstraints(); + + public virtual bool IsAssignableFrom(TypeInfo typeInfo) + { + if (typeInfo == null) + { + return false; + } + if (this == typeInfo) + { + return true; + } + if (typeInfo.IsSubclassOf(_type)) + { + return true; + } + if (IsInterface) + { + return typeInfo._type.ImplementInterface(_type); + } + if (this.IsGenericParameter) + { + Type[] genericParameterConstraints = this.GetGenericParameterConstraints(); + for (int i = 0; i < genericParameterConstraints.Length; i++) + { + if (!genericParameterConstraints[i].IsAssignableFrom(typeInfo._type)) + { + return false; + } + } + return true; + } + return false; + } + } + + static class CustomAttributeExtensions + { + public static T GetCustomAttribute(this MemberInfo element, bool inherit) where T : Attribute + { + return (T)Attribute.GetCustomAttribute(element, typeof(T), inherit); + } + + public static Attribute GetCustomAttribute(this MemberInfo element, Type attributeType) + { + return Attribute.GetCustomAttribute(element, attributeType, false); + } + } + + static class TypeExtensions + { + public static bool ImplementInterface(this Type type, Type ifaceType) + { + while (type != null) + { + Type[] interfaces = type.GetInterfaces(); + if (interfaces != null) + { + for (int i = 0; i < interfaces.Length; i++) + { + if (interfaces[i] == ifaceType || (interfaces[i] != null && interfaces[i].ImplementInterface(ifaceType))) + { + return true; + } + } + } + type = type.BaseType; + } + return false; + } + } + + static class RuntimeReflectionExtensions + { + public static IEnumerable GetRuntimeMethods(this Type type) => + type.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); + + public static IEnumerable GetRuntimeProperties(this Type type) => + type.GetProperties(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); + + public static FieldInfo GetRuntimeField(this Type type, string name) => + type.GetField(name); + + public static EventInfo GetRuntimeEvent(this Type type, string name) => + type.GetEvent(name); + } +} +#endif \ No newline at end of file diff --git a/tools/AutoMapper/ResolutionContext.cs b/tools/AutoMapper/ResolutionContext.cs new file mode 100644 index 0000000000..c2c9972ba5 --- /dev/null +++ b/tools/AutoMapper/ResolutionContext.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections.Generic; + +namespace AutoMapper +{ + /// + /// Context information regarding resolution of a destination value + /// + public class ResolutionContext + { + private Dictionary _instanceCache; + private Dictionary _typeDepth; + + /// + /// Mapping operation options + /// + public IMappingOperationOptions Options { get; } + + internal object GetDestination(object source, Type destinationType) + { + InstanceCache.TryGetValue(new ContextCacheKey(source, destinationType), out object destination); + return destination; + } + + internal void CacheDestination(object source, Type destinationType, object destination) + { + InstanceCache[new ContextCacheKey(source, destinationType)] = destination; + } + + /// + /// Instance cache for resolving circular references + /// + public Dictionary InstanceCache + { + get + { + CheckDefault(); + if(_instanceCache != null) + { + return _instanceCache; + } + _instanceCache = new Dictionary(); + return _instanceCache; + } + } + + private void CheckDefault() + { + if(IsDefault) + { + throw new InvalidOperationException(); + } + } + + /// + /// Instance cache for resolving keeping track of depth + /// + private Dictionary TypeDepth + { + get + { + CheckDefault(); + if(_typeDepth != null) + { + return _typeDepth; + } + _typeDepth = new Dictionary(); + return _typeDepth; + } + } + + internal void IncrementTypeDepth(TypePair types) + { + TypeDepth[types]++; + } + + internal void DecrementTypeDepth(TypePair types) + { + TypeDepth[types]--; + } + + internal int GetTypeDepth(TypePair types) + { + if (!TypeDepth.ContainsKey(types)) + TypeDepth[types] = 1; + + return TypeDepth[types]; + } + + /// + /// Current mapper + /// + public IRuntimeMapper Mapper { get; } + + /// + /// Current configuration + /// + public IConfigurationProvider ConfigurationProvider => Mapper.ConfigurationProvider; + + /// + /// Context items from + /// + public IDictionary Items => Options.Items; + + public ResolutionContext(IMappingOperationOptions options, IRuntimeMapper mapper) + { + Options = options; + Mapper = mapper; + } + + internal bool IsDefault => this == Mapper.DefaultContext; + + internal TDestination Map(TSource source, TDestination destination) + => Mapper.Map(source, destination, this); + + internal object Map(object source, object destination, Type sourceType, Type destinationType) + => Mapper.Map(source, destination, sourceType, destinationType, this); + + internal void ValidateMap(TypeMap typeMap) + => ConfigurationProvider.AssertConfigurationIsValid(typeMap); + } + + public struct ContextCacheKey : IEquatable + { + public static bool operator ==(ContextCacheKey left, ContextCacheKey right) => left.Equals(right); + public static bool operator !=(ContextCacheKey left, ContextCacheKey right) => !left.Equals(right); + + private readonly object _source; + private readonly Type _destinationType; + + public ContextCacheKey(object source, Type destinationType) + { + _source = source; + _destinationType = destinationType; + } + + public override int GetHashCode() => HashCodeCombiner.Combine(_source, _destinationType); + + public bool Equals(ContextCacheKey other) => + _source == other._source && _destinationType == other._destinationType; + + public override bool Equals(object other) => + other is ContextCacheKey && Equals((ContextCacheKey)other); + } +} \ No newline at end of file diff --git a/tools/AutoMapper/SemanticModel.cd b/tools/AutoMapper/SemanticModel.cd new file mode 100644 index 0000000000..366ddf4a49 --- /dev/null +++ b/tools/AutoMapper/SemanticModel.cd @@ -0,0 +1,163 @@ + + + + + + + + AAAAAAIAABAAgAAAAAAAAAAAAAAAAAAAgAEAAAIBCAA= + MappingEngine.cs + + + + + + + + + + + + + + + + AAAABAgAEAJBwACAASAAgCAIAACAIUAAgEgQAEEZEgA= + Configuration.cs + + + + + + + + + + + + + + AACCAgAAAAAgAAAABAAAEAAAEAAAAEgAABAACAAAAAA= + TypeMap.cs + + + + + + + + + + + AAAQBCAQCAAQAwQQAGAIAAIAQQAhAAAIQABACAAgYgA= + PropertyMap.cs + + + + + + + + + + AAAACAAAEAAARAAAACAQAAAIAAAAIAAAgAAADAABIAE= + Internal\FormatterExpression.cs + + + + + + + + + + AAAAAAAAAAIAAAAAAAAAAAAAAACAABAAAAAAIAEIAAA= + IConfiguration.cs + + + + + + AAACAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + IObjectMapper.cs + + + + + + AAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + IValueResolver.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAA= + IValueFormatter.cs + + + + \ No newline at end of file diff --git a/tools/AutoMapper/TypeDetails.cs b/tools/AutoMapper/TypeDetails.cs new file mode 100644 index 0000000000..623577374f --- /dev/null +++ b/tools/AutoMapper/TypeDetails.cs @@ -0,0 +1,231 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using AutoMapper.Configuration; + +namespace AutoMapper +{ + /// + /// Contains cached reflection information for easy retrieval + /// + [DebuggerDisplay("{Type}")] + public class TypeDetails + { + public TypeDetails(Type type, ProfileMap config) + { + Type = type; + var membersToMap = MembersToMap(config.ShouldMapProperty, config.ShouldMapField); + var publicReadableMembers = GetAllPublicReadableMembers(membersToMap); + var publicWritableMembers = GetAllPublicWritableMembers(membersToMap); + PublicReadAccessors = BuildPublicReadAccessors(publicReadableMembers); + PublicWriteAccessors = BuildPublicAccessors(publicWritableMembers); + PublicNoArgMethods = BuildPublicNoArgMethods(); + Constructors = type.GetDeclaredConstructors().Where(ci => !ci.IsStatic).ToArray(); + PublicNoArgExtensionMethods = BuildPublicNoArgExtensionMethods(config.SourceExtensionMethods); + AllMembers = PublicReadAccessors.Concat(PublicNoArgMethods).Concat(PublicNoArgExtensionMethods).ToList(); + DestinationMemberNames = AllMembers.Select(mi => new DestinationMemberName { Member = mi, Possibles = PossibleNames(mi.Name, config.Prefixes, config.Postfixes).ToArray() }); + } + + private IEnumerable PossibleNames(string memberName, IEnumerable prefixes, IEnumerable postfixes) + { + yield return memberName; + + if (!postfixes.Any()) + { + foreach (var withoutPrefix in prefixes.Where(prefix => memberName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)).Select(prefix => memberName.Substring(prefix.Length))) + { + yield return withoutPrefix; + } + yield break; + } + + foreach (var withoutPrefix in prefixes.Where(prefix => memberName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)).Select(prefix => memberName.Substring(prefix.Length))) + { + yield return withoutPrefix; + foreach (var s in PostFixes(postfixes, withoutPrefix)) + yield return s; + } + foreach (var s in PostFixes(postfixes, memberName)) + yield return s; + } + + private static IEnumerable PostFixes(IEnumerable postfixes, string name) + { + return + postfixes.Where(postfix => name.EndsWith(postfix, StringComparison.OrdinalIgnoreCase)) + .Select(postfix => name.Remove(name.Length - postfix.Length)); + } + + private static Func MembersToMap(Func shouldMapProperty, Func shouldMapField) + { + return m => + { + switch (m) + { + case PropertyInfo property: + return !property.IsStatic() && shouldMapProperty(property); + case FieldInfo field: + return !field.IsStatic && shouldMapField(field); + default: + throw new ArgumentException("Should be a field or property."); + } + }; + } + + public struct DestinationMemberName + { + public MemberInfo Member { get; set; } + public string[] Possibles { get; set; } + } + + public Type Type { get; } + + public IEnumerable Constructors { get; } + + public IEnumerable PublicReadAccessors { get; } + + public IEnumerable PublicWriteAccessors { get; } + + public IEnumerable PublicNoArgMethods { get; } + + public IEnumerable PublicNoArgExtensionMethods { get; } + + public IEnumerable AllMembers { get; } + + public IEnumerable DestinationMemberNames { get; set; } + + private IEnumerable BuildPublicNoArgExtensionMethods(IEnumerable sourceExtensionMethodSearch) + { + var explicitExtensionMethods = sourceExtensionMethodSearch.Where(method => method.GetParameters()[0].ParameterType == Type); + + var genericInterfaces = Type.GetTypeInfo().ImplementedInterfaces.Where(t => t.IsGenericType()); + + if (Type.IsInterface() && Type.IsGenericType()) + { + genericInterfaces = genericInterfaces.Union(new[] { Type }); + } + return explicitExtensionMethods.Union + ( + from genericInterface in genericInterfaces + let genericInterfaceArguments = genericInterface.GetTypeInfo().GenericTypeArguments + let matchedMethods = ( + from extensionMethod in sourceExtensionMethodSearch + where !extensionMethod.IsGenericMethodDefinition + select extensionMethod + ).Concat( + from extensionMethod in sourceExtensionMethodSearch + where extensionMethod.IsGenericMethodDefinition + && extensionMethod.GetGenericArguments().Length == genericInterfaceArguments.Length + let constructedGeneric = MakeGenericMethod(extensionMethod, genericInterfaceArguments) + where constructedGeneric != null + select constructedGeneric + ) + from methodMatch in matchedMethods + where methodMatch.GetParameters()[0].ParameterType.GetTypeInfo().IsAssignableFrom(genericInterface.GetTypeInfo()) + select methodMatch + ).ToArray(); + } + + // Use method.MakeGenericMethod(genericArguments) wrapped in a try/catch(ArgumentException) + // in order to catch exceptions resulting from the generic arguments not being compatible + // with any constraints that may be on the generic method's generic parameters. + static MethodInfo MakeGenericMethod(MethodInfo genericMethod, Type[] genericArguments) + { + try + { + return genericMethod.MakeGenericMethod(genericArguments); + } + catch (ArgumentException) + { + return null; + } + } + + private static MemberInfo[] BuildPublicReadAccessors(IEnumerable allMembers) + { + // Multiple types may define the same property (e.g. the class and multiple interfaces) - filter this to one of those properties + var filteredMembers = allMembers + .OfType() + .GroupBy(x => x.Name) // group properties of the same name together + .Select(x => x.First()) + .Concat(allMembers.Where(x => x is FieldInfo)); // add FieldInfo objects back + + return filteredMembers.ToArray(); + } + + private static MemberInfo[] BuildPublicAccessors(IEnumerable allMembers) + { + // Multiple types may define the same property (e.g. the class and multiple interfaces) - filter this to one of those properties + var filteredMembers = allMembers + .OfType() + .GroupBy(x => x.Name) // group properties of the same name together + .Select(x => + x.Any(y => y.CanWrite && y.CanRead) + ? // favor the first property that can both read & write - otherwise pick the first one + x.First(y => y.CanWrite && y.CanRead) + : x.First()) + .Where(pi => pi.CanWrite || pi.PropertyType.IsListOrDictionaryType()) + //.OfType() // cast back to MemberInfo so we can add back FieldInfo objects + .Concat(allMembers.Where(x => x is FieldInfo)); // add FieldInfo objects back + + return filteredMembers.ToArray(); + } + + private IEnumerable GetAllPublicReadableMembers(Func membersToMap) + => GetAllPublicMembers(PropertyReadable, FieldReadable, membersToMap); + + private IEnumerable GetAllPublicWritableMembers(Func membersToMap) + => GetAllPublicMembers(PropertyWritable, FieldWritable, membersToMap); + + private static bool PropertyReadable(PropertyInfo propertyInfo) => propertyInfo.CanRead; + + private static bool FieldReadable(FieldInfo fieldInfo) => true; + + private static bool PropertyWritable(PropertyInfo propertyInfo) + { + var propertyIsEnumerable = (typeof(string) != propertyInfo.PropertyType) + && typeof(IEnumerable).GetTypeInfo().IsAssignableFrom(propertyInfo.PropertyType.GetTypeInfo()); + + return propertyInfo.CanWrite || propertyIsEnumerable; + } + + private static bool FieldWritable(FieldInfo fieldInfo) => !fieldInfo.IsInitOnly; + + private IEnumerable GetAllPublicMembers( + Func propertyAvailableFor, + Func fieldAvailableFor, + Func memberAvailableFor) + { + var typesToScan = new List(); + for (var t = Type; t != null; t = t.BaseType()) + typesToScan.Add(t); + + if (Type.IsInterface()) + typesToScan.AddRange(Type.GetTypeInfo().ImplementedInterfaces); + + // Scan all types for public properties and fields + return typesToScan + .Where(x => x != null) // filter out null types (e.g. type.BaseType == null) + .SelectMany(x => x.GetDeclaredMembers() + .Where(mi => mi.DeclaringType != null && mi.DeclaringType == x) + .Where( + m => + m is FieldInfo && fieldAvailableFor((FieldInfo)m) || + m is PropertyInfo && propertyAvailableFor((PropertyInfo)m) && + !((PropertyInfo)m).GetIndexParameters().Any()) + .Where(memberAvailableFor) + ); + } + + private MethodInfo[] BuildPublicNoArgMethods() + { + return Type.GetAllMethods() + .Where(mi => mi.IsPublic && !mi.IsStatic && mi.DeclaringType != typeof(object)) + .Where(m => (m.ReturnType != typeof(void)) && (m.GetParameters().Length == 0)) + .ToArray(); + } + } +} diff --git a/tools/AutoMapper/TypeExtensions.cs b/tools/AutoMapper/TypeExtensions.cs new file mode 100644 index 0000000000..bf425a46d9 --- /dev/null +++ b/tools/AutoMapper/TypeExtensions.cs @@ -0,0 +1,155 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; + +namespace AutoMapper +{ +#if NET45 || NET40 + using System.Reflection.Emit; +#endif + + internal static class TypeExtensions + { + public static bool Has(this Type type) where TAttribute : Attribute => type.GetTypeInfo().IsDefined(typeof(TAttribute), inherit: false); + + public static Type GetGenericTypeDefinitionIfGeneric(this Type type) => type.IsGenericType() ? type.GetGenericTypeDefinition() : type; + + public static Type[] GetGenericArguments(this Type type) => type.GetTypeInfo().GenericTypeArguments; + + public static Type[] GetGenericParameters(this Type type) => type.GetGenericTypeDefinition().GetTypeInfo().GenericTypeParameters; + + public static IEnumerable GetDeclaredConstructors(this Type type) => type.GetTypeInfo().DeclaredConstructors; + +#if !NET40 && !NET45 + public static MethodInfo GetAddMethod(this EventInfo eventInfo) => eventInfo.AddMethod; + + public static MethodInfo GetRemoveMethod(this EventInfo eventInfo) => eventInfo.RemoveMethod; +#endif + + public static Type CreateType(this TypeBuilder type) + { +#if NET40 + return type.CreateType(); +#else + return type.CreateTypeInfo().AsType(); +#endif + } + + public static IEnumerable GetDeclaredMembers(this Type type) => type.GetTypeInfo().DeclaredMembers; + + public static IEnumerable GetTypeInheritance(this Type type) + { + yield return type; + + var baseType = type.BaseType(); + while(baseType != null) + { + yield return baseType; + baseType = baseType.BaseType(); + } + } + + public static IEnumerable GetDeclaredMethods(this Type type) => type.GetTypeInfo().DeclaredMethods; + + public static MethodInfo GetDeclaredMethod(this Type type, string name) => type.GetAllMethods().FirstOrDefault(mi => mi.Name == name); + + public static MethodInfo GetDeclaredMethod(this Type type, string name, Type[] parameters) => + type.GetAllMethods().Where(mi => mi.Name == name).MatchParameters(parameters); + + public static ConstructorInfo GetDeclaredConstructor(this Type type, Type[] parameters) => + type.GetDeclaredConstructors().MatchParameters(parameters); + + private static TMethod MatchParameters(this IEnumerable methods, Type[] parameters) where TMethod : MethodBase => + methods.FirstOrDefault(mi => mi.GetParameters().Select(pi => pi.ParameterType).SequenceEqual(parameters)); + + public static IEnumerable GetAllMethods(this Type type) => type.GetRuntimeMethods(); + + public static IEnumerable GetDeclaredProperties(this Type type) => type.GetTypeInfo().DeclaredProperties; + + public static PropertyInfo GetDeclaredProperty(this Type type, string name) + => type.GetTypeInfo().GetDeclaredProperty(name); + + public static object[] GetCustomAttributes(this Type type, Type attributeType, bool inherit) + => type.GetTypeInfo().GetCustomAttributes(attributeType, inherit).Cast().ToArray(); + + public static bool IsStatic(this FieldInfo fieldInfo) => fieldInfo?.IsStatic ?? false; + + public static bool IsStatic(this PropertyInfo propertyInfo) => propertyInfo?.GetGetMethod(true)?.IsStatic + ?? propertyInfo?.GetSetMethod(true)?.IsStatic + ?? false; + + public static bool IsStatic(this MemberInfo memberInfo) => (memberInfo as FieldInfo).IsStatic() + || (memberInfo as PropertyInfo).IsStatic() + || ((memberInfo as MethodInfo)?.IsStatic + ?? false); + + public static bool IsPublic(this PropertyInfo propertyInfo) => (propertyInfo?.GetGetMethod(true)?.IsPublic ?? false) + || (propertyInfo?.GetSetMethod(true)?.IsPublic ?? false); + + public static IEnumerable PropertiesWithAnInaccessibleSetter(this Type type) + { + return type.GetDeclaredProperties().Where(pm => pm.HasAnInaccessibleSetter()); + } + + public static bool HasAnInaccessibleSetter(this PropertyInfo property) + { + var setMethod = property.GetSetMethod(true); + return setMethod == null || setMethod.IsPrivate || setMethod.IsFamily; + } + + public static bool IsPublic(this MemberInfo memberInfo) => (memberInfo as FieldInfo)?.IsPublic ?? (memberInfo as PropertyInfo).IsPublic(); + + public static bool IsNotPublic(this ConstructorInfo constructorInfo) => constructorInfo.IsPrivate + || constructorInfo.IsFamilyAndAssembly + || constructorInfo.IsFamilyOrAssembly + || constructorInfo.IsFamily; + + public static Assembly Assembly(this Type type) => type.GetTypeInfo().Assembly; + + public static Type BaseType(this Type type) => type.GetTypeInfo().BaseType; + + public static bool IsAssignableFrom(this Type type, Type other) => type.GetTypeInfo().IsAssignableFrom(other.GetTypeInfo()); + + public static bool IsAbstract(this Type type) => type.GetTypeInfo().IsAbstract; + + public static bool IsClass(this Type type) => type.GetTypeInfo().IsClass; + + public static bool IsEnum(this Type type) => type.GetTypeInfo().IsEnum; + + public static bool IsGenericType(this Type type) => type.GetTypeInfo().IsGenericType; + + public static bool IsGenericTypeDefinition(this Type type) => type.GetTypeInfo().IsGenericTypeDefinition; + + public static bool IsInterface(this Type type) => type.GetTypeInfo().IsInterface; + + public static bool IsPrimitive(this Type type) => type.GetTypeInfo().IsPrimitive; + + public static bool IsSealed(this Type type) => type.GetTypeInfo().IsSealed; + + public static bool IsValueType(this Type type) => type.GetTypeInfo().IsValueType; + + public static bool IsLiteralType(this Type type) => type == typeof(string) || type.GetTypeInfo().IsValueType; + + public static bool IsInstanceOfType(this Type type, object o) => o != null && type.GetTypeInfo().IsAssignableFrom(o.GetType().GetTypeInfo()); + + public static PropertyInfo[] GetProperties(this Type type) => type.GetRuntimeProperties().ToArray(); + +#if NET40 + public static MethodInfo GetGetMethod(this PropertyInfo propertyInfo, bool ignored) => propertyInfo.GetGetMethod(); + + public static MethodInfo GetSetMethod(this PropertyInfo propertyInfo, bool ignored) => propertyInfo.GetSetMethod(); +#else + public static MethodInfo GetGetMethod(this PropertyInfo propertyInfo, bool ignored) => propertyInfo.GetMethod; + + public static MethodInfo GetSetMethod(this PropertyInfo propertyInfo, bool ignored) => propertyInfo.SetMethod; + + public static MethodInfo GetGetMethod(this PropertyInfo propertyInfo) => propertyInfo.GetMethod; + + public static MethodInfo GetSetMethod(this PropertyInfo propertyInfo) => propertyInfo.SetMethod; +#endif + + public static FieldInfo GetField(this Type type, string name) => type.GetRuntimeField(name); + } +} diff --git a/tools/AutoMapper/TypeMap.cs b/tools/AutoMapper/TypeMap.cs new file mode 100644 index 0000000000..6bdeedc60a --- /dev/null +++ b/tools/AutoMapper/TypeMap.cs @@ -0,0 +1,399 @@ + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using AutoMapper.Configuration; +using AutoMapper.Execution; + +namespace AutoMapper +{ + using Internal; + + /// + /// Main configuration object holding all mapping configuration for a source and destination type + /// + [DebuggerDisplay("{SourceType.Name} -> {DestinationType.Name}")] + public class TypeMap + { + private readonly List _afterMapActions = new List(); + private readonly List _beforeMapActions = new List(); + private readonly HashSet _includedDerivedTypes = new HashSet(); + private readonly HashSet _includedBaseTypes = new HashSet(); + private readonly List _propertyMaps = new List(); + private readonly List _pathMaps = new List(); + private readonly List _sourceMemberConfigs = new List(); + private readonly IList _inheritedMaps = new List(); + private PropertyMap[] _orderedPropertyMaps; + private bool _sealed; + private readonly IList _inheritedTypeMaps = new List(); + private readonly List _valueTransformerConfigs = new List(); + + public TypeMap(TypeDetails sourceType, TypeDetails destinationType, ProfileMap profile) + { + SourceTypeDetails = sourceType; + DestinationTypeDetails = destinationType; + Types = new TypePair(sourceType.Type, destinationType.Type); + Profile = profile; + } + + public PathMap FindOrCreatePathMapFor(LambdaExpression destinationExpression, MemberPath path, TypeMap typeMap) + { + var pathMap = _pathMaps.SingleOrDefault(p => p.MemberPath == path); + if(pathMap == null) + { + pathMap = new PathMap(destinationExpression, path, typeMap); + _pathMaps.Add(pathMap); + } + return pathMap; + } + + public PathMap FindPathMapByDestinationPath(string destinationFullPath) => + PathMaps.SingleOrDefault(item => string.Join(".", item.MemberPath.Members.Select(m => m.Name)) == destinationFullPath); + + public LambdaExpression MapExpression { get; private set; } + + public TypePair Types { get; } + + public ConstructorMap ConstructorMap { get; set; } + + public TypeDetails SourceTypeDetails { get; } + public TypeDetails DestinationTypeDetails { get; } + + public Type SourceType => SourceTypeDetails.Type; + public Type DestinationType => DestinationTypeDetails.Type; + + public ProfileMap Profile { get; } + + public LambdaExpression CustomMapper { get; set; } + public LambdaExpression CustomProjection { get; set; } + public LambdaExpression DestinationCtor { get; set; } + + public Type DestinationTypeOverride { get; set; } + public Type DestinationTypeToUse => DestinationTypeOverride ?? DestinationType; + + public bool ConstructDestinationUsingServiceLocator { get; set; } + + public MemberList ConfiguredMemberList { get; set; } + + public IEnumerable IncludedDerivedTypes => _includedDerivedTypes; + public IEnumerable IncludedBaseTypes => _includedBaseTypes; + + public IEnumerable BeforeMapActions => _beforeMapActions; + public IEnumerable AfterMapActions => _afterMapActions; + public IEnumerable ValueTransformers => _valueTransformerConfigs; + + public bool PreserveReferences { get; set; } + public LambdaExpression Condition { get; set; } + + public int MaxDepth { get; set; } + + public LambdaExpression Substitution { get; set; } + public LambdaExpression ConstructExpression { get; set; } + public Type TypeConverterType { get; set; } + public bool DisableConstructorValidation { get; set; } + + public PropertyMap[] GetPropertyMaps() => _orderedPropertyMaps ?? _propertyMaps.Concat(_inheritedMaps).ToArray(); + public IEnumerable PathMaps => _pathMaps; + public bool IsConventionMap { get; set; } + public bool? IsValid { get; set; } + + public bool ConstructorParameterMatches(string destinationPropertyName) => + ConstructorMap?.CtorParams.Any(c => !c.DefaultValue && string.Equals(c.Parameter.Name, destinationPropertyName, StringComparison.OrdinalIgnoreCase)) == true; + + public void AddPropertyMap(MemberInfo destProperty, IEnumerable resolvers) + { + var propertyMap = new PropertyMap(destProperty, this); + + propertyMap.ChainMembers(resolvers); + + _propertyMaps.Add(propertyMap); + } + + public string[] GetUnmappedPropertyNames() + { + string GetPropertyName(PropertyMap pm) => ConfiguredMemberList == MemberList.Destination + ? pm.DestinationProperty.Name + : pm.SourceMember != null + ? pm.SourceMember.Name + : pm.DestinationProperty.Name; + string[] GetPropertyNames(IEnumerable propertyMaps) => propertyMaps.Where(pm => pm.IsMapped()).Select(GetPropertyName).ToArray(); + + var autoMappedProperties = GetPropertyNames(_propertyMaps); + var inheritedProperties = GetPropertyNames(_inheritedMaps); + + IEnumerable properties; + + if(ConfiguredMemberList == MemberList.Destination) + { + properties = DestinationTypeDetails.PublicWriteAccessors + .Select(p => p.Name) + .Except(autoMappedProperties) + .Except(inheritedProperties); + } + else + { + var redirectedSourceMembers = _propertyMaps + .Where(pm => pm.IsMapped() && pm.SourceMember != null && pm.SourceMember.Name != pm.DestinationProperty.Name) + .Select(pm => pm.SourceMember.Name); + + var ignoredSourceMembers = _sourceMemberConfigs + .Where(smc => smc.IsIgnored()) + .Select(pm => pm.SourceMember.Name).ToList(); + + properties = SourceTypeDetails.PublicReadAccessors + .Select(p => p.Name) + .Except(autoMappedProperties) + .Except(inheritedProperties) + .Except(redirectedSourceMembers) + .Except(ignoredSourceMembers); + } + + return properties.Where(memberName => !Profile.GlobalIgnores.Any(memberName.StartsWith)).ToArray(); + } + + public bool PassesCtorValidation() + { + if (DisableConstructorValidation) + return true; + + if (DestinationCtor != null) + return true; + + if (ConstructDestinationUsingServiceLocator) + return true; + + if (ConstructorMap?.CanResolve == true) + return true; + + if (DestinationTypeToUse.IsInterface()) + return true; + + if (DestinationTypeToUse.IsAbstract()) + return true; + + if (DestinationTypeToUse.IsGenericTypeDefinition()) + return true; + + if (DestinationTypeToUse.IsValueType()) + return true; + + var constructors = DestinationTypeToUse + .GetDeclaredConstructors() + .Where(ci => !ci.IsStatic); + + //find a ctor with only optional args + var ctorWithOptionalArgs = constructors.FirstOrDefault(c => c.GetParameters().All(p => p.IsOptional)); + + return ctorWithOptionalArgs != null; + } + + public PropertyMap FindOrCreatePropertyMapFor(MemberInfo destinationProperty) + { + var propertyMap = GetExistingPropertyMapFor(destinationProperty); + + if (propertyMap != null) return propertyMap; + + propertyMap = new PropertyMap(destinationProperty, this); + + _propertyMaps.Add(propertyMap); + + return propertyMap; + } + + public void IncludeDerivedTypes(Type derivedSourceType, Type derivedDestinationType) + { + var derivedTypes = new TypePair(derivedSourceType, derivedDestinationType); + if (derivedTypes.Equals(Types)) + { + throw new InvalidOperationException("You cannot include a type map into itself."); + } + _includedDerivedTypes.Add(derivedTypes); + } + + public void IncludeBaseTypes(Type baseSourceType, Type baseDestinationType) + { + var baseTypes = new TypePair(baseSourceType, baseDestinationType); + if (baseTypes.Equals(Types)) + { + throw new InvalidOperationException("You cannot include a type map into itself."); + } + _includedBaseTypes.Add(baseTypes); + } + + internal void IgnorePaths(MemberInfo destinationMember) + { + foreach(var pathMap in _pathMaps.Where(pm => pm.MemberPath.First == destinationMember)) + { + pathMap.Ignored = true; + } + } + + public Type GetDerivedTypeFor(Type derivedSourceType) + { + if (DestinationTypeOverride != null) + { + return DestinationTypeOverride; + } + // This might need to be fixed for multiple derived source types to different dest types + var match = _includedDerivedTypes.FirstOrDefault(tp => tp.SourceType == derivedSourceType); + + return match.DestinationType ?? DestinationType; + } + + public bool TypeHasBeenIncluded(TypePair derivedTypes) => _includedDerivedTypes.Contains(derivedTypes); + + public bool HasDerivedTypesToInclude() => _includedDerivedTypes.Any() || DestinationTypeOverride != null; + + public void AddBeforeMapAction(LambdaExpression beforeMap) + { + if(!_beforeMapActions.Contains(beforeMap)) + { + _beforeMapActions.Add(beforeMap); + } + } + + public void AddAfterMapAction(LambdaExpression afterMap) + { + if(!_afterMapActions.Contains(afterMap)) + { + _afterMapActions.Add(afterMap); + } + } + + public void AddValueTransformation(ValueTransformerConfiguration valueTransformerConfiguration) + { + _valueTransformerConfigs.Add(valueTransformerConfiguration); + } + + public void Seal(IConfigurationProvider configurationProvider, Stack typeMapsPath = null) + { + if(_sealed) + { + return; + } + _sealed = true; + + foreach (var inheritedTypeMap in _inheritedTypeMaps) + { + ApplyInheritedTypeMap(inheritedTypeMap); + } + + _orderedPropertyMaps = + _propertyMaps + .Union(_inheritedMaps) + .OrderBy(map => map.MappingOrder).ToArray(); + + MapExpression = new TypeMapPlanBuilder(configurationProvider, this).CreateMapperLambda(typeMapsPath); + } + + public PropertyMap GetExistingPropertyMapFor(MemberInfo destinationProperty) + { + if (!destinationProperty.DeclaringType.IsAssignableFrom(DestinationType)) + return null; + var propertyMap = + _propertyMaps.FirstOrDefault(pm => pm.DestinationProperty.Name.Equals(destinationProperty.Name)); + + if (propertyMap != null) + return propertyMap; + + propertyMap = + _inheritedMaps.FirstOrDefault(pm => pm.DestinationProperty.Name.Equals(destinationProperty.Name)); + + if (propertyMap == null) + return null; + + var propertyInfo = propertyMap.DestinationProperty as PropertyInfo; + + if (propertyInfo == null) + return propertyMap; + + var baseAccessor = propertyInfo.GetGetMethod(); + + if (baseAccessor.IsAbstract || baseAccessor.IsVirtual) + return propertyMap; + + var accessor = ((PropertyInfo)destinationProperty).GetGetMethod(); + + if (baseAccessor.DeclaringType == accessor.DeclaringType) + return propertyMap; + + return null; + } + + public void InheritTypes(TypeMap inheritedTypeMap) + { + foreach (var includedDerivedType in inheritedTypeMap._includedDerivedTypes + .Where(includedDerivedType => !_includedDerivedTypes.Contains(includedDerivedType))) + { + _includedDerivedTypes.Add(includedDerivedType); + } + } + + public SourceMemberConfig FindOrCreateSourceMemberConfigFor(MemberInfo sourceMember) + { + var config = _sourceMemberConfigs.FirstOrDefault(smc => Equals(smc.SourceMember, sourceMember)); + + if (config != null) return config; + + config = new SourceMemberConfig(sourceMember); + _sourceMemberConfigs.Add(config); + + return config; + } + + public void AddInheritedMap(TypeMap inheritedTypeMap) + { + _inheritedTypeMaps.Add(inheritedTypeMap); + } + + public bool ShouldCheckForValid() => CustomMapper == null + && CustomProjection == null + && TypeConverterType == null + && DestinationTypeOverride == null + && ConfiguredMemberList != MemberList.None + && !(IsValid ?? false); + + private void ApplyInheritedTypeMap(TypeMap inheritedTypeMap) + { + foreach (var inheritedMappedProperty in inheritedTypeMap.GetPropertyMaps().Where(m => m.IsMapped())) + { + var conventionPropertyMap = GetPropertyMaps() + .SingleOrDefault(m => + m.DestinationProperty.Name == inheritedMappedProperty.DestinationProperty.Name); + + if (conventionPropertyMap != null) + { + conventionPropertyMap.ApplyInheritedPropertyMap(inheritedMappedProperty); + } + else + { + var propertyMap = new PropertyMap(inheritedMappedProperty, this); + + _inheritedMaps.Add(propertyMap); + } + } + + //Include BeforeMap + foreach (var beforeMapAction in inheritedTypeMap._beforeMapActions) + { + AddBeforeMapAction(beforeMapAction); + } + //Include AfterMap + foreach (var afterMapAction in inheritedTypeMap._afterMapActions) + { + AddAfterMapAction(afterMapAction); + } + var notOverridenSourceConfigs = + inheritedTypeMap._sourceMemberConfigs.Where( + baseConfig => _sourceMemberConfigs.All(derivedConfig => derivedConfig.SourceMember != baseConfig.SourceMember)); + _sourceMemberConfigs.AddRange(notOverridenSourceConfigs); + var notOverridenPathMaps = + inheritedTypeMap.PathMaps.Where( + baseConfig => PathMaps.All(derivedConfig => derivedConfig.MemberPath != baseConfig.MemberPath)); + _pathMaps.AddRange(notOverridenPathMaps); + } + } +} diff --git a/tools/AutoMapper/TypeMapFactory.cs b/tools/AutoMapper/TypeMapFactory.cs new file mode 100644 index 0000000000..d9ee1e7461 --- /dev/null +++ b/tools/AutoMapper/TypeMapFactory.cs @@ -0,0 +1,72 @@ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace AutoMapper +{ + public class TypeMapFactory + { + public TypeMap CreateTypeMap(Type sourceType, Type destinationType, ProfileMap options) + { + var sourceTypeInfo = options.CreateTypeDetails(sourceType); + var destTypeInfo = options.CreateTypeDetails(destinationType); + + var typeMap = new TypeMap(sourceTypeInfo, destTypeInfo, options); + + foreach (var destProperty in destTypeInfo.PublicWriteAccessors) + { + var resolvers = new LinkedList(); + + if (MapDestinationPropertyToSource(options, sourceTypeInfo, destProperty.DeclaringType, destProperty.GetMemberType(), destProperty.Name, resolvers)) + { + typeMap.AddPropertyMap(destProperty, resolvers); + } + } + if (!destinationType.IsAbstract()) + { + foreach (var destCtor in destTypeInfo.Constructors.OrderByDescending(ci => ci.GetParameters().Length)) + { + if (MapDestinationCtorToSource(typeMap, destCtor, sourceTypeInfo, options)) + { + break; + } + } + } + return typeMap; + } + + private bool MapDestinationPropertyToSource(ProfileMap options, TypeDetails sourceTypeInfo, Type destType, Type destMemberType, string destMemberInfo, LinkedList members) + { + return options.MemberConfigurations.Any(_ => _.MapDestinationPropertyToSource(options, sourceTypeInfo, destType, destMemberType, destMemberInfo, members)); + } + + private bool MapDestinationCtorToSource(TypeMap typeMap, ConstructorInfo destCtor, TypeDetails sourceTypeInfo, ProfileMap options) + { + var ctorParameters = destCtor.GetParameters(); + + if (ctorParameters.Length == 0 || !options.ConstructorMappingEnabled) + return false; + + var ctorMap = new ConstructorMap(destCtor, typeMap); + + foreach (var parameter in ctorParameters) + { + var resolvers = new LinkedList(); + + var canResolve = MapDestinationPropertyToSource(options, sourceTypeInfo, destCtor.DeclaringType, parameter.GetType(), parameter.Name, resolvers); + if(!canResolve && parameter.GetHasDefaultValue()) + { + canResolve = true; + } + + ctorMap.AddParameter(parameter, resolvers.ToArray(), canResolve); + } + + typeMap.ConstructorMap = ctorMap; + + return ctorMap.CanResolve; + } + } +} \ No newline at end of file diff --git a/tools/AutoMapper/TypeMapRegistry.cs b/tools/AutoMapper/TypeMapRegistry.cs new file mode 100644 index 0000000000..8a9be604c0 --- /dev/null +++ b/tools/AutoMapper/TypeMapRegistry.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using AutoMapper.Configuration; + +namespace AutoMapper +{ + public class TypeMapRegistry + { + private readonly IDictionary _typeMaps = new Dictionary(); + + public IEnumerable TypeMaps => _typeMaps.Values; + + public void RegisterTypeMap(TypeMap typeMap) => _typeMaps[typeMap.Types] = typeMap; + + public TypeMap GetTypeMap(TypePair typePair) => _typeMaps.GetOrDefault(typePair); + } +} \ No newline at end of file diff --git a/tools/AutoMapper/TypePair.cs b/tools/AutoMapper/TypePair.cs new file mode 100644 index 0000000000..70ddc3be88 --- /dev/null +++ b/tools/AutoMapper/TypePair.cs @@ -0,0 +1,178 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using AutoMapper.Configuration; + +namespace AutoMapper +{ + [DebuggerDisplay("{RequestedTypes.SourceType.Name}, {RequestedTypes.DestinationType.Name} : {RuntimeTypes.SourceType.Name}, {RuntimeTypes.DestinationType.Name}")] + public struct MapRequest : IEquatable + { + public TypePair RequestedTypes { get; } + public TypePair RuntimeTypes { get; } + public ITypeMapConfiguration InlineConfig { get; } + + public MapRequest(TypePair requestedTypes, TypePair runtimeTypes) + : this(requestedTypes, runtimeTypes, new MapperConfiguration.DefaultTypeMapConfig(requestedTypes)) + { + } + + public MapRequest(TypePair requestedTypes, TypePair runtimeTypes, ITypeMapConfiguration inlineConfig) + { + RequestedTypes = requestedTypes; + RuntimeTypes = runtimeTypes; + InlineConfig = inlineConfig; + } + + public bool Equals(MapRequest other) => RequestedTypes.Equals(other.RequestedTypes) && RuntimeTypes.Equals(other.RuntimeTypes); + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + return obj is MapRequest && Equals((MapRequest) obj); + } + + public override int GetHashCode() => HashCodeCombiner.Combine(RequestedTypes, RuntimeTypes); + + public static bool operator ==(MapRequest left, MapRequest right) => left.Equals(right); + + public static bool operator !=(MapRequest left, MapRequest right) => !left.Equals(right); + } + + [DebuggerDisplay("{SourceType.Name}, {DestinationType.Name}")] + public struct TypePair : IEquatable + { + public static bool operator ==(TypePair left, TypePair right) => left.Equals(right); + public static bool operator !=(TypePair left, TypePair right) => !left.Equals(right); + + public TypePair(Type sourceType, Type destinationType) + { + SourceType = sourceType; + DestinationType = destinationType; + } + + public static TypePair Create(TSource source, Type sourceType, Type destinationType) + { + if(source != null) + { + sourceType = source.GetType(); + } + return new TypePair(sourceType, destinationType); + } + + public static TypePair Create(TSource source, TDestination destination, Type sourceType, Type destinationType) + { + if(source != null) + { + sourceType = source.GetType(); + } + if(destination != null) + { + destinationType = destination.GetType(); + } + return new TypePair(sourceType, destinationType); + } + + public Type SourceType { get; } + + public Type DestinationType { get; } + + public bool Equals(TypePair other) => SourceType == other.SourceType && DestinationType == other.DestinationType; + + public override bool Equals(object other) => other is TypePair && Equals((TypePair)other); + + public override int GetHashCode() => HashCodeCombiner.Combine(SourceType, DestinationType); + + public TypePair? GetOpenGenericTypePair() + { + var isGeneric = SourceType.IsGenericType() || DestinationType.IsGenericType(); + if (!isGeneric) + return null; + + var sourceGenericDefinition = SourceType.IsGenericType() ? SourceType.GetGenericTypeDefinition() : SourceType; + var destGenericDefinition = DestinationType.IsGenericType() ? DestinationType.GetGenericTypeDefinition() : DestinationType; + + var genericTypePair = new TypePair(sourceGenericDefinition, destGenericDefinition); + + return genericTypePair; + } + + public IEnumerable GetRelatedTypePairs() + { + var @this = this; + var subTypePairs = + from destinationType in GetAllTypes(DestinationType) + from sourceType in GetAllTypes(@this.SourceType) + select new TypePair(sourceType, destinationType); + return subTypePairs; + } + + private static IEnumerable GetAllTypes(Type type) + { + var typeInheritance = type.GetTypeInheritance(); + foreach(var item in typeInheritance) + { + yield return item; + } + var interfaceComparer = new InterfaceComparer(type); + var allInterfaces = type.GetTypeInfo().ImplementedInterfaces.OrderByDescending(t => t, interfaceComparer); + foreach(var interfaceType in allInterfaces) + { + yield return interfaceType; + } + } + + private class InterfaceComparer : IComparer + { + private readonly List _typeInheritance; + + public InterfaceComparer(Type target) + { + _typeInheritance = target.GetTypeInheritance().Select(type => type.GetTypeInfo()).Reverse().ToList(); + } + + public int Compare(Type x, Type y) + { + var xLessOrEqualY = x.IsAssignableFrom(y); + var yLessOrEqualX = y.IsAssignableFrom(x); + + if (xLessOrEqualY & !yLessOrEqualX) + { + return -1; + } + if (!xLessOrEqualY & yLessOrEqualX) + { + return 1; + } + if (xLessOrEqualY & yLessOrEqualX) + { + return 0; + } + + var xFirstIntroduceTypeIndex = _typeInheritance.FindIndex(type => type.ImplementedInterfaces.Contains(x)); + var yFirstIntroduceTypeIndex = _typeInheritance.FindIndex(type => type.ImplementedInterfaces.Contains(y)); + + if (xFirstIntroduceTypeIndex < yFirstIntroduceTypeIndex) + { + return -1; + } + if (yFirstIntroduceTypeIndex > xFirstIntroduceTypeIndex) + { + return 1; + } + + return 0; + } + } + } + + public static class HashCodeCombiner + { + public static int Combine(T1 obj1, T2 obj2) => + CombineCodes(obj1.GetHashCode(), obj2.GetHashCode()); + + public static int CombineCodes(int h1, int h2) => ((h1 << 5) + h1) ^ h2; + } +} diff --git a/tools/AutoMapper/ValueResolverConfiguration.cs b/tools/AutoMapper/ValueResolverConfiguration.cs new file mode 100644 index 0000000000..7fc255d1b1 --- /dev/null +++ b/tools/AutoMapper/ValueResolverConfiguration.cs @@ -0,0 +1,26 @@ +using System; +using System.Linq.Expressions; + +namespace AutoMapper +{ + public class ValueResolverConfiguration + { + public object Instance { get; } + public Type ConcreteType { get; } + public Type InterfaceType { get; } + public LambdaExpression SourceMember { get; set; } + public string SourceMemberName { get; set; } + + public ValueResolverConfiguration(Type concreteType, Type interfaceType) + { + ConcreteType = concreteType; + InterfaceType = interfaceType; + } + + public ValueResolverConfiguration(object instance, Type interfaceType) + { + Instance = instance; + InterfaceType = interfaceType; + } + } +} \ No newline at end of file diff --git a/tools/AutoMapper/ValueTransformerConfiguration.cs b/tools/AutoMapper/ValueTransformerConfiguration.cs new file mode 100644 index 0000000000..82f5ec2d35 --- /dev/null +++ b/tools/AutoMapper/ValueTransformerConfiguration.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; + +namespace AutoMapper +{ + public struct ValueTransformerConfiguration + { + public ValueTransformerConfiguration(Type valueType, LambdaExpression transformerExpression) + { + ValueType = valueType; + TransformerExpression = transformerExpression; + } + + public Type ValueType { get; } + public LambdaExpression TransformerExpression { get; } + + public bool IsMatch(PropertyMap propertyMap) + { + return propertyMap.DestinationPropertyType.IsAssignableFrom(ValueType); + } + } + + public static class ValueTransformerConfigurationExtensions + { + /// + /// Apply a transformation function after any resolved destination member value with the given type + /// + /// Value type to match and transform + /// Value transformer list + /// Transformation expression + public static void Add(this IList valueTransformers, + Expression> transformer) + { + var config = new ValueTransformerConfiguration(typeof(TValue), transformer); + + valueTransformers.Add(config); + } + } +} \ No newline at end of file diff --git a/tools/AutoMapper/XpressionMapper/ArgumentMappers/ArgumentMapper.cs b/tools/AutoMapper/XpressionMapper/ArgumentMappers/ArgumentMapper.cs new file mode 100644 index 0000000000..1c2e454ec0 --- /dev/null +++ b/tools/AutoMapper/XpressionMapper/ArgumentMappers/ArgumentMapper.cs @@ -0,0 +1,30 @@ +using System.Linq.Expressions; + +namespace AutoMapper.XpressionMapper.ArgumentMappers +{ + internal abstract class ArgumentMapper + { + protected ArgumentMapper(XpressionMapperVisitor expressionVisitor, Expression argument) + { + ExpressionVisitor = expressionVisitor; + Argument = argument; + } + + protected Expression Argument { get; } + protected virtual XpressionMapperVisitor ExpressionVisitor { get; } + public abstract Expression MappedArgumentExpression { get; } + + public static ArgumentMapper Create(XpressionMapperVisitor expressionVisitor, Expression argument) + { + switch (argument.NodeType) + { + case ExpressionType.Lambda: + return new LambdaArgumentMapper(expressionVisitor, argument); + case ExpressionType.Quote: + return new QuoteArgumentMapper(expressionVisitor, argument); + default: + return new DefaultArgumentMapper(expressionVisitor, argument); + } + } + } +} diff --git a/tools/AutoMapper/XpressionMapper/ArgumentMappers/DefaultArgumentMapper.cs b/tools/AutoMapper/XpressionMapper/ArgumentMappers/DefaultArgumentMapper.cs new file mode 100644 index 0000000000..7ab385a60d --- /dev/null +++ b/tools/AutoMapper/XpressionMapper/ArgumentMappers/DefaultArgumentMapper.cs @@ -0,0 +1,14 @@ +using System.Linq.Expressions; + +namespace AutoMapper.XpressionMapper.ArgumentMappers +{ + internal class DefaultArgumentMapper : ArgumentMapper + { + public DefaultArgumentMapper(XpressionMapperVisitor expressionVisitor, Expression argument) + : base(expressionVisitor, argument) + { + } + + public override Expression MappedArgumentExpression => ExpressionVisitor.Visit(Argument); + } +} diff --git a/tools/AutoMapper/XpressionMapper/ArgumentMappers/LambdaArgumentMapper.cs b/tools/AutoMapper/XpressionMapper/ArgumentMappers/LambdaArgumentMapper.cs new file mode 100644 index 0000000000..944612dc9b --- /dev/null +++ b/tools/AutoMapper/XpressionMapper/ArgumentMappers/LambdaArgumentMapper.cs @@ -0,0 +1,25 @@ +using System.Linq.Expressions; +using AutoMapper.XpressionMapper.Extensions; + +namespace AutoMapper.XpressionMapper.ArgumentMappers +{ + internal class LambdaArgumentMapper : ArgumentMapper + { + public LambdaArgumentMapper(XpressionMapperVisitor expressionVisitor, Expression argument) + : base(expressionVisitor, argument) + { + } + + public override Expression MappedArgumentExpression + { + get + { + var lambdaExpression = (LambdaExpression)Argument; + var ex = ExpressionVisitor.Visit(lambdaExpression.Body); + + var mapped = Expression.Lambda(ex, lambdaExpression.GetDestinationParameterExpressions(ExpressionVisitor.InfoDictionary, ExpressionVisitor.TypeMappings)); + return mapped; + } + } + } +} diff --git a/tools/AutoMapper/XpressionMapper/ArgumentMappers/QuoteArgumentMapper.cs b/tools/AutoMapper/XpressionMapper/ArgumentMappers/QuoteArgumentMapper.cs new file mode 100644 index 0000000000..e08e26c7a7 --- /dev/null +++ b/tools/AutoMapper/XpressionMapper/ArgumentMappers/QuoteArgumentMapper.cs @@ -0,0 +1,25 @@ +using System.Linq.Expressions; +using AutoMapper.XpressionMapper.Extensions; + +namespace AutoMapper.XpressionMapper.ArgumentMappers +{ + internal class QuoteArgumentMapper : ArgumentMapper + { + public QuoteArgumentMapper(XpressionMapperVisitor expressionVisitor, Expression argument) + : base(expressionVisitor, argument) + { + } + + public override Expression MappedArgumentExpression + { + get + { + var lambdaExpression = (LambdaExpression)((UnaryExpression)Argument).Operand; + var ex = ExpressionVisitor.Visit(lambdaExpression.Body); + + var mapped = Expression.Lambda(ex, lambdaExpression.GetDestinationParameterExpressions(ExpressionVisitor.InfoDictionary, ExpressionVisitor.TypeMappings)); + return Expression.Quote(mapped); + } + } + } +} diff --git a/tools/AutoMapper/XpressionMapper/Extensions/MapperExtensions.cs b/tools/AutoMapper/XpressionMapper/Extensions/MapperExtensions.cs new file mode 100644 index 0000000000..7667ac3a11 --- /dev/null +++ b/tools/AutoMapper/XpressionMapper/Extensions/MapperExtensions.cs @@ -0,0 +1,247 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using static System.Linq.Expressions.Expression; +using System.Reflection; +using AutoMapper.Mappers.Internal; + +namespace AutoMapper.XpressionMapper.Extensions +{ + public static class MapperExtensions + { + /// + /// Maps an expression given a dictionary of types where the source type is the key and the destination type is the value. + /// + /// + /// + /// + /// + public static TDestDelegate MapExpression(this IMapper mapper, LambdaExpression expression) + where TDestDelegate : LambdaExpression + { + if (expression == null) + return default(TDestDelegate); + + var typeSourceFunc = expression.GetType().GetGenericArguments()[0]; + var typeDestFunc = typeof(TDestDelegate).GetGenericArguments()[0]; + + var typeMappings = new Dictionary() + .AddTypeMappingsFromDelegates(typeSourceFunc, typeDestFunc); + + var visitor = new XpressionMapperVisitor(mapper == null ? Mapper.Configuration : mapper.ConfigurationProvider, typeMappings); + var remappedBody = visitor.Visit(expression.Body); + if (remappedBody == null) + throw new InvalidOperationException(Resource.cantRemapExpression); + + return (TDestDelegate)Lambda(typeDestFunc, remappedBody, expression.GetDestinationParameterExpressions(visitor.InfoDictionary, typeMappings)); + } + + /// + /// Maps an expression given a dictionary of types where the source type is the key and the destination type is the value. + /// + /// + /// + /// + /// + /// + public static TDestDelegate MapExpression(this IMapper mapper, TSourceDelegate expression) + where TSourceDelegate : LambdaExpression + where TDestDelegate : LambdaExpression + => mapper.MapExpression(expression); + + /// + /// Maps an expression to be used as an "Include" given a dictionary of types where the source type is the key and the destination type is the value. + /// + /// + /// + /// + /// + public static TDestDelegate MapExpressionAsInclude(this IMapper mapper, LambdaExpression expression) + where TDestDelegate : LambdaExpression + { + if (expression == null) + return null; + + var typeSourceFunc = expression.GetType().GetGenericArguments()[0]; + var typeDestFunc = typeof(TDestDelegate).GetGenericArguments()[0]; + + var typeMappings = new Dictionary() + .AddTypeMappingsFromDelegates(typeSourceFunc, typeDestFunc); + + XpressionMapperVisitor visitor = new MapIncludesVisitor(mapper == null ? Mapper.Configuration : mapper.ConfigurationProvider, typeMappings); + var remappedBody = visitor.Visit(expression.Body); + if (remappedBody == null) + throw new InvalidOperationException(Resource.cantRemapExpression); + + return (TDestDelegate)Lambda(typeDestFunc, remappedBody, expression.GetDestinationParameterExpressions(visitor.InfoDictionary, typeMappings)); + } + + /// + /// Maps an expression to be used as an "Include" given a dictionary of types where the source type is the key and the destination type is the value. + /// + /// + /// + /// + /// + /// + public static TDestDelegate MapExpressionAsInclude(this IMapper mapper, TSourceDelegate expression) + where TSourceDelegate : LambdaExpression + where TDestDelegate : LambdaExpression + => mapper.MapExpressionAsInclude(expression); + + /// + /// Maps a collection of expressions given a dictionary of types where the source type is the key and the destination type is the value. + /// + /// + /// + /// + /// + /// + public static ICollection MapExpressionList(this IMapper mapper, ICollection collection) + where TSourceDelegate : LambdaExpression + where TDestDelegate : LambdaExpression + => collection?.Select(mapper.MapExpression).ToList(); + + /// + /// Maps a collection of expressions given a dictionary of types where the source type is the key and the destination type is the value. + /// + /// + /// + /// + /// + public static ICollection MapExpressionList(this IMapper mapper, IEnumerable collection) + where TDestDelegate : LambdaExpression + => collection?.Select(mapper.MapExpression).ToList(); + + /// + /// Maps a collection of expressions to be used as a "Includes" given a dictionary of types where the source type is the key and the destination type is the value. + /// + /// + /// + /// + /// + /// + public static ICollection MapIncludesList(this IMapper mapper, ICollection collection) + where TSourceDelegate : LambdaExpression + where TDestDelegate : LambdaExpression + => collection?.Select(mapper.MapExpressionAsInclude).ToList(); + + /// + /// Maps a collection of expressions to be used as a "Includes" given a dictionary of types where the source type is the key and the destination type is the value. + /// + /// + /// + /// + /// + public static ICollection MapIncludesList(this IMapper mapper, IEnumerable collection) + where TDestDelegate : LambdaExpression + => collection?.Select(mapper.MapExpressionAsInclude).ToList(); + + /// + /// Takes a list of parameters from the source lamda expression and returns a list of parameters for the destination lambda expression. + /// + /// + /// + /// + /// + public static List GetDestinationParameterExpressions(this LambdaExpression expression, MapperInfoDictionary infoDictionary, Dictionary typeMappings) + { + foreach (var p in expression.Parameters.Where(p => !infoDictionary.ContainsKey(p))) + { + infoDictionary.Add(p, typeMappings); + } + + return expression.Parameters.Select(p => infoDictionary[p].NewParameter).ToList(); + } + + /// + /// Adds a new source and destination key-value pair to a dictionary of type mappings based on the generic arguments. + /// + /// + /// + /// + /// + public static Dictionary AddTypeMapping(this Dictionary typeMappings) + => typeMappings == null + ? throw new ArgumentException(Resource.typeMappingsDictionaryIsNull) + : typeMappings.AddTypeMapping(typeof(TSource), typeof(TDest)); + + private static bool HasUnderlyingType(this Type type) + { + return (type.IsGenericType() && typeof(System.Collections.IEnumerable).IsAssignableFrom(type)) || type.IsArray; + } + + private static void AddUnderlyingTypes(this Dictionary typeMappings, Type sourceType, Type destType) + { + var sourceArguments = !sourceType.HasUnderlyingType() + ? new List() + : ElementTypeHelper.GetElementTypes(sourceType).ToList(); + + var destArguments = !destType.HasUnderlyingType() + ? new List() + : ElementTypeHelper.GetElementTypes(destType).ToList(); + + if (sourceArguments.Count != destArguments.Count) + throw new ArgumentException(Resource.invalidArgumentCount); + + sourceArguments.Aggregate(typeMappings, (dic, next) => + { + if (!dic.ContainsKey(next) && next != destArguments[sourceArguments.IndexOf(next)]) + dic.AddTypeMapping(next, destArguments[sourceArguments.IndexOf(next)]); + + return dic; + }); + } + + /// + /// Adds a new source and destination key-value pair to a dictionary of type mappings based on the arguments. + /// + /// + /// + /// + /// + public static Dictionary AddTypeMapping(this Dictionary typeMappings, Type sourceType, Type destType) + { + if (typeMappings == null) + throw new ArgumentException(Resource.typeMappingsDictionaryIsNull); + + if (sourceType.GetTypeInfo().IsGenericType && sourceType.GetGenericTypeDefinition() == typeof(Expression<>)) + { + sourceType = sourceType.GetGenericArguments()[0]; + destType = destType.GetGenericArguments()[0]; + } + + if (!typeMappings.ContainsKey(sourceType) && sourceType != destType) + { + typeMappings.Add(sourceType, destType); + if (typeof(Delegate).IsAssignableFrom(sourceType)) + typeMappings.AddTypeMappingsFromDelegates(sourceType, destType); + else + typeMappings.AddUnderlyingTypes(sourceType, destType); + } + + return typeMappings; + } + + private static Dictionary AddTypeMappingsFromDelegates(this Dictionary typeMappings, Type sourceType, Type destType) + { + if (typeMappings == null) + throw new ArgumentException(Resource.typeMappingsDictionaryIsNull); + + var sourceArguments = sourceType.GetGenericArguments().ToList(); + var destArguments = destType.GetGenericArguments().ToList(); + + if (sourceArguments.Count != destArguments.Count) + throw new ArgumentException(Resource.invalidArgumentCount); + + return sourceArguments.Aggregate(typeMappings, (dic, next) => + { + if (!dic.ContainsKey(next) && next != destArguments[sourceArguments.IndexOf(next)]) + dic.AddTypeMapping(next, destArguments[sourceArguments.IndexOf(next)]); + + return dic; + }); + } + } +} diff --git a/tools/AutoMapper/XpressionMapper/Extensions/VisitorExtensions.cs b/tools/AutoMapper/XpressionMapper/Extensions/VisitorExtensions.cs new file mode 100644 index 0000000000..2a7676f961 --- /dev/null +++ b/tools/AutoMapper/XpressionMapper/Extensions/VisitorExtensions.cs @@ -0,0 +1,185 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Runtime.CompilerServices; +using AutoMapper.XpressionMapper.Structures; + +namespace AutoMapper.XpressionMapper.Extensions +{ + using AutoMapper.Internal; + using Configuration; + + internal static class VisitorExtensions + { + /// + /// Returns true if the expression is a direct or descendant member expression of the parameter. + /// + /// + /// + public static bool IsMemberExpression(this Expression expression) + { + if (expression.NodeType == ExpressionType.MemberAccess) + { + var memberExpression = (MemberExpression)expression; + return IsMemberOrParameterExpression(memberExpression.Expression); + } + + return false; + } + + private static bool IsMemberOrParameterExpression(Expression expression) + { + //the node represents parameter of the expression + switch (expression.NodeType) + { + case ExpressionType.Parameter: + return true; + case ExpressionType.MemberAccess: + var memberExpression = (MemberExpression)expression; + return IsMemberOrParameterExpression(memberExpression.Expression); + } + + return false; + } + + /// + /// Returns the fully qualified name of the member starting with the immediate child member of the parameter + /// + /// + /// + public static string GetPropertyFullName(this Expression expression) + { + const string period = "."; + + //the node represents parameter of the expression + switch (expression.NodeType) + { + case ExpressionType.Parameter: + return string.Empty; + case ExpressionType.MemberAccess: + var memberExpression = (MemberExpression)expression; + var parentFullName = memberExpression.Expression.GetPropertyFullName(); + return string.IsNullOrEmpty(parentFullName) + ? memberExpression.Member.Name + : string.Concat(memberExpression.Expression.GetPropertyFullName(), period, memberExpression.Member.Name); + } + + throw new InvalidOperationException(Resource.invalidExpErr); + } + + private static MemberExpression GetMemberExpression(LambdaExpression expr) + { + MemberExpression me; + switch (expr.Body.NodeType) + { + case ExpressionType.Convert: + case ExpressionType.ConvertChecked: + var ue = expr.Body as UnaryExpression; + me = ue?.Operand as MemberExpression; + break; + default: + me = expr.Body as MemberExpression; + if (me == null) + { + if (expr.Body is BinaryExpression binaryExpression) + { + if (binaryExpression.Left is MemberExpression left) + return left; + if (binaryExpression.Right is MemberExpression right) + return right; + } + } + break; + } + + return me; + } + + /// + /// Returns the ParameterExpression for the LINQ parameter. + /// + /// + /// + public static ParameterExpression GetParameterExpression(this Expression expression) + { + if (expression == null) + return null; + + //the node represents parameter of the expression + switch (expression.NodeType) + { + case ExpressionType.Parameter: + return (ParameterExpression)expression; + case ExpressionType.Quote: + return GetParameterExpression(GetMemberExpression((LambdaExpression)((UnaryExpression)expression).Operand)); + case ExpressionType.Lambda: + return GetParameterExpression(GetMemberExpression((LambdaExpression)expression)); + case ExpressionType.ConvertChecked: + case ExpressionType.Convert: + var ue = expression as UnaryExpression; + return GetParameterExpression(ue?.Operand); + case ExpressionType.MemberAccess: + return GetParameterExpression(((MemberExpression)expression).Expression); + case ExpressionType.Call: + var methodExpression = expression as MethodCallExpression; + var memberExpression = methodExpression?.Object as MemberExpression;//Method is an instance method + + var isExtension = methodExpression != null && methodExpression.Method.IsDefined(typeof(ExtensionAttribute), true); + if (isExtension && memberExpression == null && methodExpression.Arguments.Count > 0) + memberExpression = methodExpression.Arguments[0] as MemberExpression;//Method is an extension method based on the type of methodExpression.Arguments[0] and methodExpression.Arguments[0] is a member expression. + + return isExtension && memberExpression == null && methodExpression.Arguments.Count > 0 + ? GetParameterExpression(methodExpression.Arguments[0]) + : (memberExpression == null ? null : GetParameterExpression(memberExpression.Expression)); + } + + return null; + } + + /// + /// Adds member expressions to an existing expression. + /// + /// + /// + /// + public static MemberExpression MemberAccesses(this Expression exp, List list) => + (MemberExpression) list.SelectMany(propertyMapInfo => propertyMapInfo.DestinationPropertyInfos).MemberAccesses(exp); + + /// + /// For the given a Lambda Expression, returns the fully qualified name of the member starting with the immediate child member of the parameter + /// + /// + /// + public static string GetMemberFullName(this LambdaExpression expr) + { + if (expr.Body.NodeType == ExpressionType.Parameter) + return string.Empty; + + MemberExpression me; + switch (expr.Body.NodeType) + { + case ExpressionType.Convert: + case ExpressionType.ConvertChecked: + me = (expr.Body as UnaryExpression)?.Operand as MemberExpression; + break; + default: + me = expr.Body as MemberExpression; + break; + } + + return me.GetPropertyFullName(); + } + + /// + /// Returns the underlying type typeof(T) when the type implements IEnumerable. + /// + /// + /// + public static List GetUnderlyingGenericTypes(this Type type) => + type == null || !type.GetTypeInfo().IsGenericType + ? new List() + : type.GetGenericArguments().ToList(); + } +} diff --git a/tools/AutoMapper/XpressionMapper/FindMemberExpressionsVisitor.cs b/tools/AutoMapper/XpressionMapper/FindMemberExpressionsVisitor.cs new file mode 100644 index 0000000000..2142034a69 --- /dev/null +++ b/tools/AutoMapper/XpressionMapper/FindMemberExpressionsVisitor.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using AutoMapper.Internal; +using AutoMapper.XpressionMapper.Extensions; + +namespace AutoMapper.XpressionMapper +{ + internal class FindMemberExpressionsVisitor : ExpressionVisitor + { + internal FindMemberExpressionsVisitor(ParameterExpression newParameter) => _newParameter = newParameter; + + private readonly ParameterExpression _newParameter; + private readonly List _memberExpressions = new List(); + + public MemberExpression Result + { + get + { + const string period = "."; + var fullNamesGrouped = _memberExpressions.Select(m => m.GetPropertyFullName()) + .GroupBy(n => n) + .Select(grp => grp.Key) + .OrderBy(a => a.Length).ToList(); + + var member = fullNamesGrouped.Aggregate(string.Empty, (result, next) => + { + if (string.IsNullOrEmpty(result) || next.Contains(result)) + result = next; + else throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, + Resource.includeExpressionTooComplex, + string.Concat(_newParameter.Type.Name, period, result), + string.Concat(_newParameter.Type.Name, period, next))); + + return result; + }); + + return ExpressionFactory.MemberAccesses(member, _newParameter); + } + } + + protected override Expression VisitMember(MemberExpression node) + { + var parameterExpression = node.GetParameterExpression(); + var sType = parameterExpression?.Type; + if (sType != null && _newParameter.Type == sType && node.IsMemberExpression()) + { + 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)); + else + _memberExpressions.Add(node); + } + + return base.VisitMember(node); + } + } +} diff --git a/tools/AutoMapper/XpressionMapper/MapIncludesVisitor.cs b/tools/AutoMapper/XpressionMapper/MapIncludesVisitor.cs new file mode 100644 index 0000000000..61f67a48cb --- /dev/null +++ b/tools/AutoMapper/XpressionMapper/MapIncludesVisitor.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using AutoMapper.Internal; +using AutoMapper.XpressionMapper.Extensions; +using AutoMapper.XpressionMapper.Structures; + +namespace AutoMapper.XpressionMapper +{ + public class MapIncludesVisitor : XpressionMapperVisitor + { + public MapIncludesVisitor(IConfigurationProvider configurationProvider, Dictionary typeMappings) + : base(configurationProvider, typeMappings) + { + } + + protected override Expression VisitUnary(UnaryExpression node) + { + switch (node.NodeType) + { + case ExpressionType.Convert: + case ExpressionType.ConvertChecked: + + var me = node.Operand as MemberExpression; + var parameterExpression = node.GetParameterExpression(); + var sType = parameterExpression?.Type; + if (me != null && (sType != null && me.Expression.NodeType == ExpressionType.MemberAccess && me.Type.IsLiteralType())) + { + //just pass me and let the FindMemberExpressionsVisitor handle removing of the value type + //me.Expression will not match the PathMap name. + return Visit(me); + } + else + { + return base.VisitUnary(node); + } + default: + return base.VisitUnary(node); + } + } + + protected override Expression VisitMember(MemberExpression node) + { + string sourcePath; + + var parameterExpression = node.GetParameterExpression(); + if (parameterExpression == null) + return base.VisitMember(node); + + InfoDictionary.Add(parameterExpression, TypeMappings); + var sType = parameterExpression.Type; + if (sType != null && InfoDictionary.ContainsKey(parameterExpression) && node.IsMemberExpression()) + { + sourcePath = node.GetPropertyFullName(); + } + else + { + return base.VisitMember(node); + } + + var propertyMapInfoList = new List(); + FindDestinationFullName(sType, InfoDictionary[parameterExpression].DestType, sourcePath, propertyMapInfoList); + string fullName; + + if (propertyMapInfoList.Any(x => x.CustomExpression != null))//CustomExpression takes precedence over DestinationPropertyInfo + { + var last = propertyMapInfoList.Last(x => x.CustomExpression != null); + var beforeCustExpression = propertyMapInfoList.Aggregate(new List(), (list, next) => + { + if (propertyMapInfoList.IndexOf(next) < propertyMapInfoList.IndexOf(last)) + list.Add(next); + return list; + }); + + var afterCustExpression = propertyMapInfoList.Aggregate(new List(), (list, next) => + { + if (propertyMapInfoList.IndexOf(next) > propertyMapInfoList.IndexOf(last)) + list.Add(next); + return list; + }); + + + fullName = BuildFullName(beforeCustExpression); + + var visitor = new PrependParentNameVisitor(last.CustomExpression.Parameters[0].Type/*Parent type of current property*/, fullName, InfoDictionary[parameterExpression].NewParameter); + + var ex = propertyMapInfoList[propertyMapInfoList.Count - 1] != last + ? visitor.Visit(last.CustomExpression.Body.MemberAccesses(afterCustExpression)) + : visitor.Visit(last.CustomExpression.Body); + + var v = new FindMemberExpressionsVisitor(InfoDictionary[parameterExpression].NewParameter); + v.Visit(ex); + + return v.Result; + } + fullName = BuildFullName(propertyMapInfoList); + var me = ExpressionFactory.MemberAccesses(fullName, InfoDictionary[parameterExpression].NewParameter); + if (me.Expression.NodeType == ExpressionType.MemberAccess && (me.Type == typeof(string) || me.Type.GetTypeInfo().IsValueType || (me.Type.GetTypeInfo().IsGenericType + && me.Type.GetGenericTypeDefinition() == typeof(Nullable<>) + && Nullable.GetUnderlyingType(me.Type).GetTypeInfo().IsValueType))) + { + return me.Expression; + } + + return me; + } + } +} diff --git a/tools/AutoMapper/XpressionMapper/MapperInfoDictionary.cs b/tools/AutoMapper/XpressionMapper/MapperInfoDictionary.cs new file mode 100644 index 0000000000..992db8bead --- /dev/null +++ b/tools/AutoMapper/XpressionMapper/MapperInfoDictionary.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using AutoMapper.XpressionMapper.Structures; + +namespace AutoMapper.XpressionMapper +{ + public class MapperInfoDictionary : Dictionary + { + public MapperInfoDictionary(ParameterExpressionEqualityComparer comparer) : base(comparer) + { + } + + //const string PREFIX = "p"; + public void Add(ParameterExpression key, Dictionary typeMappings) + { + if (ContainsKey(key)) + return; + + Add(key, typeMappings.ContainsKey(key.Type) + ? new MapperInfo(Expression.Parameter(typeMappings[key.Type], key.Name), key.Type,typeMappings[key.Type]) + : new MapperInfo(Expression.Parameter(key.Type, key.Name), key.Type, key.Type)); + } + } +} diff --git a/tools/AutoMapper/XpressionMapper/ParameterExpressionEqualityComparer.cs b/tools/AutoMapper/XpressionMapper/ParameterExpressionEqualityComparer.cs new file mode 100644 index 0000000000..5c7c4072af --- /dev/null +++ b/tools/AutoMapper/XpressionMapper/ParameterExpressionEqualityComparer.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using System.Linq.Expressions; + +namespace AutoMapper.XpressionMapper +{ + public class ParameterExpressionEqualityComparer : IEqualityComparer + { + public bool Equals(ParameterExpression x, ParameterExpression y) => ReferenceEquals(x, y); + + public int GetHashCode(ParameterExpression obj) => obj.GetHashCode(); + } +} diff --git a/tools/AutoMapper/XpressionMapper/PrependParentNameVisitor.cs b/tools/AutoMapper/XpressionMapper/PrependParentNameVisitor.cs new file mode 100644 index 0000000000..9fc87508e5 --- /dev/null +++ b/tools/AutoMapper/XpressionMapper/PrependParentNameVisitor.cs @@ -0,0 +1,48 @@ +using System; +using System.Linq.Expressions; +using AutoMapper.Internal; +using AutoMapper.XpressionMapper.Extensions; + +namespace AutoMapper.XpressionMapper +{ + internal class PrependParentNameVisitor : ExpressionVisitor + { + public PrependParentNameVisitor(Type currentParameterType, string parentFullName, ParameterExpression newParameter) + { + CurrentParameterType = currentParameterType; + ParentFullName = parentFullName; + NewParameter = newParameter; + } + + public Type CurrentParameterType { get; } + public string ParentFullName { get; } + public ParameterExpression NewParameter { get; } + + protected override Expression VisitMember(MemberExpression node) + { + if (node.NodeType == ExpressionType.Constant) + return base.VisitMember(node); + + string sourcePath; + + var parameterExpression = node.GetParameterExpression(); + var sType = parameterExpression?.Type; + if (sType != null && sType == CurrentParameterType && node.IsMemberExpression()) + { + sourcePath = node.GetPropertyFullName(); + } + else + { + return base.VisitMember(node); + } + + var fullName = string.IsNullOrEmpty(ParentFullName) + ? sourcePath + : string.Concat(ParentFullName, ".", sourcePath); + + var me = ExpressionFactory.MemberAccesses(fullName, NewParameter); + + return me; + } + } +} diff --git a/tools/AutoMapper/XpressionMapper/Resource.Designer.cs b/tools/AutoMapper/XpressionMapper/Resource.Designer.cs new file mode 100644 index 0000000000..4d59a7b05e --- /dev/null +++ b/tools/AutoMapper/XpressionMapper/Resource.Designer.cs @@ -0,0 +1,172 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace AutoMapper.XpressionMapper { + using System; + using System.Reflection; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // 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.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resource { + + 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() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("AutoMapper.XpressionMapper.Resource", typeof(Resource).GetTypeInfo().Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Cannot create a binary expression for the following pair. Node: {0}, Type: {1} and Node: {2}, Type: {3}.. + /// + internal static string cannotCreateBinaryExpressionFormat { + get { + return ResourceManager.GetString("cannotCreateBinaryExpressionFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Can't rempa expression. + /// + internal static string cantRemapExpression { + get { + return ResourceManager.GetString("cantRemapExpression", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Custom resolvers are not supported for expression mapping.. + /// + internal static string customResolversNotSupported { + get { + return ResourceManager.GetString("customResolversNotSupported", resourceCulture); + } + } + + /// + /// 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}.. + /// + internal static string expressionMapValueTypeMustMatchFormat { + get { + return ResourceManager.GetString("expressionMapValueTypeMustMatchFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The Include value-type expression uses multiple sibling navigation objects "{0}", "{1}" and is too complex to translate.. + /// + internal static string includeExpressionTooComplex { + get { + return ResourceManager.GetString("includeExpressionTooComplex", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Source and destination must have the same number of arguments.. + /// + internal static string invalidArgumentCount { + get { + return ResourceManager.GetString("invalidArgumentCount", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid expression type for this operation.. + /// + internal static string invalidExpErr { + get { + return ResourceManager.GetString("invalidExpErr", 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.. + /// + internal static string mappedMemberIsChildOfTheParameterFormat { + get { + return ResourceManager.GetString("mappedMemberIsChildOfTheParameterFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Mapper Info dictionary cannot be null.. + /// + internal static string mapperInfoDictionaryIsNull { + get { + return ResourceManager.GetString("mapperInfoDictionaryIsNull", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Arguments must be expressions.. + /// + internal static string mustBeExpressions { + get { + return ResourceManager.GetString("mustBeExpressions", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to SourceMember cannot be null. Source Type: {0}, Destination Type: {1}, Property: {2}.. + /// + internal static string srcMemberCannotBeNullFormat { + get { + return ResourceManager.GetString("srcMemberCannotBeNullFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Type Mappings dictionary cannot be null.. + /// + internal static string typeMappingsDictionaryIsNull { + get { + return ResourceManager.GetString("typeMappingsDictionaryIsNull", resourceCulture); + } + } + } +} diff --git a/tools/AutoMapper/XpressionMapper/Resource.resx b/tools/AutoMapper/XpressionMapper/Resource.resx new file mode 100644 index 0000000000..70a1c6f2b9 --- /dev/null +++ b/tools/AutoMapper/XpressionMapper/Resource.resx @@ -0,0 +1,161 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 value 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/tools/AutoMapper/XpressionMapper/Structures/MapperInfo.cs b/tools/AutoMapper/XpressionMapper/Structures/MapperInfo.cs new file mode 100644 index 0000000000..598de859eb --- /dev/null +++ b/tools/AutoMapper/XpressionMapper/Structures/MapperInfo.cs @@ -0,0 +1,24 @@ +using System; +using System.Linq.Expressions; + +namespace AutoMapper.XpressionMapper.Structures +{ + public class MapperInfo + { + public MapperInfo() + { + + } + + public MapperInfo(ParameterExpression newParameter, Type sourceType, Type destType) + { + NewParameter = newParameter; + SourceType = sourceType; + DestType = destType; + } + + public Type SourceType { get; set; } + public Type DestType { get; set; } + public ParameterExpression NewParameter { get; set; } + } +} diff --git a/tools/AutoMapper/XpressionMapper/Structures/PropertyMapInfo.cs b/tools/AutoMapper/XpressionMapper/Structures/PropertyMapInfo.cs new file mode 100644 index 0000000000..1c64fb2b39 --- /dev/null +++ b/tools/AutoMapper/XpressionMapper/Structures/PropertyMapInfo.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Reflection; + +namespace AutoMapper.XpressionMapper.Structures +{ + public class PropertyMapInfo + { + public PropertyMapInfo(LambdaExpression customExpression, List destinationPropertyInfos) + { + CustomExpression = customExpression; + DestinationPropertyInfos = destinationPropertyInfos; + } + + public LambdaExpression CustomExpression { get; set; } + public List DestinationPropertyInfos { get; set; } + } +} diff --git a/tools/AutoMapper/XpressionMapper/XpressionMapperVisitor.cs b/tools/AutoMapper/XpressionMapper/XpressionMapperVisitor.cs new file mode 100644 index 0000000000..71e42b6dc4 --- /dev/null +++ b/tools/AutoMapper/XpressionMapper/XpressionMapperVisitor.cs @@ -0,0 +1,267 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; +using AutoMapper.Configuration; +using AutoMapper.Internal; +using AutoMapper.QueryableExtensions.Impl; +using AutoMapper.XpressionMapper.ArgumentMappers; +using AutoMapper.XpressionMapper.Extensions; +using AutoMapper.XpressionMapper.Structures; + +namespace AutoMapper.XpressionMapper +{ + public class XpressionMapperVisitor : ExpressionVisitor + { + public XpressionMapperVisitor(IConfigurationProvider configurationProvider, Dictionary typeMappings) + { + TypeMappings = typeMappings; + InfoDictionary = new MapperInfoDictionary(new ParameterExpressionEqualityComparer()); + ConfigurationProvider = configurationProvider; + } + + public MapperInfoDictionary InfoDictionary { get; } + + public Dictionary TypeMappings { get; } + + protected IConfigurationProvider ConfigurationProvider { get; } + + protected override Expression VisitParameter(ParameterExpression parameterExpression) + { + InfoDictionary.Add(parameterExpression, TypeMappings); + var pair = InfoDictionary.SingleOrDefault(a => a.Key.Equals(parameterExpression)); + return !pair.Equals(default(KeyValuePair)) ? pair.Value.NewParameter : base.VisitParameter(parameterExpression); + } + + protected override Expression VisitMember(MemberExpression node) + { + string sourcePath; + + var parameterExpression = node.GetParameterExpression(); + if (parameterExpression == null) + return base.VisitMember(node); + + InfoDictionary.Add(parameterExpression, TypeMappings); + + var sType = parameterExpression.Type; + if (InfoDictionary.ContainsKey(parameterExpression) && node.IsMemberExpression()) + { + sourcePath = node.GetPropertyFullName(); + } + else + { + return base.VisitMember(node); + } + + var propertyMapInfoList = new List(); + FindDestinationFullName(sType, InfoDictionary[parameterExpression].DestType, sourcePath, propertyMapInfoList); + string fullName; + + if (propertyMapInfoList.Any(x => x.CustomExpression != null)) + { + var last = propertyMapInfoList.Last(x => x.CustomExpression != null); + var beforeCustExpression = propertyMapInfoList.Aggregate(new List(), (list, next) => + { + if (propertyMapInfoList.IndexOf(next) < propertyMapInfoList.IndexOf(last)) + list.Add(next); + return list; + }); + + var afterCustExpression = propertyMapInfoList.Aggregate(new List(), (list, next) => + { + if (propertyMapInfoList.IndexOf(next) > propertyMapInfoList.IndexOf(last)) + list.Add(next); + return list; + }); + + fullName = BuildFullName(beforeCustExpression); + var visitor = new PrependParentNameVisitor(last.CustomExpression.Parameters[0].Type/*Parent type of current property*/, fullName, InfoDictionary[parameterExpression].NewParameter); + + var ex = propertyMapInfoList[propertyMapInfoList.Count - 1] != last + ? visitor.Visit(last.CustomExpression.Body.MemberAccesses(afterCustExpression)) + : visitor.Visit(last.CustomExpression.Body); + + this.TypeMappings.AddTypeMapping(node.Type, ex.Type); + return ex; + } + fullName = BuildFullName(propertyMapInfoList); + var me = ExpressionFactory.MemberAccesses(fullName, InfoDictionary[parameterExpression].NewParameter); + + this.TypeMappings.AddTypeMapping(node.Type, me.Type); + return me; + } + + protected override Expression VisitConstant(ConstantExpression node) + { + if (this.TypeMappings.TryGetValue(node.Type, out Type newType)) + return base.VisitConstant(Expression.Constant(node.Value, newType)); + + return base.VisitConstant(node); + } + + protected override Expression VisitMethodCall(MethodCallExpression node) + { + var parameterExpression = node.GetParameterExpression(); + if (parameterExpression == null) + return base.VisitMethodCall(node); + + InfoDictionary.Add(parameterExpression, TypeMappings); + + var listOfArgumentsForNewMethod = node.Arguments.Aggregate(new List(), (lst, next) => + { + var mappedNext = ArgumentMapper.Create(this, next).MappedArgumentExpression; + TypeMappings.AddTypeMapping(next.Type, mappedNext.Type); + + lst.Add(mappedNext); + return lst; + });//Arguments could be expressions or other objects. e.g. s => s.UserId or a string "ZZZ". For extention methods node.Arguments[0] is usually the helper object itself + + //type args are the generic type args e.g. T1 and T2 MethodName(method arguments); + var typeArgsForNewMethod = node.Method.IsGenericMethod + ? node.Method.GetGenericArguments().Select(i => TypeMappings.ContainsKey(i) ? TypeMappings[i] : i).ToList()//not converting the type it is not in the typeMappings dictionary + : null; + + MethodCallExpression resultExp; + if (!node.Method.IsStatic) + { + var instance = ArgumentMapper.Create(this, node.Object).MappedArgumentExpression; + + resultExp = node.Method.IsGenericMethod + ? Expression.Call(instance, node.Method.Name, typeArgsForNewMethod.ToArray(), listOfArgumentsForNewMethod.ToArray()) + : Expression.Call(instance, node.Method, listOfArgumentsForNewMethod.ToArray()); + } + else + { + resultExp = node.Method.IsGenericMethod + ? Expression.Call(node.Method.DeclaringType, node.Method.Name, typeArgsForNewMethod.ToArray(), listOfArgumentsForNewMethod.ToArray()) + : Expression.Call(node.Method, listOfArgumentsForNewMethod.ToArray()); + } + + return resultExp; + } + + protected string BuildFullName(List propertyMapInfoList) + { + var fullName = string.Empty; + foreach (var info in propertyMapInfoList) + { + if (info.CustomExpression != null) + { + fullName = string.IsNullOrEmpty(fullName) + ? info.CustomExpression.GetMemberFullName() + : string.Concat(fullName, ".", info.CustomExpression.GetMemberFullName()); + } + else + { + var additions = info.DestinationPropertyInfos.Aggregate(new StringBuilder(fullName), (sb, next) => + { + if (sb.ToString() == string.Empty) + sb.Append(next.Name); + else + { + sb.Append("."); + sb.Append(next.Name); + } + return sb; + }); + + fullName = additions.ToString(); + } + } + + return fullName; + } + + private static void AddPropertyMapInfo(Type parentType, string name, List propertyMapInfoList) + { + var sourceMemberInfo = parentType.GetFieldOrProperty(name); + switch (sourceMemberInfo) + { + case PropertyInfo propertyInfo: + propertyMapInfoList.Add(new PropertyMapInfo(null, new List {propertyInfo})); + break; + case FieldInfo fieldInfo: + propertyMapInfoList.Add(new PropertyMapInfo(null, new List {fieldInfo})); + break; + } + } + + protected void FindDestinationFullName(Type typeSource, Type typeDestination, string sourceFullName, List propertyMapInfoList) + { + const string period = "."; + if (typeSource == typeDestination) + { + var sourceFullNameArray = sourceFullName.Split(new[] { period[0] }, StringSplitOptions.RemoveEmptyEntries); + sourceFullNameArray.Aggregate(propertyMapInfoList, (list, next) => + { + + if (list.Count == 0) + { + AddPropertyMapInfo(typeSource, next, list); + } + else + { + var last = list[list.Count - 1]; + AddPropertyMapInfo(last.CustomExpression == null + ? last.DestinationPropertyInfos[last.DestinationPropertyInfos.Count - 1].GetMemberType() + : last.CustomExpression.ReturnType, next, list); + } + return list; + }); + return; + } + + var typeMap = ConfigurationProvider.CheckIfMapExists(sourceType: typeDestination, destinationType: typeSource);//The destination becomes the source because to map a source expression to a destination expression, + //we need the expressions used to create the source from the destination + + PathMap pathMap = typeMap.FindPathMapByDestinationPath(destinationFullPath: sourceFullName); + if (pathMap != null) + { + propertyMapInfoList.Add(new PropertyMapInfo(pathMap.SourceExpression, new List())); + return; + } + + + if (sourceFullName.IndexOf(period, StringComparison.OrdinalIgnoreCase) < 0) + { + var propertyMap = typeMap.GetPropertyMapByDestinationProperty(sourceFullName); + var sourceMemberInfo = typeSource.GetFieldOrProperty(propertyMap.DestinationProperty.Name); + if (propertyMap.ValueResolverConfig != null) + { + throw new InvalidOperationException(Resource.customResolversNotSupported); + } + if (propertyMap.CustomExpression != null) + { + if (propertyMap.CustomExpression.ReturnType.IsValueType() && sourceMemberInfo.GetMemberType() != propertyMap.CustomExpression.ReturnType) + throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Resource.expressionMapValueTypeMustMatchFormat, propertyMap.CustomExpression.ReturnType.Name, propertyMap.CustomExpression.ToString(), sourceMemberInfo.GetMemberType().Name, propertyMap.DestinationProperty.Name)); + } + else + { + if (propertyMap.SourceMember.GetMemberType().IsValueType() && sourceMemberInfo.GetMemberType() != propertyMap.SourceMember.GetMemberType()) + throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Resource.expressionMapValueTypeMustMatchFormat, propertyMap.SourceMember.GetMemberType().Name, propertyMap.SourceMember.Name, sourceMemberInfo.GetMemberType().Name, propertyMap.DestinationProperty.Name)); + } + + propertyMapInfoList.Add(new PropertyMapInfo(propertyMap.CustomExpression, propertyMap.SourceMembers.ToList())); + } + else + { + var propertyName = sourceFullName.Substring(0, sourceFullName.IndexOf(period, StringComparison.OrdinalIgnoreCase)); + var propertyMap = typeMap.GetPropertyMapByDestinationProperty(propertyName); + + var sourceMemberInfo = typeSource.GetFieldOrProperty(propertyMap.DestinationProperty.Name); + if (propertyMap.CustomExpression == null && propertyMap.SourceMember == null)//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)); + + propertyMapInfoList.Add(new PropertyMapInfo(propertyMap.CustomExpression, propertyMap.SourceMembers.ToList())); + var childFullName = sourceFullName.Substring(sourceFullName.IndexOf(period, StringComparison.OrdinalIgnoreCase) + 1); + + FindDestinationFullName(sourceMemberInfo.GetMemberType(), propertyMap.CustomExpression == null + ? propertyMap.SourceMember.GetMemberType() + : propertyMap.CustomExpression.ReturnType, childFullName, propertyMapInfoList); + } + } + } +} \ No newline at end of file diff --git a/tools/AutoMapper/licenses/LICENSE.txt b/tools/AutoMapper/licenses/LICENSE.txt new file mode 100644 index 0000000000..377ac6309e --- /dev/null +++ b/tools/AutoMapper/licenses/LICENSE.txt @@ -0,0 +1,9 @@ +The MIT License (MIT) + +Copyright (c) 2010 Jimmy Bogard + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file