├── .gitattributes ├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .nuget ├── NuGet.Config ├── NuGet.exe └── NuGet.targets ├── AutoMapper.Collection.sln ├── Directory.Build.props ├── LICENSE ├── Push.ps1 ├── README.md ├── build.ps1 ├── global.json ├── icon.png ├── src ├── AutoMapper.Collection.EntityFramework.Tests │ ├── AutoMapper.Collection.EntityFramework.Tests.csproj │ ├── EntityFramworkTests.cs │ └── MappingTestBase.cs ├── AutoMapper.Collection.EntityFramework │ ├── AutoMapper.Collection.EntityFramework.csproj │ ├── Extensions.cs │ ├── GenerateEntityFrameworkPrimaryKeyPropertyMaps.cs │ ├── IPersistence.cs │ └── Persistence.cs ├── AutoMapper.Collection.LinqToSQL │ ├── AutoMapper.Collection.LinqToSQL.csproj │ ├── GetLinqToSQLPrimaryKeyProperties.cs │ ├── IPersistence.cs │ ├── Persistence.cs │ └── PersistenceExtensions.cs ├── AutoMapper.Collection.Tests │ ├── AutoMapper.Collection.Tests.csproj │ ├── InheritanceWithCollectionTests.cs │ ├── MapCollectionWithEqualityTests.cs │ ├── MapCollectionWithEqualityThreadSafetyTests.cs │ ├── MappingTestBase.cs │ ├── NullableIdTests.cs │ ├── OptionsTests.cs │ └── ValueTypeTests.cs ├── AutoMapper.Collection │ ├── AutoMapper.Collection.csproj │ ├── Configuration │ │ ├── CollectionMappingExpressionFeature.cs │ │ └── GeneratePropertyMapsExpressionFeature.cs │ ├── EquivalencyExpression │ │ ├── CustomExpressionVisitor.cs │ │ ├── EquivalentExpression.cs │ │ ├── EquivalentExpressions.cs │ │ ├── ExpressionExtentions.cs │ │ ├── GenerateEquivilentExpressionFromTypeMap.cs │ │ ├── HashableExpressionsVisitor.cs │ │ ├── IEquivalentComparer.cs │ │ └── IGeneratePropertyMaps.cs │ ├── Mappers │ │ ├── EquivalentExpressionAddRemoveCollectionMapper.cs │ │ ├── IConfigurationObjectMapper.cs │ │ └── ObjectToEquivalencyExpressionByEquivalencyExistingMapper.cs │ ├── PrimitiveExtensions.cs │ ├── ReflectionHelper.cs │ ├── Runtime │ │ ├── CollectionMappingFeature.cs │ │ └── GeneratePropertyMapsFeature.cs │ ├── TypeExtensions.cs │ └── TypeHelper.cs └── Key.snk └── version.props /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | jobs: 11 | build: 12 | strategy: 13 | fail-fast: false 14 | runs-on: windows-2022 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 0 20 | - name: Build and Test 21 | run: ./Build.ps1 22 | shell: pwsh 23 | - name: Push to MyGet 24 | env: 25 | NUGET_URL: https://www.myget.org/F/automapperdev/api/v3/index.json 26 | NUGET_API_KEY: ${{ secrets.MYGET_CI_API_KEY }} 27 | run: ./Push.ps1 28 | shell: pwsh 29 | - name: Artifacts 30 | uses: actions/upload-artifact@v4 31 | with: 32 | name: artifacts 33 | path: artifacts/**/* 34 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*.*.*' 7 | jobs: 8 | build: 9 | strategy: 10 | fail-fast: false 11 | runs-on: windows-2022 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | - name: Build and Test 18 | run: ./Build.ps1 19 | shell: pwsh 20 | - name: Push to MyGet 21 | env: 22 | NUGET_URL: https://www.myget.org/F/automapperdev/api/v3/index.json 23 | NUGET_API_KEY: ${{ secrets.MYGET_CI_API_KEY }} 24 | run: ./Push.ps1 25 | shell: pwsh 26 | - name: Push to NuGet 27 | env: 28 | NUGET_URL: https://api.nuget.org/v3/index.json 29 | NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} 30 | run: ./Push.ps1 31 | shell: pwsh 32 | - name: Artifacts 33 | uses: actions/upload-artifact@v4 34 | with: 35 | name: artifacts 36 | path: artifacts/**/* 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.sln.docstates 8 | 9 | # Build results 10 | [Dd]ebug/ 11 | [Dd]ebugPublic/ 12 | [Rr]elease/ 13 | x64/ 14 | build/ 15 | bld/ 16 | [Bb]in/ 17 | [Oo]bj/ 18 | 19 | # Roslyn cache directories 20 | *.ide/ 21 | 22 | # MSTest test Results 23 | [Tt]est[Rr]esult*/ 24 | [Bb]uild[Ll]og.* 25 | 26 | #NUNIT 27 | *.VisualState.xml 28 | TestResult.xml 29 | 30 | # Build Results of an ATL Project 31 | [Dd]ebugPS/ 32 | [Rr]eleasePS/ 33 | dlldata.c 34 | 35 | *_i.c 36 | *_p.c 37 | *_i.h 38 | *.ilk 39 | *.meta 40 | *.obj 41 | *.pch 42 | *.pdb 43 | *.pgc 44 | *.pgd 45 | *.rsp 46 | *.sbr 47 | *.tlb 48 | *.tli 49 | *.tlh 50 | *.tmp 51 | *.tmp_proj 52 | *.log 53 | *.vspscc 54 | *.vssscc 55 | .builds 56 | *.pidb 57 | *.svclog 58 | *.scc 59 | 60 | # Chutzpah Test files 61 | _Chutzpah* 62 | 63 | # Visual C++ cache files 64 | ipch/ 65 | *.aps 66 | *.ncb 67 | *.opensdf 68 | *.sdf 69 | *.cachefile 70 | 71 | # Visual Studio profiler 72 | *.psess 73 | *.vsp 74 | *.vspx 75 | 76 | # TFS 2012 Local Workspace 77 | $tf/ 78 | 79 | # Guidance Automation Toolkit 80 | *.gpState 81 | 82 | # ReSharper is a .NET coding add-in 83 | _ReSharper*/ 84 | *.[Rr]e[Ss]harper 85 | *.DotSettings.user 86 | 87 | # JustCode is a .NET coding addin-in 88 | .JustCode 89 | 90 | # TeamCity is a build add-in 91 | _TeamCity* 92 | 93 | # DotCover is a Code Coverage Tool 94 | *.dotCover 95 | 96 | # NCrunch 97 | _NCrunch_* 98 | .*crunch*.local.xml 99 | 100 | # MightyMoose 101 | *.mm.* 102 | AutoTest.Net/ 103 | 104 | # Web workbench (sass) 105 | .sass-cache/ 106 | 107 | # Installshield output folder 108 | [Ee]xpress/ 109 | 110 | # DocProject is a documentation generator add-in 111 | DocProject/buildhelp/ 112 | DocProject/Help/*.HxT 113 | DocProject/Help/*.HxC 114 | DocProject/Help/*.hhc 115 | DocProject/Help/*.hhk 116 | DocProject/Help/*.hhp 117 | DocProject/Help/Html2 118 | DocProject/Help/html 119 | 120 | # Click-Once directory 121 | publish/ 122 | 123 | # Publish Web Output 124 | *.[Pp]ublish.xml 125 | *.azurePubxml 126 | ## TODO: Comment the next line if you want to checkin your 127 | ## web deploy settings but do note that will include unencrypted 128 | ## passwords 129 | #*.pubxml 130 | 131 | # NuGet Packages Directory 132 | packages/* 133 | ## TODO: If the tool you use requires repositories.config 134 | ## uncomment the next line 135 | #!packages/repositories.config 136 | 137 | # Enable "build/" folder in the NuGet Packages folder since 138 | # NuGet packages use it for MSBuild targets. 139 | # This line needs to be after the ignore of the build folder 140 | # (and the packages folder if the line above has been uncommented) 141 | !packages/build/ 142 | 143 | # Windows Azure Build Output 144 | csx/ 145 | *.build.csdef 146 | 147 | # Windows Store app package directory 148 | AppPackages/ 149 | 150 | # Others 151 | sql/ 152 | *.Cache 153 | ClientBin/ 154 | [Ss]tyle[Cc]op.* 155 | ~$* 156 | *~ 157 | *.dbmdl 158 | *.dbproj.schemaview 159 | *.pfx 160 | *.publishsettings 161 | node_modules/ 162 | 163 | # RIA/Silverlight projects 164 | Generated_Code/ 165 | 166 | # Backup & report files from converting an old project file 167 | # to a newer Visual Studio version. Backup files are not needed, 168 | # because we have git ;-) 169 | _UpgradeReport_Files/ 170 | Backup*/ 171 | UpgradeLog*.XML 172 | UpgradeLog*.htm 173 | 174 | # SQL Server files 175 | *.mdf 176 | *.ldf 177 | 178 | # Business Intelligence projects 179 | *.rdl.data 180 | *.bim.layout 181 | *.bim_*.settings 182 | 183 | # Microsoft Fakes 184 | FakesAssemblies/ 185 | 186 | # LightSwitch generated files 187 | GeneratedArtifacts/ 188 | _Pvt_Extensions/ 189 | ModelManifest.xml 190 | /packages/ 191 | /.vs 192 | *.GhostDoc.xml 193 | /artifacts 194 | *.lock.json 195 | /results 196 | -------------------------------------------------------------------------------- /.nuget/NuGet.Config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.nuget/NuGet.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutoMapper/AutoMapper.Collection/eafbf005becf73f989e1b1c0d52e8699439f837c/.nuget/NuGet.exe -------------------------------------------------------------------------------- /.nuget/NuGet.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(MSBuildProjectDirectory)\..\ 5 | 6 | 7 | false 8 | 9 | 10 | false 11 | 12 | 13 | true 14 | 15 | 16 | false 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | $([System.IO.Path]::Combine($(SolutionDir), ".nuget")) 31 | $([System.IO.Path]::Combine($(ProjectDir), "packages.config")) 32 | 33 | 34 | 35 | 36 | $(SolutionDir).nuget 37 | packages.config 38 | 39 | 40 | 41 | 42 | $(NuGetToolsPath)\NuGet.exe 43 | @(PackageSource) 44 | 45 | "$(NuGetExePath)" 46 | mono --runtime=v4.0.30319 $(NuGetExePath) 47 | 48 | $(TargetDir.Trim('\\')) 49 | 50 | -RequireConsent 51 | -NonInteractive 52 | 53 | "$(SolutionDir) " 54 | "$(SolutionDir)" 55 | 56 | 57 | $(NuGetCommand) install "$(PackagesConfig)" -source "$(PackageSources)" $(NonInteractiveSwitch) $(RequireConsentSwitch) -solutionDir $(PaddedSolutionDir) 58 | $(NuGetCommand) pack "$(ProjectPath)" -Properties "Configuration=$(Configuration);Platform=$(Platform)" $(NonInteractiveSwitch) -OutputDirectory "$(PackageOutputDir)" -symbols 59 | 60 | 61 | 62 | RestorePackages; 63 | $(BuildDependsOn); 64 | 65 | 66 | 67 | 68 | $(BuildDependsOn); 69 | BuildPackage; 70 | 71 | 72 | 73 | 74 | 75 | 76 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 91 | 92 | 95 | 96 | 97 | 98 | 100 | 101 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 133 | 134 | 135 | 136 | -------------------------------------------------------------------------------- /AutoMapper.Collection.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.1.32210.238 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{578F2483-CF08-409D-A316-31BCB7C5D9D0}" 7 | ProjectSection(SolutionItems) = preProject 8 | Build.ps1 = Build.ps1 9 | .github\workflows\ci.yml = .github\workflows\ci.yml 10 | Directory.Build.props = Directory.Build.props 11 | global.json = global.json 12 | icon.png = icon.png 13 | Push.ps1 = Push.ps1 14 | README.md = README.md 15 | .github\workflows\release.yml = .github\workflows\release.yml 16 | version.props = version.props 17 | EndProjectSection 18 | EndProject 19 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoMapper.Collection", "src\AutoMapper.Collection\AutoMapper.Collection.csproj", "{37AD667A-8080-476C-88FD-20310AC7CAF3}" 20 | EndProject 21 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoMapper.Collection.EntityFramework", "src\AutoMapper.Collection.EntityFramework\AutoMapper.Collection.EntityFramework.csproj", "{E0FD41FD-AF5B-4BEC-970F-8412E7B6C914}" 22 | EndProject 23 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoMapper.Collection.Tests", "src\AutoMapper.Collection.Tests\AutoMapper.Collection.Tests.csproj", "{2D3D34AD-6A0A-4382-9A2F-894F52D184A7}" 24 | EndProject 25 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoMapper.Collection.EntityFramework.Tests", "src\AutoMapper.Collection.EntityFramework.Tests\AutoMapper.Collection.EntityFramework.Tests.csproj", "{BDE127AB-AC3F-44DF-BC33-210DAFD12E15}" 26 | EndProject 27 | Global 28 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 29 | Debug|Any CPU = Debug|Any CPU 30 | Release|Any CPU = Release|Any CPU 31 | EndGlobalSection 32 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 33 | {37AD667A-8080-476C-88FD-20310AC7CAF3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {37AD667A-8080-476C-88FD-20310AC7CAF3}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {37AD667A-8080-476C-88FD-20310AC7CAF3}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {37AD667A-8080-476C-88FD-20310AC7CAF3}.Release|Any CPU.Build.0 = Release|Any CPU 37 | {E0FD41FD-AF5B-4BEC-970F-8412E7B6C914}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 38 | {E0FD41FD-AF5B-4BEC-970F-8412E7B6C914}.Debug|Any CPU.Build.0 = Debug|Any CPU 39 | {E0FD41FD-AF5B-4BEC-970F-8412E7B6C914}.Release|Any CPU.ActiveCfg = Release|Any CPU 40 | {E0FD41FD-AF5B-4BEC-970F-8412E7B6C914}.Release|Any CPU.Build.0 = Release|Any CPU 41 | {2D3D34AD-6A0A-4382-9A2F-894F52D184A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 42 | {2D3D34AD-6A0A-4382-9A2F-894F52D184A7}.Debug|Any CPU.Build.0 = Debug|Any CPU 43 | {2D3D34AD-6A0A-4382-9A2F-894F52D184A7}.Release|Any CPU.ActiveCfg = Release|Any CPU 44 | {2D3D34AD-6A0A-4382-9A2F-894F52D184A7}.Release|Any CPU.Build.0 = Release|Any CPU 45 | {BDE127AB-AC3F-44DF-BC33-210DAFD12E15}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 46 | {BDE127AB-AC3F-44DF-BC33-210DAFD12E15}.Debug|Any CPU.Build.0 = Debug|Any CPU 47 | {BDE127AB-AC3F-44DF-BC33-210DAFD12E15}.Release|Any CPU.ActiveCfg = Release|Any CPU 48 | {BDE127AB-AC3F-44DF-BC33-210DAFD12E15}.Release|Any CPU.Build.0 = Release|Any CPU 49 | EndGlobalSection 50 | GlobalSection(SolutionProperties) = preSolution 51 | HideSolutionNode = FALSE 52 | EndGlobalSection 53 | GlobalSection(ExtensibilityGlobals) = postSolution 54 | SolutionGuid = {D5C8CD86-1F3F-4D3A-8A90-5F3726E8F998} 55 | EndGlobalSection 56 | EndGlobal 57 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 4.15.0 6 | 15.5.0 7 | 2.4.1 8 | 2.2.1 9 | 10 | 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Tyler Carlson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Push.ps1: -------------------------------------------------------------------------------- 1 | $scriptName = $MyInvocation.MyCommand.Name 2 | $artifacts = "./artifacts" 3 | 4 | if ([string]::IsNullOrEmpty($Env:NUGET_API_KEY)) { 5 | Write-Host "${scriptName}: NUGET_API_KEY is empty or not set. Skipped pushing package(s)." 6 | } else { 7 | Get-ChildItem $artifacts -Filter "*.nupkg" | ForEach-Object { 8 | Write-Host "$($scriptName): Pushing $($_.Name)" 9 | dotnet nuget push $_ --source $Env:NUGET_URL --api-key $Env:NUGET_API_KEY 10 | if ($lastexitcode -ne 0) { 11 | throw ("Exec: " + $errorMessage) 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | AutoMapper 2 | 3 | # AutoMapper.Collection 4 | Adds ability to map collections to existing collections without re-creating the collection object. 5 | 6 | Will Add/Update/Delete items from a preexisting collection object based on user defined equivalency between the collection's generic item type from the source collection and the destination collection. 7 | 8 | [![NuGet](http://img.shields.io/nuget/v/AutoMapper.Collection.svg)](https://www.nuget.org/packages/AutoMapper.Collection/) 9 | 10 | ## How to add to AutoMapper? 11 | Call AddCollectionMappers when configuring 12 | ``` 13 | Mapper.Initialize(cfg => 14 | { 15 | cfg.AddCollectionMappers(); 16 | // Configuration code 17 | }); 18 | ``` 19 | Will add new IObjectMapper objects into the master mapping list. 20 | 21 | ## Adding equivalency between two classes 22 | Adding equivalence to objects is done with EqualityComparison extended from the IMappingExpression class. 23 | ``` 24 | cfg.CreateMap().EqualityComparison((odto, o) => odto.ID == o.ID); 25 | ``` 26 | Mapping OrderDTO back to Order will compare Order items list based on if their ID's match 27 | ``` 28 | Mapper.Map,List>(orderDtos, orders); 29 | ``` 30 | If ID's match, then AutoMapper will map OrderDTO to Order 31 | 32 | If OrderDTO exists and Order doesn't, then AutoMapper will add a new Order mapped from OrderDTO to the collection 33 | 34 | If Order exists and OrderDTO doesn't, then AutoMapper will remove Order from collection 35 | 36 | ## Why update collection? Just recreate it 37 | ORMs don't like setting the collection, so you need to add and remove from preexisting one. 38 | 39 | This automates the process by just specifying what is equal to each other. 40 | 41 | ## Can it just figure out the ID equivalency for me in Entity Framework? 42 | `Automapper.Collection.EntityFramework` or `Automapper.Collection.EntityFrameworkCore` can do that for you. 43 | 44 | ``` 45 | Mapper.Initialize(cfg => 46 | { 47 | cfg.AddCollectionMappers(); 48 | // entity framework 49 | cfg.SetGeneratePropertyMaps>(); 50 | // entity framework core 51 | cfg.SetGeneratePropertyMaps>(); 52 | // Configuration code 53 | }); 54 | ``` 55 | User defined equality expressions will overwrite primary key expressions. 56 | 57 | ## What about comparing to a single existing Entity for updating? 58 | Automapper.Collection.EntityFramework does that as well through extension method from of DbSet. 59 | 60 | Translate equality between dto and EF object to an expression of just the EF using the dto's values as constants. 61 | ``` 62 | dbContext.Orders.Persist().InsertOrUpdate(newOrderDto); 63 | dbContext.Orders.Persist().InsertOrUpdate(existingOrderDto); 64 | dbContext.Orders.Persist().Remove(deletedOrderDto); 65 | dbContext.SubmitChanges(); 66 | ``` 67 | **Note:** This is done by converting the OrderDTO to Expression> and using that to find matching type in the database. You can also map objects to expressions as well. 68 | 69 | Persist doesn't call submit changes automatically 70 | 71 | ## Where can I get it? 72 | 73 | First, [install NuGet](http://docs.nuget.org/docs/start-here/installing-nuget). Then, install [AutoMapper.Collection](https://www.nuget.org/packages/AutoMapper.Collection/) from the package manager console: 74 | ``` 75 | PM> Install-Package AutoMapper.Collection 76 | ``` 77 | 78 | ### Additional packages 79 | 80 | #### AutoMapper Collection for Entity Framework 81 | ``` 82 | PM> Install-Package AutoMapper.Collection.EntityFramework 83 | ``` 84 | 85 | #### AutoMapper Collection for Entity Framework Core 86 | ``` 87 | PM> Install-Package AutoMapper.Collection.EntityFrameworkCore 88 | ``` 89 | 90 | #### AutoMapper Collection for LinqToSQL 91 | ``` 92 | PM> Install-Package AutoMapper.Collection.LinqToSQL 93 | ``` 94 | -------------------------------------------------------------------------------- /build.ps1: -------------------------------------------------------------------------------- 1 | # Taken from psake https://github.com/psake/psake 2 | 3 | <# 4 | .SYNOPSIS 5 | This is a helper function that runs a scriptblock and checks the PS variable $lastexitcode 6 | to see if an error occcured. If an error is detected then an exception is thrown. 7 | This function allows you to run command-line programs without having to 8 | explicitly check the $lastexitcode variable. 9 | .EXAMPLE 10 | exec { svn info $repository_trunk } "Error executing SVN. Please verify SVN command-line client is installed" 11 | #> 12 | function Exec 13 | { 14 | [CmdletBinding()] 15 | param( 16 | [Parameter(Position=0,Mandatory=1)][scriptblock]$cmd, 17 | [Parameter(Position=1,Mandatory=0)][string]$errorMessage = ($msgs.error_bad_command -f $cmd) 18 | ) 19 | & $cmd 20 | if ($lastexitcode -ne 0) { 21 | throw ("Exec: " + $errorMessage) 22 | } 23 | } 24 | 25 | $artifacts = ".\artifacts" 26 | 27 | if(Test-Path $artifacts) { Remove-Item $artifacts -Force -Recurse } 28 | 29 | exec { & dotnet test -c Release -r $artifacts -l trx --verbosity=normal } 30 | 31 | exec { & dotnet pack .\AutoMapper.Collection.sln -c Release -o $artifacts --no-build } 32 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | {"projects":[]} -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutoMapper/AutoMapper.Collection/eafbf005becf73f989e1b1c0d52e8699439f837c/icon.png -------------------------------------------------------------------------------- /src/AutoMapper.Collection.EntityFramework.Tests/AutoMapper.Collection.EntityFramework.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | AutoMapper.Collection.EntityFramework.Tests 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | all 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | %(RecursiveDir)%(FileName)%(Extension) 28 | PreserveNewest 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/AutoMapper.Collection.EntityFramework.Tests/EntityFramworkTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data.Entity; 4 | using System.Linq; 5 | using AutoMapper.EntityFramework; 6 | using AutoMapper.EquivalencyExpression; 7 | using FluentAssertions; 8 | using Xunit; 9 | 10 | namespace AutoMapper.Collection.EntityFramework.Tests 11 | { 12 | public class EntityFramworkTests : MappingTestBase 13 | { 14 | private void ConfigureMapper(IMapperConfigurationExpression cfg) 15 | { 16 | cfg.AddCollectionMappers(); 17 | cfg.CreateMap().ReverseMap(); 18 | cfg.SetGeneratePropertyMaps>(); 19 | } 20 | 21 | [Fact] 22 | public void Should_Persist_To_Update() 23 | { 24 | var mapper = CreateMapper(ConfigureMapper); 25 | 26 | var db = new DB(); 27 | db.Things.Add(new Thing { Title = "Test2" }); 28 | db.Things.Add(new Thing { Title = "Test3" }); 29 | db.Things.Add(new Thing { Title = "Test4" }); 30 | db.SaveChanges(); 31 | 32 | Assert.Equal(3, db.Things.Count()); 33 | 34 | var item = db.Things.First(); 35 | 36 | db.Things.Persist(mapper).InsertOrUpdate(new ThingDto { ID = item.ID, Title = "Test" }); 37 | Assert.Equal(1, db.ChangeTracker.Entries().Count(x => x.State == EntityState.Modified)); 38 | 39 | Assert.Equal(3, db.Things.Count()); 40 | 41 | db.Things.First(x => x.ID == item.ID).Title.Should().Be("Test"); 42 | } 43 | 44 | [Fact] 45 | public void Should_Persist_To_Insert() 46 | { 47 | var mapper = CreateMapper(ConfigureMapper); 48 | 49 | var db = new DB(); 50 | db.Things.Add(new Thing { Title = "Test2" }); 51 | db.Things.Add(new Thing { Title = "Test3" }); 52 | db.Things.Add(new Thing { Title = "Test4" }); 53 | db.SaveChanges(); 54 | 55 | Assert.Equal(3, db.Things.Count()); 56 | 57 | db.Things.Persist(mapper).InsertOrUpdate(new ThingDto { Title = "Test" }); 58 | Assert.Equal(3, db.Things.Count()); 59 | Assert.Equal(1, db.ChangeTracker.Entries().Count(x => x.State == EntityState.Added)); 60 | 61 | db.SaveChanges(); 62 | 63 | Assert.Equal(4, db.Things.Count()); 64 | 65 | db.Things.OrderByDescending(x => x.ID).First().Title.Should().Be("Test"); 66 | } 67 | 68 | public class DB : DbContext 69 | { 70 | public DB() 71 | : base(Effort.DbConnectionFactory.CreateTransient(), contextOwnsConnection: true) 72 | { 73 | Things.RemoveRange(Things); 74 | SaveChanges(); 75 | } 76 | 77 | public DbSet Things { get; set; } 78 | } 79 | 80 | public class Thing 81 | { 82 | public int ID { get; set; } 83 | public string Title { get; set; } 84 | public override string ToString() { return Title; } 85 | } 86 | 87 | public class ThingDto 88 | { 89 | public int ID { get; set; } 90 | public string Title { get; set; } 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/AutoMapper.Collection.EntityFramework.Tests/MappingTestBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace AutoMapper.Collection 4 | { 5 | public abstract class MappingTestBase 6 | { 7 | protected IMapper CreateMapper(Action cfg) 8 | { 9 | var map = new MapperConfiguration(cfg); 10 | map.CompileMappings(); 11 | 12 | var mapper = map.CreateMapper(); 13 | mapper.ConfigurationProvider.AssertConfigurationIsValid(); 14 | return mapper; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/AutoMapper.Collection.EntityFramework/AutoMapper.Collection.EntityFramework.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Collection updating support for EntityFramework with AutoMapper. Extends DBSet<T> with Persist<TDto>().InsertUpdate(dto) and Persist<TDto>().Delete(dto). Will find the matching object and will Insert/Update/Delete. 5 | Tyler Carlson 6 | net8.0 7 | AutoMapper.Collection.EntityFramework 8 | AutoMapper.Collection.EntityFramework 9 | icon.png 10 | https://github.com/AutoMapper/Automapper.Collection 11 | ../Key.snk 12 | true 13 | MIT 14 | true 15 | true 16 | v 17 | snupkg 18 | true 19 | true 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | all 35 | runtime; build; native; contentfiles; analyzers; buildtransitive 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/AutoMapper.Collection.EntityFramework/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Data.Entity; 5 | using AutoMapper.Extensions.ExpressionMapping.Impl; 6 | 7 | namespace AutoMapper.EntityFramework 8 | { 9 | public static class Extensions 10 | { 11 | /// 12 | /// Obsolete: Use Persist(IMapper) instead. 13 | /// Create a Persistence object for the to have data persisted or removed from 14 | /// Uses static API's Mapper for finding TypeMap between classes 15 | /// 16 | /// Source table type to be updated 17 | /// DbSet to be updated 18 | /// Persistence object to Update or Remove data 19 | [Obsolete("Use Persist(IMapper) instead.", true)] 20 | public static IPersistence Persist(this DbSet source) 21 | where TSource : class 22 | { 23 | throw new NotSupportedException(); 24 | } 25 | 26 | /// 27 | /// Create a Persistence object for the to have data persisted or removed from 28 | /// 29 | /// Source table type to be updated 30 | /// DbSet to be updated 31 | /// IMapper used to find TypeMap between classes 32 | /// Persistence object to Update or Remove data 33 | public static IPersistence Persist(this DbSet source, IMapper mapper) 34 | where TSource : class 35 | { 36 | return new Persistence(source, mapper); 37 | } 38 | 39 | /// 40 | /// Non Generic call for For 41 | /// 42 | /// 43 | /// 44 | /// 45 | /// 46 | public static IEnumerable For(this IQueryDataSourceInjection source, Type destType) 47 | { 48 | var forMethod = source.GetType().GetMethod("For").MakeGenericMethod(destType); 49 | var listType = typeof(List<>).MakeGenericType(destType); 50 | var forResult = forMethod.Invoke(source, new object[] { null }); 51 | var enumeratedResult = Activator.CreateInstance(listType, forResult); 52 | return enumeratedResult as IEnumerable; 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /src/AutoMapper.Collection.EntityFramework/GenerateEntityFrameworkPrimaryKeyPropertyMaps.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data.Entity.Core.Metadata.Edm; 4 | using System.Data.Entity.Core.Objects; 5 | using System.Data.Entity.Infrastructure; 6 | using System.Linq; 7 | using System.Reflection; 8 | using AutoMapper.EquivalencyExpression; 9 | 10 | namespace AutoMapper.EntityFramework 11 | { 12 | public class GenerateEntityFrameworkPrimaryKeyPropertyMaps : IGeneratePropertyMaps 13 | where TDatabaseContext : IObjectContextAdapter, new() 14 | { 15 | private readonly TDatabaseContext _context = new TDatabaseContext(); 16 | private readonly MethodInfo _createObjectSetMethodInfo = typeof(ObjectContext).GetMethod("CreateObjectSet", Type.EmptyTypes); 17 | 18 | public IEnumerable GeneratePropertyMaps(TypeMap typeMap) 19 | { 20 | var propertyMaps = typeMap.PropertyMaps; 21 | try 22 | { 23 | var createObjectSetMethod = _createObjectSetMethodInfo.MakeGenericMethod(typeMap.DestinationType); 24 | dynamic objectSet = createObjectSetMethod.Invoke(_context.ObjectContext, null); 25 | 26 | IEnumerable keyMembers = objectSet.EntitySet.ElementType.KeyMembers; 27 | var primaryKeyPropertyMatches = keyMembers.Select(m => propertyMaps.FirstOrDefault(p => p.DestinationMember.Name == m.Name)); 28 | 29 | return primaryKeyPropertyMatches; 30 | } 31 | catch (Exception) 32 | { 33 | return Enumerable.Empty(); 34 | } 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /src/AutoMapper.Collection.EntityFramework/IPersistence.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace AutoMapper.EntityFramework 4 | { 5 | public interface IPersistence 6 | { 7 | /// 8 | /// Insert Or Update the with 9 | /// 10 | /// Uses > to find equality between Source and From Types to determine if insert or update 11 | /// Source Type mapping from 12 | /// Object to update to 13 | void InsertOrUpdate(TFrom from) where TFrom : class; 14 | /// 15 | /// Insert Or Update the with 16 | /// 17 | /// Uses > to find equality between Source and From Types to determine if insert or update 18 | /// Source Type mapping from 19 | /// Object to update to 20 | void InsertOrUpdate(Type type, object from); 21 | /// 22 | /// Remove from with 23 | /// 24 | /// Uses > to find equality between Source and From Types to determine if insert or update 25 | /// Source Type mapping from 26 | /// Object to remove that is Equivalent in 27 | void Remove(TFrom from) where TFrom : class; 28 | } 29 | } -------------------------------------------------------------------------------- /src/AutoMapper.Collection.EntityFramework/Persistence.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data.Entity; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | 6 | namespace AutoMapper.EntityFramework 7 | { 8 | public class Persistence : IPersistence 9 | where TTo : class 10 | { 11 | private readonly DbSet _sourceSet; 12 | private readonly IMapper _mapper; 13 | 14 | public Persistence(DbSet sourceSet, IMapper mapper) 15 | { 16 | _sourceSet = sourceSet; 17 | _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); 18 | } 19 | 20 | public void InsertOrUpdate(TFrom from) 21 | where TFrom : class 22 | { 23 | InsertOrUpdate(typeof(TFrom), from); 24 | } 25 | 26 | public void InsertOrUpdate(Type type, object from) 27 | { 28 | if (!(_mapper.Map(from, type, typeof(Expression>)) is Expression> equivExpr)) 29 | return; 30 | 31 | var to = _sourceSet.FirstOrDefault(equivExpr); 32 | 33 | if (to == null) 34 | { 35 | to = _sourceSet.Create(); 36 | _sourceSet.Add(to); 37 | } 38 | _mapper.Map(from, to); 39 | } 40 | 41 | public void Remove(TFrom from) 42 | where TFrom : class 43 | { 44 | var equivExpr = _mapper.Map>>(from); 45 | if (equivExpr == null) 46 | return; 47 | var to = _sourceSet.FirstOrDefault(equivExpr); 48 | 49 | if (to != null) 50 | _sourceSet.Remove(to); 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /src/AutoMapper.Collection.LinqToSQL/AutoMapper.Collection.LinqToSQL.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Collection updating support for LinqToSQL with AutoMapper. Extends Table<T> with Persist<TDto>().InsertUpdate(dto) and Persist<TDto>().Delete(dto). Will find the matching object and will Insert/Update/Delete. 5 | Tyler Carlson 6 | net461 7 | AutoMapper.Collection.LinqToSQL 8 | AutoMapper.Collection.LinqToSQL 9 | icon.png 10 | https://github.com/AutoMapper/Automapper.Collection 11 | ../Key.snk 12 | true 13 | MIT 14 | true 15 | true 16 | v 17 | snupkg 18 | true 19 | true 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | all 34 | runtime; build; native; contentfiles; analyzers; buildtransitive 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/AutoMapper.Collection.LinqToSQL/GetLinqToSQLPrimaryKeyProperties.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Data.Linq.Mapping; 3 | using System.Linq; 4 | using System.Reflection; 5 | using AutoMapper.EquivalencyExpression; 6 | 7 | namespace AutoMapper.Collection.LinqToSQL 8 | { 9 | public class GetLinqToSQLPrimaryKeyProperties : IGeneratePropertyMaps 10 | { 11 | public IEnumerable GeneratePropertyMaps(TypeMap typeMap) 12 | { 13 | var propertyMaps = typeMap.PropertyMaps; 14 | 15 | var primaryKeyPropertyMatches = typeMap.DestinationType.GetProperties() 16 | .Where(IsPrimaryKey) 17 | .Select(m => propertyMaps.FirstOrDefault(p => p.DestinationMember.Name == m.Name)) 18 | .ToList(); 19 | 20 | return primaryKeyPropertyMatches; 21 | } 22 | 23 | private static bool IsPrimaryKey(PropertyInfo propertyInfo) 24 | { 25 | return propertyInfo.GetCustomAttributes(typeof(ColumnAttribute), false) 26 | .OfType().Any(ca => ca.IsPrimaryKey); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/AutoMapper.Collection.LinqToSQL/IPersistence.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace AutoMapper.Collection.LinqToSQL 4 | { 5 | public interface IPersistence 6 | { 7 | /// 8 | /// Insert Or Update the with 9 | /// 10 | /// Uses > to find equality between Source and From Types to determine if insert or update 11 | /// Source Type mapping from 12 | /// Object to update to 13 | void InsertOrUpdate(TFrom from) where TFrom : class; 14 | 15 | void InsertOrUpdate(Type type, object from); 16 | 17 | /// 18 | /// Remove from with 19 | /// 20 | /// Uses > to find equality between Source and From Types to determine if insert or update 21 | /// Source Type mapping from 22 | /// Object to remove that is Equivalent in 23 | void Remove(TFrom from) where TFrom : class; 24 | } 25 | } -------------------------------------------------------------------------------- /src/AutoMapper.Collection.LinqToSQL/Persistence.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data.Linq; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | 6 | namespace AutoMapper.Collection.LinqToSQL 7 | { 8 | public class Persistence : IPersistence 9 | where TTo : class 10 | { 11 | private readonly Table _sourceSet; 12 | private readonly IMapper _mapper; 13 | 14 | public Persistence(Table sourceSet, IMapper mapper) 15 | { 16 | _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); 17 | _sourceSet = sourceSet; 18 | } 19 | 20 | public void InsertOrUpdate(TFrom from) 21 | where TFrom : class 22 | { 23 | InsertOrUpdate(typeof(TFrom), from); 24 | } 25 | 26 | public void InsertOrUpdate(Type type, object from) 27 | { 28 | if (!(_mapper.Map(from, type, typeof(Expression>)) is Expression> equivExpr)) 29 | return; 30 | 31 | var to = _sourceSet.FirstOrDefault(equivExpr); 32 | 33 | if (to == null) 34 | { 35 | to = Activator.CreateInstance(); 36 | _sourceSet.InsertOnSubmit(to); 37 | } 38 | _mapper.Map(from, to); 39 | } 40 | 41 | public void Remove(TFrom from) 42 | where TFrom : class 43 | { 44 | var equivExpr = _mapper.Map>>(from); 45 | if (equivExpr == null) 46 | return; 47 | var to = _sourceSet.FirstOrDefault(equivExpr); 48 | 49 | if (to != null) 50 | _sourceSet.DeleteOnSubmit(to); 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /src/AutoMapper.Collection.LinqToSQL/PersistenceExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Data.Linq; 5 | using AutoMapper.Extensions.ExpressionMapping.Impl; 6 | 7 | namespace AutoMapper.Collection.LinqToSQL 8 | { 9 | public static class PersistenceExtensions 10 | { 11 | /// 12 | /// Obsolete: Use Persist(IMapper) instead. 13 | /// Create a Persistence object for the to have data persisted or removed from 14 | /// Uses static API's Mapper for finding TypeMap between classes 15 | /// 16 | /// Source table type to be updated 17 | /// Table to be updated 18 | /// Persistence object to Update or Remove data 19 | [Obsolete("Use Persist(IMapper) instead.", true)] 20 | public static IPersistence Persist(this Table source) 21 | where TSource : class 22 | { 23 | throw new NotSupportedException(); 24 | } 25 | 26 | /// 27 | /// Create a Persistence object for the to have data persisted or removed from 28 | /// 29 | /// Source table type to be updated 30 | /// Table to be updated 31 | /// IMapper used to find TypeMap between classes 32 | /// Persistence object to Update or Remove data 33 | public static IPersistence Persist(this Table source, IMapper mapper) 34 | where TSource : class 35 | { 36 | return new Persistence(source, mapper); 37 | } 38 | 39 | public static IEnumerable For(this IQueryDataSourceInjection source, Type destType) 40 | { 41 | var forMethod = source.GetType().GetMethod("For").MakeGenericMethod(destType); 42 | var listType = typeof(List<>).MakeGenericType(destType); 43 | var forResult = forMethod.Invoke(source, new object[] { null }); 44 | var enumeratedResult = Activator.CreateInstance(listType, forResult); 45 | return enumeratedResult as IEnumerable; 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /src/AutoMapper.Collection.Tests/AutoMapper.Collection.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | AutoMapper.Collection.Tests 6 | AutoMapper.Collection 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/AutoMapper.Collection.Tests/InheritanceWithCollectionTests.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper.EquivalencyExpression; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using AutoMapper.Internal; 5 | using FluentAssertions; 6 | using Xunit; 7 | 8 | namespace AutoMapper.Collection 9 | { 10 | public abstract class InheritanceWithCollectionTests : MappingTestBase 11 | { 12 | protected abstract void ConfigureMapper(IMapperConfigurationExpression cfg); 13 | 14 | [Fact] 15 | public void TypeMap_Should_include_base_types() 16 | { 17 | var mapper = CreateMapper(ConfigureMapper); 18 | var typeMap = mapper.ConfigurationProvider.Internal().ResolveTypeMap(typeof(MailOrderDomain), typeof(OrderEf)); 19 | 20 | var typePairs = new[]{ 21 | new TypePair(typeof(OrderDomain), typeof(OrderEf)) 22 | }; 23 | typeMap.IncludedBaseTypes.ShouldBeEquivalentTo(typePairs); 24 | } 25 | 26 | [Fact] 27 | public void TypeMap_Should_include_derivied_types() 28 | { 29 | var mapper = CreateMapper(ConfigureMapper); 30 | var typeMap = mapper.ConfigurationProvider.Internal().ResolveTypeMap(typeof(OrderDomain), typeof(OrderEf)); 31 | 32 | var typePairs = new[]{ 33 | new TypePair(typeof(OnlineOrderDomain), typeof(OnlineOrderEf)), 34 | new TypePair(typeof(MailOrderDomain), typeof(MailOrderEf)) 35 | }; 36 | typeMap.IncludedDerivedTypes.ShouldBeEquivalentTo(typePairs); 37 | } 38 | 39 | [Fact] 40 | public void Map_Should_ReturnOnlineOrderEf_When_ListIsOfTypeOrderEf() 41 | { 42 | var mapper = CreateMapper(ConfigureMapper); 43 | 44 | //arrange 45 | var orderDomain = new OnlineOrderDomain { Id = "Id", Key = "Key" }; 46 | var rootDomain = new RootDomain { OnlineOrders = { orderDomain } }; 47 | 48 | //act 49 | RootEf mappedRootEf = mapper.Map(rootDomain); 50 | 51 | //assert 52 | OrderEf orderEf = mappedRootEf.Orders[0]; 53 | 54 | orderEf.Should().BeOfType(); 55 | orderEf.Id.ShouldBeEquivalentTo(orderDomain.Id); 56 | 57 | var onlineOrderEf = (OnlineOrderEf)orderEf; 58 | onlineOrderEf.Key.ShouldBeEquivalentTo(orderDomain.Key); 59 | 60 | // ------------------------------------------------------------- // 61 | 62 | //arrange again 63 | mappedRootEf.Orders.Add(new OnlineOrderEf { Id = "NewId" }); 64 | 65 | mapper.Map(mappedRootEf, rootDomain); 66 | 67 | //assert again 68 | rootDomain.OnlineOrders.Count.ShouldBeEquivalentTo(2); 69 | rootDomain.OnlineOrders.Last().Should().BeOfType(); 70 | 71 | //Assert.AreSame(rootDomain.OnlineOrders.First(), orderDomain); that doesn't matter when we map from EF to Domain 72 | } 73 | 74 | [Fact] 75 | public void Map_FromEfToDomain_And_AddAnOnlineOrderInTheDomainObject_And_ThenMapBackToEf_Should_UseTheSameReferenceInTheEfCollection() 76 | { 77 | var mapper = CreateMapper(ConfigureMapper); 78 | 79 | //arrange 80 | var onlineOrderEf = new OnlineOrderEf { Id = "Id", Key = "Key" }; 81 | var mailOrderEf = new MailOrderEf { Id = "MailOrderId" }; 82 | var rootEf = new RootEf { Orders = { onlineOrderEf, mailOrderEf } }; 83 | 84 | //act 85 | RootDomain mappedRootDomain = mapper.Map(rootEf); 86 | 87 | //assert 88 | OnlineOrderDomain onlineOrderDomain = mappedRootDomain.OnlineOrders[0]; 89 | 90 | onlineOrderDomain.Should().BeOfType(); 91 | onlineOrderEf.Id.ShouldBeEquivalentTo(onlineOrderEf.Id); 92 | 93 | // IMPORTANT ASSERT ------------------------------------------------------------- IMPORTANT ASSERT // 94 | 95 | //arrange again 96 | mappedRootDomain.OnlineOrders.Add(new OnlineOrderDomain { Id = "NewOnlineOrderId", Key = "NewKey" }); 97 | mappedRootDomain.MailOrders.Add(new MailOrderDomain { Id = "NewMailOrderId", }); 98 | onlineOrderDomain.Id = "Hi"; 99 | 100 | //act again 101 | mapper.Map(mappedRootDomain, rootEf); 102 | 103 | //assert again 104 | OrderEf existingMailOrderEf = rootEf.Orders.Single(orderEf => orderEf.Id == mailOrderEf.Id); 105 | OrderEf existingOnlineOrderEf = rootEf.Orders.Single(orderEf => orderEf.Id == onlineOrderEf.Id); 106 | 107 | OrderEf newOnlineOrderEf = rootEf.Orders.Single(orderEf => orderEf.Id == "NewOnlineOrderId"); 108 | OrderEf newMailOrderEf = rootEf.Orders.Single(orderEf => orderEf.Id == "NewMailOrderId"); 109 | 110 | rootEf.Orders.Count.ShouldBeEquivalentTo(4); 111 | onlineOrderEf.Should().BeSameAs(existingOnlineOrderEf); 112 | mailOrderEf.Should().BeSameAs(existingMailOrderEf); 113 | 114 | newOnlineOrderEf.Should().BeOfType(); 115 | newMailOrderEf.Should().BeOfType(); 116 | } 117 | 118 | private static bool BaseEquals(OrderDomain oo, OrderEf dto) 119 | { 120 | return oo.Id == dto.Id; 121 | } 122 | 123 | private static bool DerivedEquals(OnlineOrderDomain ood, OnlineOrderEf ooe) 124 | { 125 | return ood.Key == ooe.Key; 126 | } 127 | 128 | public class MailOrderDomain : OrderDomain 129 | { 130 | } 131 | 132 | public class MailOrderEf : OrderEf 133 | { 134 | } 135 | 136 | public class OnlineOrderDomain : OrderDomain 137 | { 138 | public string Key { get; set; } 139 | } 140 | 141 | public class OnlineOrderEf : OrderEf 142 | { 143 | public string Key { get; set; } 144 | } 145 | 146 | public abstract class OrderDomain 147 | { 148 | public string Id { get; set; } 149 | } 150 | 151 | public abstract class OrderEf 152 | { 153 | public string Id { get; set; } 154 | } 155 | 156 | public class RootDomain 157 | { 158 | public List OnlineOrders { get; set; } = new List(); 159 | public List MailOrders { get; set; } = new List(); 160 | } 161 | 162 | public class RootEf 163 | { 164 | public List Orders { get; set; } = new List(); 165 | } 166 | 167 | public class MergeDomainOrdersToEfOrdersValueResolver : IValueResolver> 168 | { 169 | public List Resolve(RootDomain source, RootEf destination, List destMember, ResolutionContext context) 170 | { 171 | var mappedOnlineOrders = new List(destination.Orders); 172 | var mappedMailOrders = new List(destination.Orders); 173 | 174 | context.Mapper.Map(source.OnlineOrders, mappedOnlineOrders); 175 | context.Mapper.Map(source.MailOrders, mappedMailOrders); 176 | 177 | var efOrders = mappedOnlineOrders.Union(mappedMailOrders).ToList(); 178 | 179 | return efOrders; 180 | } 181 | } 182 | 183 | public class Include : InheritanceWithCollectionTests 184 | { 185 | protected override void ConfigureMapper(IMapperConfigurationExpression cfg) 186 | { 187 | cfg.ShouldMapProperty = propertyInfo => propertyInfo.GetMethod.IsPublic || propertyInfo.GetMethod.IsAssembly || propertyInfo.GetMethod.IsFamily || propertyInfo.GetMethod.IsPrivate; 188 | cfg.AddCollectionMappers(); 189 | 190 | //DOMAIN --> EF 191 | cfg.CreateMap() 192 | .ForMember(rootEf => rootEf.Orders, opt => opt.MapFrom()) 193 | ; 194 | 195 | //collection type 196 | cfg.CreateMap() 197 | .EqualityComparison((oo, dto) => BaseEquals(oo, dto)) 198 | .Include() 199 | .Include() 200 | ; 201 | 202 | cfg.CreateMap() 203 | .EqualityComparison((ood, ooe) => DerivedEquals(ood, ooe)) 204 | ; 205 | 206 | cfg.CreateMap() 207 | ; 208 | 209 | //EF --> DOMAIN 210 | cfg.CreateMap() 211 | .ForMember(rootDomain => rootDomain.OnlineOrders, opt => opt.MapFrom(rootEf => rootEf.Orders.Where(orderEf => orderEf.GetType() == typeof(OnlineOrderEf)))) 212 | .ForMember(rootDomain => rootDomain.MailOrders, opt => opt.MapFrom(rootEf => rootEf.Orders.Where(orderEf => orderEf.GetType() == typeof(MailOrderEf)))) 213 | ; 214 | 215 | cfg.CreateMap() 216 | .Include() 217 | .Include() 218 | ; 219 | 220 | cfg.CreateMap() 221 | ; 222 | 223 | cfg.CreateMap() 224 | ; 225 | } 226 | } 227 | 228 | public class IncludeBase : InheritanceWithCollectionTests 229 | { 230 | protected override void ConfigureMapper(IMapperConfigurationExpression cfg) 231 | { 232 | cfg.ShouldMapProperty = propertyInfo => propertyInfo.GetMethod.IsPublic || propertyInfo.GetMethod.IsAssembly || propertyInfo.GetMethod.IsFamily || propertyInfo.GetMethod.IsPrivate; 233 | cfg.AddCollectionMappers(); 234 | 235 | //DOMAIN --> EF 236 | cfg.CreateMap() 237 | .ForMember(rootEf => rootEf.Orders, opt => opt.MapFrom()) 238 | ; 239 | 240 | //collection type 241 | cfg.CreateMap() 242 | .EqualityComparison((oo, dto) => BaseEquals(oo, dto)) 243 | ; 244 | 245 | cfg.CreateMap() 246 | .EqualityComparison((ood, ooe) => DerivedEquals(ood, ooe)) 247 | .IncludeBase() 248 | ; 249 | 250 | cfg.CreateMap() 251 | .IncludeBase() 252 | ; 253 | 254 | //EF --> DOMAIN 255 | cfg.CreateMap() 256 | .ForMember(rootDomain => rootDomain.OnlineOrders, opt => opt.MapFrom(rootEf => rootEf.Orders.Where(orderEf => orderEf.GetType() == typeof(OnlineOrderEf)))) 257 | .ForMember(rootDomain => rootDomain.MailOrders, opt => opt.MapFrom(rootEf => rootEf.Orders.Where(orderEf => orderEf.GetType() == typeof(MailOrderEf)))) 258 | ; 259 | 260 | cfg.CreateMap() 261 | ; 262 | 263 | cfg.CreateMap() 264 | .IncludeBase() 265 | ; 266 | 267 | cfg.CreateMap() 268 | .IncludeBase() 269 | ; 270 | } 271 | } 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /src/AutoMapper.Collection.Tests/MapCollectionWithEqualityTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using AutoMapper.EquivalencyExpression; 5 | using FluentAssertions; 6 | using Xunit; 7 | 8 | namespace AutoMapper.Collection 9 | { 10 | public class MapCollectionWithEqualityTests : MappingTestBase 11 | { 12 | protected virtual void ConfigureMapper(IMapperConfigurationExpression cfg) 13 | { 14 | cfg.AddCollectionMappers(); 15 | cfg.CreateMap().EqualityComparison((dto, entity) => dto.ID == entity.ID); 16 | } 17 | 18 | [Fact] 19 | public void Should_Keep_Existing_List() 20 | { 21 | var mapper = CreateMapper(ConfigureMapper); 22 | var dtos = new List 23 | { 24 | new ThingDto { ID = 1, Title = "test0" }, 25 | new ThingDto { ID = 2, Title = "test2" } 26 | }; 27 | 28 | var items = new List 29 | { 30 | new Thing { ID = 1, Title = "test1" }, 31 | new Thing { ID = 3, Title = "test3" }, 32 | }; 33 | 34 | mapper.Map(dtos, items).Should().BeSameAs(items); 35 | } 36 | 37 | [Fact] 38 | public void Should_Update_Existing_Item() 39 | { 40 | var mapper = CreateMapper(ConfigureMapper); 41 | 42 | var dtos = new List 43 | { 44 | new ThingDto { ID = 1, Title = "test0" }, 45 | new ThingDto { ID = 2, Title = "test2" } 46 | }; 47 | 48 | var items = new List 49 | { 50 | new Thing { ID = 1, Title = "test1" }, 51 | new Thing { ID = 3, Title = "test3" }, 52 | }; 53 | 54 | mapper.Map(dtos, items.ToList()).Should().HaveElementAt(0, items.First()); 55 | } 56 | 57 | [Fact] 58 | public void Should_Be_Fast_With_Large_Lists() 59 | { 60 | var mapper = CreateMapper(ConfigureMapper); 61 | 62 | var dtos = new object[100000].Select((_, i) => new ThingDto { ID = i }).ToList(); 63 | 64 | var items = new object[100000].Select((_, i) => new Thing { ID = i }).ToList(); 65 | 66 | mapper.Map(dtos, items.ToList()).Should().HaveElementAt(0, items.First()); 67 | } 68 | 69 | [Fact] 70 | public void Should_Be_Fast_With_Large_Reversed_Lists() 71 | { 72 | var mapper = CreateMapper(ConfigureMapper); 73 | 74 | var dtos = new object[100000].Select((_, i) => new ThingDto { ID = i }).ToList(); 75 | dtos.Reverse(); 76 | 77 | var items = new object[100000].Select((_, i) => new Thing { ID = i }).ToList(); 78 | 79 | mapper.Map(dtos, items.ToList()).Should().HaveElementAt(0, items.First()); 80 | } 81 | 82 | [Fact] 83 | public void Should_Be_Fast_With_Large_Lists_MultiProperty_Mapping() 84 | { 85 | var mapper = CreateMapper(x => 86 | { 87 | x.AddCollectionMappers(); 88 | x.CreateMap().EqualityComparison((ThingDto dto, Thing entity) => dto.ID == entity.ID && dto.ID == entity.ID); 89 | }); 90 | 91 | var dtos = new object[100000].Select((_, i) => new ThingDto { ID = i }).ToList(); 92 | 93 | var items = new object[100000].Select((_, i) => new Thing { ID = i }).ToList(); 94 | 95 | mapper.Map(dtos, items.ToList()).Should().HaveElementAt(0, items.First()); 96 | } 97 | 98 | [Fact] 99 | public void Should_Be_Fast_With_Large_Lists_MultiProperty_Mapping_Cant_Extract() 100 | { 101 | var mapper = CreateMapper(x => 102 | { 103 | x.AddCollectionMappers(); 104 | x.CreateMap().EqualityComparison((ThingDto dto, Thing entity) => dto.ID == entity.ID || dto.ID == entity.ID); 105 | }); 106 | 107 | var dtos = new object[100000].Select((_, i) => new ThingDto { ID = i }).ToList(); 108 | 109 | var items = new object[100000].Select((_, i) => new Thing { ID = i }).ToList(); 110 | 111 | mapper.Map(dtos, items.ToList()).Should().HaveElementAt(0, items.First()); 112 | } 113 | 114 | [Fact] 115 | public void Should_Be_Fast_With_Large_Lists_Cant_Extract_Negative() 116 | { 117 | var mapper = CreateMapper(x => 118 | { 119 | x.AddCollectionMappers(); 120 | // ReSharper disable once NegativeEqualityExpression 121 | x.CreateMap().EqualityComparison((ThingDto dto, Thing entity) => !(dto.ID != entity.ID)); 122 | }); 123 | 124 | var dtos = new object[100000].Select((_, i) => new ThingDto { ID = i }).ToList(); 125 | 126 | var items = new object[100000].Select((_, i) => new Thing { ID = i }).ToList(); 127 | 128 | mapper.Map(dtos, items.ToList()).Should().HaveElementAt(0, items.First()); 129 | } 130 | 131 | [Fact] 132 | public void Should_Be_Fast_With_Large_Lists_MultiProperty_Mapping_Cant_Extract_Negative() 133 | { 134 | var mapper = CreateMapper(x => 135 | { 136 | x.AddCollectionMappers(); 137 | // ReSharper disable once NegativeEqualityExpression 138 | x.CreateMap().EqualityComparison((ThingDto dto, Thing entity) => dto.ID == entity.ID && !(dto.ID != entity.ID)); 139 | }); 140 | 141 | var dtos = new object[100000].Select((_, i) => new ThingDto { ID = i }).ToList(); 142 | 143 | var items = new object[100000].Select((_, i) => new Thing { ID = i }).ToList(); 144 | 145 | mapper.Map(dtos, items.ToList()).Should().HaveElementAt(0, items.First()); 146 | } 147 | 148 | [Fact] 149 | public void Should_Be_Fast_With_Large_Lists_SubObject() 150 | { 151 | var mapper = CreateMapper(x => 152 | { 153 | x.AddCollectionMappers(); 154 | x.CreateMap().EqualityComparison((ThingDto source, Thing dest) => dest.ID == (source is ThingSubDto ? ((ThingSubDto)source).ID2 : source.ID)); 155 | }); 156 | 157 | var dtos = new object[100000].Select((_, i) => new ThingSubDto { ID = i + 100000 }).Cast().ToList(); 158 | 159 | var items = new object[100000].Select((_, i) => new Thing { ID = i }).ToList(); 160 | 161 | mapper.Map(dtos, items.ToList()).Should().HaveElementAt(0, items.First()); 162 | } 163 | 164 | [Fact] 165 | public void Should_Be_Fast_With_Large_Lists_SubObject_switch_left_and_right_expression() 166 | { 167 | var mapper = CreateMapper(x => 168 | { 169 | x.AddCollectionMappers(); 170 | x.CreateMap().EqualityComparison((ThingDto source, Thing dest) => (source is ThingSubDto ? ((ThingSubDto)source).ID2 : source.ID) == dest.ID); 171 | }); 172 | 173 | var dtos = new object[100000].Select((_, i) => new ThingSubDto { ID = i + 100000 }).ToList(); 174 | 175 | var items = new object[100000].Select((_, i) => new Thing { ID = i }).ToList(); 176 | 177 | mapper.Map(dtos, items.ToList()).Should().HaveElementAt(0, items.First()); 178 | } 179 | 180 | [Fact] 181 | public void Should_Work_With_Conditionals() 182 | { 183 | var mapper = CreateMapper(cfg => 184 | { 185 | cfg.AddCollectionMappers(); 186 | cfg.CreateMap() 187 | .ForMember(x => x.DtoId, m => m.Ignore()) 188 | .EqualityComparison((ClientDto src, Client dest) => dest.DtoId == 0 ? src.Code == dest.Code : src.Id == dest.DtoId); 189 | }); 190 | 191 | var dto = new ClientDto 192 | { 193 | Code = "abc", 194 | Id = 1 195 | }; 196 | var entity = new Client {Code = dto.Code, Id = 42}; 197 | var entityCollection = new List {entity}; 198 | 199 | mapper.Map(new[] { dto }, entityCollection); 200 | 201 | entity.ShouldBeEquivalentTo(entityCollection[0]); 202 | } 203 | 204 | public class Client 205 | { 206 | public long Id { get; set; } 207 | public string Code { get; set; } 208 | public long DtoId { get; set; } 209 | } 210 | 211 | public class ClientDto 212 | { 213 | public long Id { get; set; } 214 | public string Code { get; set; } 215 | } 216 | 217 | 218 | [Fact] 219 | public void Should_Work_With_Null_Destination() 220 | { 221 | var mapper = CreateMapper(ConfigureMapper); 222 | 223 | var dtos = new List 224 | { 225 | new ThingDto { ID = 1, Title = "test0" }, 226 | new ThingDto { ID = 2, Title = "test2" } 227 | }; 228 | 229 | mapper.Map>(dtos).Should().HaveSameCount(dtos); 230 | } 231 | 232 | [Fact] 233 | public void Should_Work_With_Comparing_String_Types() 234 | { 235 | var mapper = CreateMapper(cfg => 236 | { 237 | cfg.AddCollectionMappers(); 238 | cfg.CreateMap() 239 | .ForMember(d => d.SaleId, (IMemberConfigurationExpression o) => o.Ignore()) 240 | .EqualityComparison((Charge c, SaleCharge sc) => sc.Category == c.Category && sc.Description == c.Description); 241 | 242 | cfg.CreateMap() 243 | .ConstructUsing( 244 | (saleCharge => new Charge(saleCharge.Category, saleCharge.Description, saleCharge.Value))) 245 | .EqualityComparison((SaleCharge sc, Charge c) => sc.Category == c.Category && sc.Description == c.Description); 246 | }); 247 | 248 | var dto = new Charge("catagory", "description", 5); 249 | var entity = new SaleCharge { Category = dto.Category, Description = dto.Description }; 250 | var entityCollection = new List { entity }; 251 | 252 | mapper.Map(new[] { dto }, entityCollection); 253 | 254 | entity.ShouldBeEquivalentTo(entityCollection[0]); 255 | } 256 | 257 | public class Charge 258 | { 259 | public Charge(string category, string description, decimal value) 260 | { 261 | Category = category; 262 | Description = description; 263 | Value = value; 264 | } 265 | 266 | public string Category { get; } 267 | public string Description { get; } 268 | public decimal Value { get; } 269 | 270 | public override string ToString() 271 | { 272 | return $"{Category}|{Description}|{Value}"; 273 | } 274 | 275 | public override int GetHashCode() 276 | { 277 | return $"{Category}|{Description}|{Value}".GetHashCode(); 278 | } 279 | 280 | public override bool Equals(object obj) 281 | { 282 | if (ReferenceEquals(this, obj)) 283 | { 284 | return true; 285 | } 286 | 287 | if (ReferenceEquals(null, obj)) 288 | { 289 | return false; 290 | } 291 | 292 | var _obj = obj as Charge; 293 | 294 | if (_obj == null) 295 | { 296 | return false; 297 | } 298 | 299 | return Category == _obj.Category && Description == _obj.Description && Value == _obj.Value; 300 | } 301 | } 302 | 303 | public class SaleCharge 304 | { 305 | public Guid SaleId { get; set; } 306 | public string Category { get; set; } 307 | public string Description { get; set; } 308 | public decimal Value { get; set; } 309 | } 310 | 311 | [Fact] 312 | public void Should_Be_Instanced_Based() 313 | { 314 | var mapper = CreateMapper(x => 315 | { 316 | x.AddCollectionMappers(); 317 | x.CreateMap().ReverseMap(); 318 | }); 319 | 320 | var dtos = new List 321 | { 322 | new ThingDto { ID = 1, Title = "test0" }, 323 | new ThingDto { ID = 2, Title = "test2" } 324 | }; 325 | 326 | var items = new List 327 | { 328 | new Thing { ID = 1, Title = "test1" }, 329 | new Thing { ID = 3, Title = "test3" }, 330 | }; 331 | 332 | mapper.Map(dtos, items.ToList()).Should().NotContain(items.First()); 333 | } 334 | 335 | [Fact] 336 | public void Parent_Should_Be_Same_As_Root_Object() 337 | { 338 | var mapper = CreateMapper( 339 | cfg => 340 | { 341 | cfg.AddCollectionMappers(); 342 | cfg.CreateMap().PreserveReferences(); 343 | cfg.CreateMap() 344 | .EqualityComparison((src, dst) => src.ID == dst.ID).PreserveReferences(); 345 | }); 346 | 347 | var root = new ThingWithCollection() 348 | { 349 | Children = new List() 350 | }; 351 | root.Children.Add(new ThingCollectionItem() { ID = 1, Parent = root }); 352 | 353 | var target = new ThingWithCollection() { Children = new List() }; 354 | mapper.Map(root, target).Should().Be(target); 355 | 356 | target.Children.Count.Should().Be(1); 357 | target.Children.Single().Parent.Should().Be(target); 358 | } 359 | 360 | public class Thing 361 | { 362 | public int ID { get; set; } 363 | public string Title { get; set; } 364 | public override string ToString() { return Title; } 365 | } 366 | 367 | public class ThingSubDto : ThingDto 368 | { 369 | public int ID2 => ID - 100000; 370 | } 371 | 372 | public class ThingDto 373 | { 374 | public int ID { get; set; } 375 | public string Title { get; set; } 376 | } 377 | 378 | public class ThingWithCollection 379 | { 380 | public ICollection Children { get; set; } 381 | } 382 | 383 | public class ThingCollectionItem 384 | { 385 | public int ID { get; set; } 386 | public ThingWithCollection Parent { get; set; } 387 | } 388 | } 389 | } -------------------------------------------------------------------------------- /src/AutoMapper.Collection.Tests/MapCollectionWithEqualityThreadSafetyTests.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper.EquivalencyExpression; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Threading.Tasks; 5 | 6 | namespace AutoMapper.Collection 7 | { 8 | public class MapCollectionWithEqualityThreadSafetyTests 9 | { 10 | public async Task Should_Work_When_Initialized_Concurrently() 11 | { 12 | Action act = () => 13 | { 14 | new MapperConfiguration(cfg => 15 | { 16 | cfg.AddCollectionMappers(); 17 | }); 18 | }; 19 | var tasks = new List(); 20 | for (var i = 0; i < 5; i++) 21 | { 22 | tasks.Add(Task.Run(act)); 23 | } 24 | 25 | await Task.WhenAll(tasks.ToArray()); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/AutoMapper.Collection.Tests/MappingTestBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace AutoMapper.Collection 4 | { 5 | public abstract class MappingTestBase 6 | { 7 | protected IMapper CreateMapper(Action cfg) 8 | { 9 | var map = new MapperConfiguration(cfg); 10 | map.CompileMappings(); 11 | 12 | var mapper = map.CreateMapper(); 13 | mapper.ConfigurationProvider.AssertConfigurationIsValid(); 14 | return mapper; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/AutoMapper.Collection.Tests/NullableIdTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using AutoMapper.EquivalencyExpression; 3 | using FluentAssertions; 4 | using Xunit; 5 | 6 | namespace AutoMapper.Collection 7 | { 8 | public class NullableIdTests : MappingTestBase 9 | { 10 | [Fact] 11 | public void Should_Work_With_Null_Id() 12 | { 13 | var mapper = CreateMapper(x => 14 | { 15 | x.AddCollectionMappers(); 16 | x.CreateMap().EqualityComparison((dto, entity) => dto.ID == entity.ID); 17 | }); 18 | 19 | var original = new List 20 | { 21 | new ThingWithStringId { ID = "1", Title = "test0" }, 22 | new ThingWithStringId { ID = "2", Title = "test2" }, 23 | }; 24 | 25 | var dtos = new List 26 | { 27 | new ThingWithStringIdDto { ID = "1", Title = "test0" }, 28 | new ThingWithStringIdDto { ID = "2", Title = "test2" }, 29 | new ThingWithStringIdDto { Title = "test3" } 30 | }; 31 | 32 | mapper.Map(dtos, original); 33 | 34 | original.Should().HaveSameCount(dtos); 35 | } 36 | 37 | 38 | [Fact] 39 | public void Should_Work_With_Multiple_Null_Id() 40 | { 41 | var mapper = CreateMapper(x => 42 | { 43 | x.AddCollectionMappers(); 44 | x.CreateMap().EqualityComparison((dto, entity) => dto.ID == entity.ID); 45 | }); 46 | 47 | var original = new List 48 | { 49 | new ThingWithStringId { ID = "1", Title = "test0" }, 50 | new ThingWithStringId { ID = "2", Title = "test2" }, 51 | new ThingWithStringId { ID = "3", Title = "test3" }, 52 | }; 53 | 54 | var dtos = new List 55 | { 56 | new ThingWithStringIdDto { ID = "1", Title = "test0" }, 57 | new ThingWithStringIdDto { ID = "2", Title = "test2" }, 58 | new ThingWithStringIdDto { Title = "test3" }, 59 | new ThingWithStringIdDto { Title = "test4" }, 60 | }; 61 | 62 | mapper.Map(dtos, original); 63 | 64 | original.Should().HaveSameCount(dtos); 65 | } 66 | 67 | public class ThingWithStringId 68 | { 69 | public string ID { get; set; } 70 | public string Title { get; set; } 71 | public override string ToString() { return Title; } 72 | } 73 | 74 | public class ThingWithStringIdDto 75 | { 76 | public string ID { get; set; } 77 | public string Title { get; set; } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/AutoMapper.Collection.Tests/OptionsTests.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper.EquivalencyExpression; 2 | using AutoMapper.Mappers; 3 | using FluentAssertions; 4 | using System.Collections.Generic; 5 | using Xunit; 6 | 7 | namespace AutoMapper.Collection 8 | { 9 | public class OptionsTests : MappingTestBase 10 | { 11 | [Fact] 12 | public void Should_Retain_Options_Passed_In_Map() 13 | { 14 | var collectionTestValue = 0; 15 | var collectionMapper = CreateMapper(cfg => 16 | { 17 | cfg.AddCollectionMappers(); 18 | cfg.CreateMap().EqualityComparison((dto, entity) => dto.ID == entity.ID).AfterMap((_, __, ctx) => collectionTestValue = (int)ctx.Items["Test"]); 19 | }); 20 | 21 | var normalTestValue = 0; 22 | var normalMapper = CreateMapper(cfg => 23 | { 24 | cfg.CreateMap().AfterMap((_, __, ctx) => normalTestValue = (int)ctx.Items["Test"]); 25 | }); 26 | 27 | var dtos = new List 28 | { 29 | new ThingDto { ID = 1, Title = "test0" }, 30 | new ThingDto { ID = 2, Title = "test2" } 31 | }; 32 | 33 | var items = new List 34 | { 35 | new Thing { ID = 1, Title = "test1" }, 36 | new Thing { ID = 3, Title = "test3" }, 37 | }; 38 | 39 | collectionMapper.Map(dtos, items, opts => opts.Items.Add("Test", 1)); 40 | normalMapper.Map(dtos, items, opts => opts.Items.Add("Test", 1)); 41 | 42 | collectionTestValue.ShouldBeEquivalentTo(1); 43 | normalTestValue.ShouldBeEquivalentTo(1); 44 | } 45 | 46 | public class Thing 47 | { 48 | public int ID { get; set; } 49 | public string Title { get; set; } 50 | 51 | public override string ToString() 52 | { 53 | return Title; 54 | } 55 | } 56 | 57 | public class ThingDto 58 | { 59 | public int ID { get; set; } 60 | public string Title { get; set; } 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /src/AutoMapper.Collection.Tests/ValueTypeTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using AutoMapper.EquivalencyExpression; 7 | using Xunit; 8 | 9 | namespace AutoMapper.Collection 10 | { 11 | public class ValueTypeTests 12 | { 13 | [Fact] 14 | public void MapValueTypes() 15 | { 16 | var mapper = new Mapper(new MapperConfiguration(c => 17 | { 18 | c.AddCollectionMappers(); 19 | 20 | c.CreateMap() 21 | .ForMember(x => x.Nationalities, m => m.MapFrom(x => x.Persons)) 22 | .ReverseMap(); 23 | 24 | c.CreateMap() 25 | .EqualityComparison((src, dest) => dest.NationalityCountryId == src); 26 | })); 27 | 28 | var persons = new[] 29 | { 30 | new PersonNationality{PersonId = 1, NationalityCountryId = 101}, 31 | new PersonNationality{PersonId = 2, NationalityCountryId = 102}, 32 | new PersonNationality{PersonId = 3, NationalityCountryId = 103}, 33 | new PersonNationality{PersonId = 4, NationalityCountryId = 104}, 34 | }; 35 | 36 | var country = new Country { Persons = new List(persons) }; 37 | var countryDto = new CountryDto { Nationalities = new List { 104, 103, 105 } }; 38 | 39 | mapper.Map(countryDto, country); 40 | 41 | Assert.NotStrictEqual(new[] { persons[3], persons[2], country.Persons.Last() }, country.Persons); 42 | Assert.Equal(0, country.Persons.Last().PersonId); 43 | } 44 | 45 | public class PersonNationality 46 | { 47 | public int PersonId { get; set; } 48 | public int NationalityCountryId { get; set; } 49 | } 50 | 51 | public class Country 52 | { 53 | public IList Persons { get; set; } 54 | } 55 | 56 | public class CountryDto 57 | { 58 | public IList Nationalities { get; set; } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/AutoMapper.Collection/AutoMapper.Collection.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Collection Add/Remove/Update support for AutoMapper. AutoMapper.Collection adds EqualityComparison Expressions for TypeMaps to determine if Source and Destination type are equivalent to each other when mapping collections. 5 | Tyler Carlson 6 | net8.0 7 | AutoMapper.Collection 8 | AutoMapper.Collection 9 | icon.png 10 | https://github.com/AutoMapper/Automapper.Collection 11 | ../Key.snk 12 | true 13 | MIT 14 | true 15 | true 16 | v 17 | snupkg 18 | true 19 | true 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | all 30 | runtime; build; native; contentfiles; analyzers; buildtransitive 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/AutoMapper.Collection/Configuration/CollectionMappingExpressionFeature.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | using AutoMapper.Features; 4 | using AutoMapper.Collection.Runtime; 5 | using AutoMapper.EquivalencyExpression; 6 | 7 | namespace AutoMapper.Collection.Configuration 8 | { 9 | public class CollectionMappingExpressionFeature : IMappingFeature 10 | { 11 | private readonly Expression> _expression; 12 | 13 | public CollectionMappingExpressionFeature(Expression> expression) 14 | { 15 | _expression = expression; 16 | } 17 | 18 | public void Configure(TypeMap typeMap) 19 | { 20 | var equivalentExpression = new EquivalentExpression(_expression); 21 | typeMap.Features.Set(new CollectionMappingFeature(equivalentExpression)); 22 | } 23 | 24 | public IMappingFeature Reverse() 25 | { 26 | var reverseExpression = Expression.Lambda>(_expression.Body, _expression.Parameters[1], _expression.Parameters[0]); 27 | return new CollectionMappingExpressionFeature(reverseExpression); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/AutoMapper.Collection/Configuration/GeneratePropertyMapsExpressionFeature.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using AutoMapper.Collection.Runtime; 5 | using AutoMapper.EquivalencyExpression; 6 | using AutoMapper.Features; 7 | using AutoMapper.Internal; 8 | using AutoMapper.Mappers; 9 | 10 | namespace AutoMapper.Collection.Configuration 11 | { 12 | public class GeneratePropertyMapsExpressionFeature : IGlobalFeature 13 | { 14 | private readonly ObjectToEquivalencyExpressionByEquivalencyExistingMapper _mapper; 15 | private readonly List, IGeneratePropertyMaps>> _generators = new List, IGeneratePropertyMaps>>(); 16 | 17 | public GeneratePropertyMapsExpressionFeature(ObjectToEquivalencyExpressionByEquivalencyExistingMapper mapper) 18 | { 19 | _mapper = mapper; 20 | } 21 | 22 | public void Add(Func, IGeneratePropertyMaps> creator) 23 | { 24 | _generators.Add(creator); 25 | } 26 | 27 | void IGlobalFeature.Configure(IGlobalConfiguration configurationProvider) 28 | { 29 | _mapper.Configuration = configurationProvider; 30 | var generators = _generators 31 | .Select(x => x.Invoke(configurationProvider.ServiceCtor)) 32 | .ToList(); 33 | configurationProvider.Features.Set(new GeneratePropertyMapsFeature(generators)); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/AutoMapper.Collection/EquivalencyExpression/CustomExpressionVisitor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | using System.Reflection; 6 | 7 | namespace AutoMapper.EquivalencyExpression 8 | { 9 | internal class CustomExpressionVisitor : ExpressionVisitor 10 | { 11 | readonly ParameterExpression _parameter; 12 | private readonly IEnumerable _propertyMaps; 13 | 14 | internal CustomExpressionVisitor(ParameterExpression parameter, IEnumerable propertyMaps) 15 | { 16 | _parameter = parameter; 17 | _propertyMaps = propertyMaps; 18 | } 19 | 20 | protected override Expression VisitParameter(ParameterExpression node) 21 | { 22 | return _parameter; 23 | } 24 | 25 | protected override Expression VisitMember(MemberExpression node) 26 | { 27 | if (node.Member is PropertyInfo) 28 | { 29 | var matchPM = _propertyMaps.FirstOrDefault(pm => pm.DestinationMember == node.Member); 30 | if (matchPM == null) 31 | throw new Exception("No matching PropertyMap"); 32 | var memberGetters = matchPM.SourceMembers; 33 | 34 | var memberExpression = Expression.Property(Visit(node.Expression), memberGetters.First() as PropertyInfo); 35 | 36 | foreach (var memberGetter in memberGetters.Skip(1)) 37 | memberExpression = Expression.Property(memberExpression, memberGetter as PropertyInfo); 38 | return memberExpression; 39 | } 40 | return base.VisitMember(node); 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /src/AutoMapper.Collection/EquivalencyExpression/EquivalentExpression.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | using System.Reflection; 4 | using AutoMapper.Collection; 5 | 6 | namespace AutoMapper.EquivalencyExpression 7 | { 8 | internal class EquivalentExpression : IEquivalentComparer 9 | { 10 | internal static IEquivalentComparer BadValue { get; private set; } 11 | 12 | static EquivalentExpression() 13 | { 14 | BadValue = new EquivalentExpression(); 15 | } 16 | 17 | public int GetHashCode(object obj) 18 | { 19 | throw new Exception("How'd you get here"); 20 | } 21 | 22 | public bool IsEquivalent(object source, object destination) 23 | { 24 | return false; 25 | } 26 | } 27 | 28 | internal class EquivalentExpression : IEquivalentComparer 29 | { 30 | private readonly Expression> _equivalentExpression; 31 | private readonly Func _equivalentFunc; 32 | private readonly Func _sourceHashCodeFunc; 33 | private readonly Func _destinationHashCodeFunc; 34 | 35 | public EquivalentExpression(Expression> equivalentExpression) 36 | { 37 | _equivalentExpression = equivalentExpression; 38 | _equivalentFunc = _equivalentExpression.Compile(); 39 | 40 | var sourceParameter = equivalentExpression.Parameters[0]; 41 | var destinationParameter = equivalentExpression.Parameters[1]; 42 | 43 | var members = HashableExpressionsVisitor.Expand(sourceParameter, destinationParameter, equivalentExpression); 44 | 45 | _sourceHashCodeFunc = members.Item1.GetHashCodeExpression(sourceParameter).Compile(); 46 | _destinationHashCodeFunc = members.Item2.GetHashCodeExpression(destinationParameter).Compile(); 47 | } 48 | 49 | public bool IsEquivalent(object source, object destination) 50 | { 51 | 52 | if (source == null && destination == null) 53 | { 54 | return true; 55 | } 56 | 57 | if (source == null || destination == null) 58 | { 59 | return false; 60 | } 61 | 62 | if (!(source is TSource src) || !(destination is TDestination dest)) 63 | { 64 | return false; 65 | } 66 | 67 | return _equivalentFunc(src, dest); 68 | } 69 | 70 | public Expression> ToSingleSourceExpression(TSource source) 71 | { 72 | if (source == null) 73 | throw new Exception("Invalid somehow"); 74 | 75 | var expression = new ParametersToConstantVisitor(source).Visit(_equivalentExpression) as LambdaExpression; 76 | return Expression.Lambda>(expression.Body, _equivalentExpression.Parameters[1]); 77 | } 78 | 79 | public int GetHashCode(object obj) 80 | { 81 | if (obj is TSource src) 82 | return _sourceHashCodeFunc(src); 83 | if (obj is TDestination dest) 84 | return _destinationHashCodeFunc(dest); 85 | return default(int); 86 | } 87 | } 88 | 89 | internal class ParametersToConstantVisitor : ExpressionVisitor 90 | { 91 | private readonly T _value; 92 | 93 | public ParametersToConstantVisitor(T value) 94 | { 95 | _value = value; 96 | } 97 | 98 | protected override Expression VisitParameter(ParameterExpression node) 99 | { 100 | return node; 101 | } 102 | 103 | protected override Expression VisitMember(MemberExpression node) 104 | { 105 | if (node.Member is PropertyInfo && node.Member.DeclaringType.GetTypeInfo().IsAssignableFrom(typeof(T).GetTypeInfo())) 106 | { 107 | var memberExpression = Expression.Constant(node.Member.GetMemberValue(_value)); 108 | return memberExpression; 109 | } 110 | return base.VisitMember(node); 111 | } 112 | } 113 | } -------------------------------------------------------------------------------- /src/AutoMapper.Collection/EquivalencyExpression/EquivalentExpressions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Linq.Expressions; 4 | using AutoMapper.Collection.Configuration; 5 | using AutoMapper.Collection.Runtime; 6 | using AutoMapper.Internal; 7 | using AutoMapper.Internal.Mappers; 8 | using AutoMapper.Mappers; 9 | 10 | namespace AutoMapper.EquivalencyExpression 11 | { 12 | public static class EquivalentExpressions 13 | { 14 | public static void AddCollectionMappers(this IMapperConfigurationExpression cfg) 15 | { 16 | var mapper = new ObjectToEquivalencyExpressionByEquivalencyExistingMapper(); 17 | cfg.Internal().Features.Set(new GeneratePropertyMapsExpressionFeature(mapper)); 18 | cfg.Internal().InsertBefore( 19 | mapper, 20 | new EquivalentExpressionAddRemoveCollectionMapper()); 21 | } 22 | 23 | private static void InsertBefore(this IMapperConfigurationExpression cfg, params IObjectMapper[] adds) 24 | where TObjectMapper : IObjectMapper 25 | { 26 | var mappers = cfg.Internal().Mappers; 27 | var targetMapper = mappers.FirstOrDefault(om => om is TObjectMapper); 28 | var index = targetMapper == null ? 0 : mappers.IndexOf(targetMapper); 29 | foreach (var mapper in adds.Reverse()) 30 | { 31 | mappers.Insert(index, mapper); 32 | } 33 | } 34 | 35 | internal static IEquivalentComparer GetEquivalentExpression(this IObjectMapper mapper, Type sourceType, 36 | Type destinationType, IConfigurationProvider configuration) 37 | { 38 | var typeMap = configuration.Internal().ResolveTypeMap(sourceType, destinationType); 39 | if (typeMap == null) 40 | { 41 | return null; 42 | } 43 | 44 | var comparer = GetEquivalentExpression(configuration, typeMap); 45 | if (comparer == null) 46 | { 47 | foreach (var item in typeMap.IncludedBaseTypes) 48 | { 49 | var baseTypeMap = configuration.Internal().ResolveTypeMap(item.SourceType, item.DestinationType); 50 | if (baseTypeMap == null) 51 | { 52 | continue; 53 | } 54 | 55 | comparer = GetEquivalentExpression(configuration, baseTypeMap); 56 | if (comparer != null) 57 | { 58 | break; 59 | } 60 | } 61 | } 62 | return comparer; 63 | } 64 | 65 | internal static IEquivalentComparer GetEquivalentExpression(IConfigurationProvider configurationProvider, TypeMap typeMap) 66 | { 67 | return typeMap.Features.Get()?.EquivalentComparer 68 | ?? configurationProvider.Internal().Features.Get().Get(typeMap); 69 | } 70 | 71 | /// 72 | /// Make Comparison between and 73 | /// 74 | /// Compared type 75 | /// Type being compared to 76 | /// Base Mapping Expression 77 | /// Equivalent Expression between and 78 | /// 79 | public static IMappingExpression EqualityComparison(this IMappingExpression mappingExpression, Expression> EquivalentExpression) 80 | { 81 | mappingExpression.Features.Set(new CollectionMappingExpressionFeature(EquivalentExpression)); 82 | return mappingExpression; 83 | } 84 | 85 | public static void SetGeneratePropertyMaps(this IMapperConfigurationExpression cfg) 86 | where TGeneratePropertyMaps : IGeneratePropertyMaps 87 | { 88 | (cfg.Internal().Features.Get() 89 | ?? throw new ArgumentException("Invoke the IMapperConfigurationExpression.AddCollectionMappers() before adding IGeneratePropertyMaps.")) 90 | .Add(serviceCtor => (IGeneratePropertyMaps)serviceCtor(typeof(TGeneratePropertyMaps))); 91 | } 92 | 93 | public static void SetGeneratePropertyMaps(this IMapperConfigurationExpression cfg, IGeneratePropertyMaps generatePropertyMaps) 94 | { 95 | (cfg.Internal().Features.Get() 96 | ?? throw new ArgumentException("Invoke the IMapperConfigurationExpression.AddCollectionMappers() before adding IGeneratePropertyMaps.")) 97 | .Add(_ => generatePropertyMaps); 98 | } 99 | } 100 | } -------------------------------------------------------------------------------- /src/AutoMapper.Collection/EquivalencyExpression/ExpressionExtentions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Linq.Expressions; 5 | using System.Reflection; 6 | using AutoMapper.Collection; 7 | 8 | namespace AutoMapper.EquivalencyExpression 9 | { 10 | internal static class ExpressionExtentions 11 | { 12 | private static readonly ConcurrentDictionary _singleParameterTypeDictionary = new ConcurrentDictionary(); 13 | 14 | public static Type GetSinglePredicateExpressionArgumentType(this Type type) 15 | { 16 | return _singleParameterTypeDictionary.GetOrAdd(type, t => 17 | { 18 | var isExpression = typeof(Expression).GetTypeInfo().IsAssignableFrom(t.GetTypeInfo()); 19 | if (!isExpression) 20 | return null; 21 | 22 | var expressionOf = t.GetTypeInfo().GenericTypeArguments[0]; 23 | var isFunction = expressionOf.GetGenericTypeDefinition() == typeof(Func<,>); 24 | if (!isFunction) 25 | return null; 26 | 27 | var isPredicate = expressionOf.GetTypeInfo().GenericTypeArguments[1] == typeof(bool); 28 | if (!isPredicate) 29 | return null; 30 | 31 | var objType = expressionOf.GetTypeInfo().GenericTypeArguments[0]; 32 | return CacheAndReturnType(type, objType); 33 | }); 34 | } 35 | 36 | private static Type CacheAndReturnType(Type type, Type objType) 37 | { 38 | _singleParameterTypeDictionary.AddOrUpdate(type, objType, (_, __) => objType); 39 | return objType; 40 | } 41 | 42 | public static Expression> GetHashCodeExpression(this List members, ParameterExpression sourceParam) 43 | { 44 | var hashMultiply = Expression.Constant(397L); 45 | 46 | var hashVariable = Expression.Variable(typeof(long), "hashCode"); 47 | var returnTarget = Expression.Label(typeof(int)); 48 | var returnExpression = Expression.Return(returnTarget, Expression.Convert(hashVariable, typeof(int)), typeof(int)); 49 | var returnLabel = Expression.Label(returnTarget, Expression.Constant(-1)); 50 | 51 | var expressions = new List(); 52 | foreach (var member in members) 53 | { 54 | // Call the GetHashCode method 55 | var hasCodeExpression = Expression.Convert(Expression.Call(member, member.Type.GetDeclaredMethod(nameof(GetHashCode))), typeof(long)); 56 | 57 | // return (((object)x) == null ? 0 : x.GetHashCode()) 58 | var hashCodeReturnTarget = Expression.Label(typeof(long)); 59 | var hashCode = Expression.Block( 60 | Expression.IfThenElse( 61 | Expression.ReferenceEqual(Expression.Convert(member, typeof(object)), Expression.Constant(null)), 62 | Expression.Return(hashCodeReturnTarget, Expression.Constant(0L, typeof(long))), 63 | Expression.Return(hashCodeReturnTarget, hasCodeExpression)), 64 | Expression.Label(hashCodeReturnTarget, Expression.Constant(0L, typeof(long)))); 65 | 66 | if (expressions.Count == 0) 67 | { 68 | expressions.Add(Expression.Assign(hashVariable, hashCode)); 69 | } 70 | else 71 | { 72 | var oldHashMultiplied = Expression.Multiply(hashVariable, hashMultiply); 73 | var xOrHash = Expression.ExclusiveOr(oldHashMultiplied, hashCode); 74 | expressions.Add(Expression.Assign(hashVariable, xOrHash)); 75 | } 76 | } 77 | 78 | expressions.Add(returnExpression); 79 | expressions.Add(returnLabel); 80 | 81 | var resutltBlock = Expression.Block(new[] { hashVariable }, expressions); 82 | 83 | return Expression.Lambda>(resutltBlock, sourceParam); 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /src/AutoMapper.Collection/EquivalencyExpression/GenerateEquivilentExpressionFromTypeMap.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using System.Collections.Generic; 3 | using System.Linq.Expressions; 4 | 5 | namespace AutoMapper.EquivalencyExpression 6 | { 7 | internal class GenerateEquivalentExpressionFromTypeMap 8 | { 9 | private static readonly ConcurrentDictionary _EquivalentExpressionses = new ConcurrentDictionary(); 10 | internal static Expression GetExpression(TypeMap typeMap, object value) 11 | { 12 | return _EquivalentExpressionses.GetOrAdd(typeMap, t => new GenerateEquivalentExpressionFromTypeMap(t)) 13 | .CreateEquivalentExpression(value); 14 | } 15 | 16 | private readonly TypeMap _typeMap; 17 | 18 | private GenerateEquivalentExpressionFromTypeMap(TypeMap typeMap) 19 | { 20 | _typeMap = typeMap; 21 | } 22 | 23 | private Expression CreateEquivalentExpression(object value) 24 | { 25 | var express = value as LambdaExpression; 26 | var destExpr = Expression.Parameter(_typeMap.SourceType, express.Parameters[0].Name); 27 | 28 | var result = new CustomExpressionVisitor(destExpr, _typeMap.PropertyMaps).Visit(express.Body); 29 | 30 | return Expression.Lambda(result, destExpr); 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /src/AutoMapper.Collection/EquivalencyExpression/HashableExpressionsVisitor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | 6 | namespace AutoMapper.EquivalencyExpression 7 | { 8 | internal class HashableExpressionsVisitor : ExpressionVisitor 9 | { 10 | private readonly List _destinationMembers = new List(); 11 | private readonly ParameterExpression _destinationParameter; 12 | private readonly List _sourceMembers = new List(); 13 | private readonly ParameterExpression _sourceParameter; 14 | 15 | private readonly ParameterFinderVisitor _paramFinder = new ParameterFinderVisitor(); 16 | 17 | internal HashableExpressionsVisitor(ParameterExpression sourceParameter, ParameterExpression destinationParameter) 18 | { 19 | _sourceParameter = sourceParameter; 20 | _destinationParameter = destinationParameter; 21 | } 22 | 23 | internal static Tuple, List> Expand(ParameterExpression sourceParameter, ParameterExpression destinationParameter, Expression expression) 24 | { 25 | var visitor = new HashableExpressionsVisitor(sourceParameter, destinationParameter); 26 | visitor.Visit(expression); 27 | return Tuple.Create(visitor._sourceMembers, visitor._destinationMembers); 28 | } 29 | 30 | protected override Expression VisitConditional(ConditionalExpression node) 31 | { 32 | return node; 33 | } 34 | 35 | protected override Expression VisitBinary(BinaryExpression node) 36 | { 37 | switch (node.NodeType) 38 | { 39 | case ExpressionType.Equal: 40 | VisitCompare(node.Left, node.Right); 41 | break; 42 | case ExpressionType.And: 43 | case ExpressionType.AndAlso: 44 | return base.VisitBinary(node); 45 | case ExpressionType.Or: 46 | case ExpressionType.OrElse: 47 | return node; // Maybe compare 0r's for expression matching on side 48 | } 49 | 50 | return node; 51 | } 52 | 53 | private void VisitCompare(Expression leftNode, Expression rightNode) 54 | { 55 | _paramFinder.Visit(leftNode); 56 | var left = _paramFinder.Parameters; 57 | _paramFinder.Visit(rightNode); 58 | var right = _paramFinder.Parameters; 59 | 60 | if (left.All(p => p == _destinationParameter) && right.All(p => p == _sourceParameter)) 61 | { 62 | _sourceMembers.Add(rightNode); 63 | _destinationMembers.Add(leftNode); 64 | } 65 | if (left.All(p => p == _sourceParameter) && right.All(p => p == _destinationParameter)) 66 | { 67 | _sourceMembers.Add(leftNode); 68 | _destinationMembers.Add(rightNode); 69 | } 70 | } 71 | } 72 | 73 | internal class ParameterFinderVisitor : ExpressionVisitor 74 | { 75 | public IList Parameters { get; private set; } 76 | 77 | public override Expression Visit(Expression node) 78 | { 79 | Parameters = new List(); 80 | return base.Visit(node); 81 | } 82 | 83 | protected override Expression VisitParameter(ParameterExpression node) 84 | { 85 | Parameters.Add(node); 86 | return node; 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /src/AutoMapper.Collection/EquivalencyExpression/IEquivalentComparer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | 4 | namespace AutoMapper.EquivalencyExpression 5 | { 6 | public interface IEquivalentComparer 7 | { 8 | int GetHashCode(object obj); 9 | bool IsEquivalent(object source, object destination); 10 | } 11 | 12 | public interface IEquivalentComparer : IEquivalentComparer 13 | { 14 | Expression> ToSingleSourceExpression(TSource destination); 15 | } 16 | } -------------------------------------------------------------------------------- /src/AutoMapper.Collection/EquivalencyExpression/IGeneratePropertyMaps.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace AutoMapper.EquivalencyExpression 4 | { 5 | public interface IGeneratePropertyMaps 6 | { 7 | IEnumerable GeneratePropertyMaps(TypeMap typeMap); 8 | } 9 | } -------------------------------------------------------------------------------- /src/AutoMapper.Collection/Mappers/EquivalentExpressionAddRemoveCollectionMapper.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | using System.Reflection; 6 | using AutoMapper.Collection; 7 | using AutoMapper.EquivalencyExpression; 8 | using AutoMapper.Internal; 9 | using AutoMapper.Internal.Mappers; 10 | using static System.Linq.Expressions.Expression; 11 | using static AutoMapper.Execution.ExpressionBuilder; 12 | 13 | namespace AutoMapper.Mappers 14 | { 15 | public class EquivalentExpressionAddRemoveCollectionMapper : IObjectMapper 16 | { 17 | private readonly CollectionMapper _collectionMapper = new CollectionMapper(); 18 | 19 | public static TDestination Map(TSource source, TDestination destination, ResolutionContext context, IEquivalentComparer equivalentComparer) 20 | where TSource : IEnumerable 21 | where TDestination : ICollection 22 | { 23 | if (source == null || destination == null) 24 | { 25 | return destination; 26 | } 27 | 28 | var destList = destination.ToLookup(x => equivalentComparer.GetHashCode(x)).ToDictionary(x => x.Key, x => x.ToList()); 29 | 30 | var items = source.Select(x => 31 | { 32 | var sourceHash = equivalentComparer.GetHashCode(x); 33 | 34 | var item = default(TDestinationItem); 35 | if (destList.TryGetValue(sourceHash, out var itemList)) 36 | { 37 | item = itemList.FirstOrDefault(dest => equivalentComparer.IsEquivalent(x, dest)); 38 | if (item != null) 39 | { 40 | itemList.Remove(item); 41 | } 42 | } 43 | return new { SourceItem = x, DestinationItem = item }; 44 | }); 45 | 46 | foreach (var keypair in items) 47 | { 48 | if (keypair.DestinationItem == null) 49 | { 50 | destination.Add((TDestinationItem)context.Mapper.Map(keypair.SourceItem, null, typeof(TSourceItem), typeof(TDestinationItem))); 51 | } 52 | else 53 | { 54 | context.Mapper.Map(keypair.SourceItem, keypair.DestinationItem); 55 | } 56 | } 57 | 58 | foreach (var removedItem in destList.SelectMany(x => x.Value)) 59 | { 60 | destination.Remove(removedItem); 61 | } 62 | 63 | return destination; 64 | } 65 | 66 | private static readonly MethodInfo _mapMethodInfo = typeof(EquivalentExpressionAddRemoveCollectionMapper).GetRuntimeMethods().Single(x => x.IsStatic && x.Name == nameof(Map)); 67 | private static readonly ConcurrentDictionary _objectMapperCache = new ConcurrentDictionary(); 68 | 69 | public bool IsMatch(TypePair typePair) 70 | { 71 | return typePair.SourceType.IsEnumerableType() 72 | && typePair.DestinationType.IsCollectionType(); 73 | } 74 | 75 | public Expression MapExpression(IGlobalConfiguration configurationProvider, ProfileMap profileMap, MemberMap memberMap, 76 | Expression sourceExpression, Expression destExpression) 77 | { 78 | var sourceType = TypeHelper.GetElementType(sourceExpression.Type); 79 | var destType = TypeHelper.GetElementType(destExpression.Type); 80 | 81 | var equivalencyExpression = this.GetEquivalentExpression(sourceType, destType, configurationProvider); 82 | if (equivalencyExpression == null) 83 | { 84 | var typePair = new TypePair(sourceExpression.Type, destExpression.Type); 85 | return _objectMapperCache.GetOrAdd(typePair, _ => 86 | { 87 | var mappers = new List(configurationProvider.GetMappers()); 88 | for (var i = mappers.IndexOf(this) + 1; i < mappers.Count; i++) 89 | { 90 | var mapper = mappers[i]; 91 | if (mapper.IsMatch(typePair)) 92 | { 93 | return mapper; 94 | } 95 | } 96 | return _collectionMapper; 97 | }) 98 | .MapExpression(configurationProvider, profileMap, memberMap, sourceExpression, destExpression); 99 | } 100 | 101 | var method = _mapMethodInfo.MakeGenericMethod(sourceExpression.Type, sourceType, destExpression.Type, destType); 102 | var map = Call(null, method, sourceExpression, destExpression, ContextParameter, Constant(equivalencyExpression)); 103 | 104 | var notNull = NotEqual(destExpression, Constant(null)); 105 | var collectionMapperExpression = _collectionMapper.MapExpression(configurationProvider, profileMap, memberMap, sourceExpression, destExpression); 106 | return Condition(notNull, map, Convert(collectionMapperExpression, destExpression.Type)); 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/AutoMapper.Collection/Mappers/IConfigurationObjectMapper.cs: -------------------------------------------------------------------------------- 1 | //using AutoMapper.Internal.Mappers; 2 | 3 | //namespace AutoMapper.Mappers 4 | //{ 5 | // public interface IConfigurationObjectMapper : IObjectMapper 6 | // { 7 | // IConfigurationProvider ConfigurationProvider { get; set; } 8 | // } 9 | //} -------------------------------------------------------------------------------- /src/AutoMapper.Collection/Mappers/ObjectToEquivalencyExpressionByEquivalencyExistingMapper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Linq.Expressions; 4 | using System.Reflection; 5 | using AutoMapper.EquivalencyExpression; 6 | using AutoMapper.Internal; 7 | using AutoMapper.Internal.Mappers; 8 | using static System.Linq.Expressions.Expression; 9 | 10 | namespace AutoMapper.Mappers 11 | { 12 | public class ObjectToEquivalencyExpressionByEquivalencyExistingMapper : IObjectMapper 13 | { 14 | internal IConfigurationProvider Configuration { get; set; } 15 | 16 | public static Expression> Map(TSource source, IEquivalentComparer toSourceExpression) 17 | { 18 | return toSourceExpression.ToSingleSourceExpression(source); 19 | } 20 | 21 | private static readonly MethodInfo MapMethodInfo = typeof(ObjectToEquivalencyExpressionByEquivalencyExistingMapper).GetRuntimeMethods().First(_ => _.IsStatic); 22 | 23 | public bool IsMatch(TypePair typePair) 24 | { 25 | var destExpressArgType = typePair.DestinationType.GetSinglePredicateExpressionArgumentType(); 26 | if (destExpressArgType == null) 27 | return false; 28 | return this.GetEquivalentExpression(typePair.SourceType, destExpressArgType, Configuration) != null; 29 | } 30 | 31 | public Expression MapExpression(IGlobalConfiguration configurationProvider, ProfileMap profileMap, MemberMap memberMap, 32 | Expression sourceExpression, Expression destExpression) 33 | { 34 | var destExpressArgType = destExpression.Type.GetSinglePredicateExpressionArgumentType(); 35 | var toSourceExpression = this.GetEquivalentExpression(sourceExpression.Type, destExpressArgType, configurationProvider); 36 | return Call(null, MapMethodInfo.MakeGenericMethod(sourceExpression.Type, destExpressArgType), sourceExpression, Constant(toSourceExpression)); 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /src/AutoMapper.Collection/PrimitiveExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace AutoMapper.Collection 2 | { 3 | using System; 4 | using System.Collections; 5 | using System.Collections.Generic; 6 | using System.Reflection; 7 | 8 | internal static class PrimitiveExtensions 9 | { 10 | public static bool IsCollectionType(this Type type) => type.ImplementsGenericInterface(typeof(ICollection<>)); 11 | public static bool IsDictionaryType(this Type type) => type.ImplementsGenericInterface(typeof(IDictionary<,>)); 12 | public static bool IsEnumerableType(this Type type) => typeof(IEnumerable).IsAssignableFrom(type); 13 | 14 | private static bool ImplementsGenericInterface(this Type type, Type interfaceType) 15 | { 16 | if (type.IsGenericType(interfaceType)) 17 | { 18 | return true; 19 | } 20 | foreach (var @interface in type.GetTypeInfo().ImplementedInterfaces) 21 | { 22 | if (@interface.IsGenericType(interfaceType)) 23 | { 24 | return true; 25 | } 26 | } 27 | return false; 28 | } 29 | 30 | private static bool IsGenericType(this Type type, Type genericType) => type.IsGenericType() && type.GetGenericTypeDefinition() == genericType; 31 | } 32 | } -------------------------------------------------------------------------------- /src/AutoMapper.Collection/ReflectionHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | 4 | namespace AutoMapper.Collection 5 | { 6 | internal static class ReflectionHelper 7 | { 8 | public static object GetMemberValue(this MemberInfo propertyOrField, object target) 9 | { 10 | var property = propertyOrField as PropertyInfo; 11 | if (property != null) 12 | return property.GetValue(target, null); 13 | var field = propertyOrField as FieldInfo; 14 | if (field != null) 15 | return field.GetValue(target); 16 | throw Expected(propertyOrField); 17 | } 18 | 19 | private static ArgumentOutOfRangeException Expected(MemberInfo propertyOrField) 20 | => new ArgumentOutOfRangeException("propertyOrField", 21 | "Expected a property or field, not " + propertyOrField); 22 | 23 | public static Type GetMemberType(this MemberInfo memberInfo) 24 | { 25 | if (memberInfo is MethodInfo) 26 | return ((MethodInfo)memberInfo).ReturnType; 27 | if (memberInfo is PropertyInfo) 28 | return ((PropertyInfo)memberInfo).PropertyType; 29 | if (memberInfo is FieldInfo) 30 | return ((FieldInfo)memberInfo).FieldType; 31 | return null; 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /src/AutoMapper.Collection/Runtime/CollectionMappingFeature.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper.EquivalencyExpression; 2 | using AutoMapper.Features; 3 | using AutoMapper.Internal; 4 | 5 | namespace AutoMapper.Collection.Runtime 6 | { 7 | public class CollectionMappingFeature : IRuntimeFeature 8 | { 9 | public CollectionMappingFeature(IEquivalentComparer equivalentComparer) 10 | { 11 | EquivalentComparer = equivalentComparer; 12 | } 13 | 14 | public IEquivalentComparer EquivalentComparer { get; } 15 | 16 | void IRuntimeFeature.Seal(IGlobalConfiguration configurationProvider) 17 | { 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/AutoMapper.Collection/Runtime/GeneratePropertyMapsFeature.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Linq.Expressions; 6 | using System.Reflection; 7 | using AutoMapper.EquivalencyExpression; 8 | using AutoMapper.Features; 9 | using AutoMapper.Internal; 10 | 11 | namespace AutoMapper.Collection.Runtime 12 | { 13 | public class GeneratePropertyMapsFeature : IRuntimeFeature 14 | { 15 | private readonly IList _generators; 16 | private readonly ConcurrentDictionary _comparers = new ConcurrentDictionary(); 17 | 18 | public GeneratePropertyMapsFeature(List generators) 19 | { 20 | _generators = generators.AsReadOnly(); 21 | } 22 | 23 | public IEquivalentComparer Get(TypeMap typeMap) 24 | { 25 | return _comparers 26 | .GetOrAdd(typeMap.Types, _ => 27 | _generators 28 | .Select(x => CreateEquivalentExpression(x.GeneratePropertyMaps(typeMap))) 29 | .FirstOrDefault(x => x != null)); 30 | } 31 | 32 | void IRuntimeFeature.Seal(IGlobalConfiguration configurationProvider) 33 | { 34 | } 35 | 36 | private IEquivalentComparer CreateEquivalentExpression(IEnumerable propertyMaps) 37 | { 38 | if (!propertyMaps.Any() || propertyMaps.Any(pm => pm.DestinationMember.GetMemberType() != pm.SourceMember.GetMemberType())) 39 | { 40 | return null; 41 | } 42 | 43 | var typeMap = propertyMaps.First().TypeMap; 44 | var srcType = typeMap.SourceType; 45 | var destType = typeMap.DestinationType; 46 | var srcExpr = Expression.Parameter(srcType, "src"); 47 | var destExpr = Expression.Parameter(destType, "dest"); 48 | 49 | var equalExpr = propertyMaps.Select(pm => SourceEqualsDestinationExpression(pm, srcExpr, destExpr)).ToList(); 50 | if (equalExpr.Count == 0) 51 | { 52 | return EquivalentExpression.BadValue; 53 | } 54 | 55 | var finalExpression = equalExpr.Skip(1).Aggregate(equalExpr[0], Expression.And); 56 | 57 | var expr = Expression.Lambda(finalExpression, srcExpr, destExpr); 58 | var genericExpressionType = typeof(EquivalentExpression<,>).MakeGenericType(srcType, destType); 59 | return Activator.CreateInstance(genericExpressionType, expr) as IEquivalentComparer; 60 | } 61 | 62 | private BinaryExpression SourceEqualsDestinationExpression(PropertyMap propertyMap, Expression srcExpr, Expression destExpr) 63 | { 64 | var srcPropExpr = Expression.Property(srcExpr, propertyMap.SourceMember as PropertyInfo); 65 | var destPropExpr = Expression.Property(destExpr, propertyMap.DestinationMember as PropertyInfo); 66 | return Expression.Equal(srcPropExpr, destPropExpr); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/AutoMapper.Collection/TypeExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace AutoMapper.Collection 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Linq.Expressions; 7 | using System.Reflection; 8 | using System.Reflection.Emit; 9 | 10 | internal static class TypeExtensions 11 | { 12 | public static Type GetGenericTypeDefinitionIfGeneric(this Type type) 13 | { 14 | return type.IsGenericType() ? type.GetGenericTypeDefinition() : type; 15 | } 16 | 17 | public static Type[] GetGenericArguments(this Type type) 18 | { 19 | return type.GetTypeInfo().GenericTypeArguments; 20 | } 21 | 22 | public static Type[] GetGenericParameters(this Type type) 23 | { 24 | return type.GetGenericTypeDefinition().GetTypeInfo().GenericTypeParameters; 25 | } 26 | 27 | public static IEnumerable GetDeclaredConstructors(this Type type) 28 | { 29 | return type.GetTypeInfo().DeclaredConstructors; 30 | } 31 | 32 | #if NET45 33 | public static Type CreateType(this TypeBuilder type) 34 | { 35 | return type.CreateTypeInfo().AsType(); 36 | } 37 | #endif 38 | 39 | public static IEnumerable GetDeclaredMembers(this Type type) 40 | { 41 | return type.GetTypeInfo().DeclaredMembers; 42 | } 43 | 44 | public static IEnumerable GetAllMembers(this Type type) 45 | { 46 | while (true) 47 | { 48 | foreach (var memberInfo in type.GetTypeInfo().DeclaredMembers) 49 | { 50 | yield return memberInfo; 51 | } 52 | 53 | type = type.BaseType(); 54 | 55 | if (type == null) 56 | { 57 | yield break; 58 | } 59 | } 60 | } 61 | 62 | public static MemberInfo[] GetMember(this Type type, string name) 63 | { 64 | return type.GetAllMembers().Where(mi => mi.Name == name).ToArray(); 65 | } 66 | 67 | public static IEnumerable GetDeclaredMethods(this Type type) 68 | { 69 | return type.GetTypeInfo().DeclaredMethods; 70 | } 71 | 72 | public static MethodInfo GetDeclaredMethod(this Type type, string name) 73 | { 74 | return type.GetAllMethods().FirstOrDefault(mi => mi.Name == name); 75 | } 76 | 77 | public static MethodInfo GetDeclaredMethod(this Type type, string name, Type[] parameters) 78 | { 79 | return type 80 | .GetAllMethods() 81 | .Where(mi => mi.Name == name) 82 | .Where(mi => mi.GetParameters().Length == parameters.Length) 83 | .FirstOrDefault(mi => mi.GetParameters().Select(pi => pi.ParameterType).SequenceEqual(parameters)); 84 | } 85 | 86 | public static ConstructorInfo GetDeclaredConstructor(this Type type, Type[] parameters) 87 | { 88 | return type 89 | .GetTypeInfo() 90 | .DeclaredConstructors 91 | .Where(mi => mi.GetParameters().Length == parameters.Length) 92 | .FirstOrDefault(mi => mi.GetParameters().Select(pi => pi.ParameterType).SequenceEqual(parameters)); 93 | } 94 | 95 | public static IEnumerable GetAllMethods(this Type type) 96 | { 97 | return type.GetRuntimeMethods(); 98 | } 99 | 100 | public static IEnumerable GetDeclaredProperties(this Type type) 101 | { 102 | return type.GetTypeInfo().DeclaredProperties; 103 | } 104 | 105 | public static PropertyInfo GetDeclaredProperty(this Type type, string name) 106 | { 107 | return type.GetTypeInfo().GetDeclaredProperty(name); 108 | } 109 | 110 | public static object[] GetCustomAttributes(this Type type, Type attributeType, bool inherit) 111 | { 112 | return type.GetTypeInfo().GetCustomAttributes(attributeType, inherit).ToArray(); 113 | } 114 | 115 | public static bool IsStatic(this FieldInfo fieldInfo) 116 | { 117 | return fieldInfo?.IsStatic ?? false; 118 | } 119 | 120 | public static bool IsStatic(this PropertyInfo propertyInfo) 121 | { 122 | return propertyInfo?.GetGetMethod(true)?.IsStatic 123 | ?? propertyInfo?.GetSetMethod(true)?.IsStatic 124 | ?? false; 125 | } 126 | 127 | public static bool IsStatic(this MemberInfo memberInfo) 128 | { 129 | return (memberInfo as FieldInfo).IsStatic() 130 | || (memberInfo as PropertyInfo).IsStatic() 131 | || ((memberInfo as MethodInfo)?.IsStatic 132 | ?? false); 133 | } 134 | 135 | public static bool IsPublic(this PropertyInfo propertyInfo) 136 | { 137 | return (propertyInfo?.GetGetMethod(true)?.IsPublic ?? false) 138 | || (propertyInfo?.GetSetMethod(true)?.IsPublic ?? false); 139 | } 140 | 141 | public static IEnumerable PropertiesWithAnInaccessibleSetter(this Type type) 142 | { 143 | return type.GetDeclaredProperties().Where(pm => pm.HasAnInaccessibleSetter()); 144 | } 145 | 146 | public static bool HasAnInaccessibleSetter(this PropertyInfo property) 147 | { 148 | var setMethod = property.GetSetMethod(true); 149 | return setMethod == null || setMethod.IsPrivate || setMethod.IsFamily; 150 | } 151 | 152 | public static bool IsPublic(this MemberInfo memberInfo) 153 | { 154 | return (memberInfo as FieldInfo)?.IsPublic ?? (memberInfo as PropertyInfo).IsPublic(); 155 | } 156 | 157 | public static bool IsNotPublic(this ConstructorInfo constructorInfo) 158 | { 159 | return constructorInfo.IsPrivate 160 | || constructorInfo.IsFamilyAndAssembly 161 | || constructorInfo.IsFamilyOrAssembly 162 | || constructorInfo.IsFamily; 163 | } 164 | 165 | public static Assembly Assembly(this Type type) 166 | { 167 | return type.GetTypeInfo().Assembly; 168 | } 169 | 170 | public static Type BaseType(this Type type) 171 | { 172 | return type.GetTypeInfo().BaseType; 173 | } 174 | 175 | public static bool IsAssignableFrom(this Type type, Type other) 176 | { 177 | return type.GetTypeInfo().IsAssignableFrom(other.GetTypeInfo()); 178 | } 179 | 180 | public static bool IsAbstract(this Type type) 181 | { 182 | return type.GetTypeInfo().IsAbstract; 183 | } 184 | 185 | public static bool IsClass(this Type type) 186 | { 187 | return type.GetTypeInfo().IsClass; 188 | } 189 | 190 | public static bool IsEnum(this Type type) 191 | { 192 | return type.GetTypeInfo().IsEnum; 193 | } 194 | 195 | public static bool IsGenericType(this Type type) 196 | { 197 | return type.GetTypeInfo().IsGenericType; 198 | } 199 | 200 | public static bool IsGenericTypeDefinition(this Type type) 201 | { 202 | return type.GetTypeInfo().IsGenericTypeDefinition; 203 | } 204 | 205 | public static bool IsInterface(this Type type) 206 | { 207 | return type.GetTypeInfo().IsInterface; 208 | } 209 | 210 | public static bool IsPrimitive(this Type type) 211 | { 212 | return type.GetTypeInfo().IsPrimitive; 213 | } 214 | 215 | public static bool IsSealed(this Type type) 216 | { 217 | return type.GetTypeInfo().IsSealed; 218 | } 219 | 220 | public static bool IsValueType(this Type type) 221 | { 222 | return type.GetTypeInfo().IsValueType; 223 | } 224 | 225 | public static bool IsInstanceOfType(this Type type, object o) 226 | { 227 | return o != null && type.GetTypeInfo().IsAssignableFrom(o.GetType().GetTypeInfo()); 228 | } 229 | 230 | public static ConstructorInfo[] GetConstructors(this Type type) 231 | { 232 | return type.GetTypeInfo().DeclaredConstructors.ToArray(); 233 | } 234 | 235 | public static PropertyInfo[] GetProperties(this Type type) 236 | { 237 | return type.GetRuntimeProperties().ToArray(); 238 | } 239 | 240 | public static MethodInfo GetGetMethod(this PropertyInfo propertyInfo, bool ignored) 241 | { 242 | return propertyInfo.GetMethod; 243 | } 244 | 245 | public static MethodInfo GetSetMethod(this PropertyInfo propertyInfo, bool ignored) 246 | { 247 | return propertyInfo.SetMethod; 248 | } 249 | 250 | public static FieldInfo GetField(this Type type, string name) 251 | { 252 | return type.GetRuntimeField(name); 253 | } 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /src/AutoMapper.Collection/TypeHelper.cs: -------------------------------------------------------------------------------- 1 | namespace AutoMapper.Collection 2 | { 3 | using System; 4 | using System.Collections; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Reflection; 8 | 9 | internal static class TypeHelper 10 | { 11 | public static Type GetElementType(Type enumerableType) => GetElementTypes(enumerableType, null)[0]; 12 | 13 | private static Type[] GetElementTypes(Type enumerableType, IEnumerable enumerable, 14 | ElemntTypeFlags flags = ElemntTypeFlags.None) 15 | { 16 | if (enumerableType.HasElementType) 17 | { 18 | return new[] { enumerableType.GetElementType() }; 19 | } 20 | 21 | if (flags.HasFlag(ElemntTypeFlags.BreakKeyValuePair) && enumerableType.IsGenericType() && 22 | enumerableType.IsDictionaryType()) 23 | { 24 | return enumerableType.GetTypeInfo().GenericTypeArguments; 25 | } 26 | 27 | if (enumerableType.IsGenericType() && 28 | enumerableType.GetGenericTypeDefinition() == typeof(IEnumerable<>)) 29 | { 30 | return enumerableType.GetTypeInfo().GenericTypeArguments; 31 | } 32 | 33 | Type ienumerableType = GetIEnumerableType(enumerableType); 34 | if (ienumerableType != null) 35 | { 36 | return ienumerableType.GetTypeInfo().GenericTypeArguments; 37 | } 38 | 39 | if (typeof(IEnumerable).IsAssignableFrom(enumerableType)) 40 | { 41 | var first = enumerable?.Cast().FirstOrDefault(); 42 | 43 | return new[] { first?.GetType() ?? typeof(object) }; 44 | } 45 | 46 | throw new ArgumentException($"Unable to find the element type for type '{enumerableType}'.", 47 | nameof(enumerableType)); 48 | } 49 | 50 | private static Type GetIEnumerableType(Type enumerableType) 51 | { 52 | try 53 | { 54 | return enumerableType.GetTypeInfo().ImplementedInterfaces.FirstOrDefault(t => t.Name == "IEnumerable`1"); 55 | } 56 | catch (AmbiguousMatchException) 57 | { 58 | return enumerableType.BaseType() != typeof(object) ? GetIEnumerableType(enumerableType.BaseType()) : null; 59 | } 60 | } 61 | } 62 | 63 | public enum ElemntTypeFlags 64 | { 65 | None = 0, 66 | BreakKeyValuePair = 1 67 | } 68 | } -------------------------------------------------------------------------------- /src/Key.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AutoMapper/AutoMapper.Collection/eafbf005becf73f989e1b1c0d52e8699439f837c/src/Key.snk -------------------------------------------------------------------------------- /version.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10.0.0 4 | 5 | 6 | --------------------------------------------------------------------------------