diff --git a/.editorconfig b/.editorconfig index 74089b2787..2524b768e6 100644 --- a/.editorconfig +++ b/.editorconfig @@ -6,6 +6,8 @@ end_of_line = lf indent_style = space indent_size = 4 +csharp_space_around_binary_operators = before_and_after + #### Naming styles #### # Constants are PascalCase @@ -84,6 +86,9 @@ dotnet_style_namespace_match_folder = true:silent dotnet_style_explicit_tuple_names = true:suggestion dotnet_style_prefer_inferred_tuple_names = true:suggestion dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +csharp_style_prefer_primary_constructors = true:suggestion +csharp_prefer_system_threading_lock = true:suggestion +csharp_space_after_keywords_in_control_flow_statements = true [*.xml] indent_size = 2 @@ -92,4 +97,7 @@ indent_size = 2 indent_size = 2 [*.csproj] +indent_size = 2 + +[*.yml] indent_size = 2 \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cc9b02ee10..cc829bc373 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,11 +3,14 @@ name: CI on: push: branches: - - master + - main pull_request: - branches: - - master +env: + DOTNET_NOLOGO: true + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + MINVERBUILDMETADATA: build.${{ github.run_id }}.${{ github.run_attempt}} permissions: + id-token: write contents: read concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} @@ -16,26 +19,64 @@ jobs: build: strategy: fail-fast: false - runs-on: windows-latest + runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4.2.0 with: fetch-depth: 0 - - name: Install SQL Local DB - run: ./Setup.ps1 - shell: pwsh + - name: Setup dotnet + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 8.0.x + 9.0.x + 10.0.x - name: Build and Test run: ./Build.ps1 shell: pwsh - - name: Push to MyGet - env: - NUGET_URL: https://www.myget.org/F/automapperdev/api/v3/index.json - NUGET_API_KEY: ${{ secrets.MYGET_CI_API_KEY }} - run: ./Push.ps1 - shell: pwsh - - name: Artifacts - uses: actions/upload-artifact@v2 - with: - name: artifacts - path: artifacts/**/* \ No newline at end of file + build-windows: + needs: build + strategy: + fail-fast: false + runs-on: windows-latest + steps: + - name: Checkout + uses: actions/checkout@v4.2.0 + with: + fetch-depth: 0 + - name: Azure Login via OIDC + uses: azure/login@v2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + - name: Setup dotnet + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 8.0.x + 9.0.x + 10.0.x + - name: Install NuGetKeyVaultSignTool + run: dotnet tool install --global NuGetKeyVaultSignTool + - name: Build and Test + run: ./Build.ps1 + shell: pwsh + - name: Sign packages + run: |- + foreach ($f in Get-ChildItem "./artifacts" -Filter "*.nupkg") { + NuGetKeyVaultSignTool sign $f.FullName --file-digest sha256 --timestamp-rfc3161 http://timestamp.digicert.com --azure-key-vault-managed-identity --azure-key-vault-url ${{ secrets.AZURE_KEYVAULT_URI }} --azure-key-vault-certificate ${{ secrets.CODESIGN_CERT_NAME }} + } + - name: Push to MyGet + if: github.ref == 'refs/heads/main' + env: + NUGET_URL: https://f.feedz.io/lucky-penny-software/automapper/nuget/index.json + NUGET_API_KEY: ${{ secrets.FEEDZIO_ACCESS_TOKEN }} + run: ./Push.ps1 + shell: pwsh + - name: Artifacts + uses: actions/upload-artifact@v4 + with: + name: artifacts + path: artifacts/**/* diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml index ad2421a611..c76bec743d 100644 --- a/.github/workflows/lock.yml +++ b/.github/workflows/lock.yml @@ -3,6 +3,7 @@ name: 'Lock threads' on: schedule: - cron: '0 0 * * 0' + workflow_dispatch: permissions: contents: read @@ -12,18 +13,21 @@ jobs: permissions: issues: write # for dessant/lock-threads to lock issues pull-requests: write # for dessant/lock-threads to lock PRs + discussions: write # for dessant/lock-threads to lock discussions runs-on: ubuntu-latest steps: - - uses: dessant/lock-threads@v2 + - uses: dessant/lock-threads@v5 with: - github-token: ${{ github.token }} - issue-lock-inactive-days: 31 - pr-lock-inactive-days: 31 - issue-lock-comment: > + issue-inactive-days: 31 + pr-inactive-days: 31 + discussion-inactive-days: 31 + issue-comment: > This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. - pr-lock-comment: > + pr-comment: > This pull request has been automatically locked since there has not been any recent activity after it was closed. - Please open a new issue for related bugs. + discussion-comment: > + This discussion has been automatically locked since there + has not been any recent activity after it was closed. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index affbcbd18c..3204a42176 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,42 +1,78 @@ name: Release - on: push: tags: - - '*.*.*' + - "*.*.*" permissions: + id-token: write contents: read - jobs: build: + strategy: + fail-fast: false + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Setup dotnet + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 8.0.x + 9.0.x + 10.0.x + - name: Build and Test + run: ./Build.ps1 + shell: pwsh + build-windows: + needs: build strategy: fail-fast: false runs-on: windows-latest steps: - - name: Checkout - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - name: Install SQL Local DB - run: ./Setup.ps1 - shell: pwsh - - name: Build and Test - run: ./Build.ps1 - shell: pwsh - - name: Push to MyGet - env: - NUGET_URL: https://www.myget.org/F/automapperdev/api/v3/index.json - NUGET_API_KEY: ${{ secrets.MYGET_CI_API_KEY }} - run: ./Push.ps1 - shell: pwsh - - name: Push to NuGet - env: - NUGET_URL: https://api.nuget.org/v3/index.json - NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} - run: ./Push.ps1 - shell: pwsh - - name: Artifacts - uses: actions/upload-artifact@v2 - with: - name: artifacts - path: artifacts/**/* \ No newline at end of file + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Azure Login via OIDC + uses: azure/login@v2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + - name: Setup dotnet + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 8.0.x + 9.0.x + 10.0.x + - name: Install NuGetKeyVaultSignTool + run: dotnet tool install --global NuGetKeyVaultSignTool + - name: Build and Test + run: ./Build.ps1 + shell: pwsh + - name: Sign packages + run: |- + foreach ($f in Get-ChildItem "./artifacts" -Filter "*.nupkg") { + NuGetKeyVaultSignTool sign $f.FullName --file-digest sha256 --timestamp-rfc3161 http://timestamp.digicert.com --azure-key-vault-managed-identity --azure-key-vault-url ${{ secrets.AZURE_KEYVAULT_URI }} --azure-key-vault-certificate ${{ secrets.CODESIGN_CERT_NAME }} + } + - name: Push to MyGet + env: + NUGET_URL: https://f.feedz.io/lucky-penny-software/automapper/nuget/index.json + NUGET_API_KEY: ${{ secrets.FEEDZIO_ACCESS_TOKEN }} + run: ./Push.ps1 + shell: pwsh + - name: Push to NuGet + env: + NUGET_URL: https://api.nuget.org/v3/index.json + NUGET_API_KEY: ${{ secrets.AUTOMAPPER_NUGET_API_KEY }} + run: ./Push.ps1 + shell: pwsh + - name: Artifacts + uses: actions/upload-artifact@v4 + with: + name: artifacts + path: artifacts/**/* diff --git a/.gitignore b/.gitignore index da0ff61b78..8915351bf8 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ packages .nuget project.lock.json .vs +.DS_Store # JetBrains Rider .idea/ diff --git a/.readthedocs.yml b/.readthedocs.yml index af59f269aa..3a7e5af642 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1 +1,13 @@ -requirements_file: docs/requirements.txt +version: "2" + +build: + os: "ubuntu-22.04" + tools: + python: "3.10" + +python: + install: + - requirements: docs/requirements.txt + +sphinx: + configuration: docs/source/conf.py \ No newline at end of file diff --git a/AutoMapper.WindowsCI.slnf b/AutoMapper.WindowsCI.slnf new file mode 100644 index 0000000000..7cf49bf142 --- /dev/null +++ b/AutoMapper.WindowsCI.slnf @@ -0,0 +1,10 @@ +{ + "solution": { + "path": "AutoMapper.sln", + "projects": [ + "src\\AutoMapper\\AutoMapper.csproj", + "src\\AutoMapper.DI.Tests\\AutoMapper.DI.Tests.csproj", + "src\\UnitTests\\AutoMapper.UnitTests.csproj" + ] + } +} diff --git a/AutoMapper.sln b/AutoMapper.sln index 92bcf049c6..d2fb09b5a1 100644 --- a/AutoMapper.sln +++ b/AutoMapper.sln @@ -15,7 +15,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Push.ps1 = Push.ps1 README.md = README.md .github\workflows\release.yml = .github\workflows\release.yml - Setup.ps1 = Setup.ps1 + AutoMapper.snk = AutoMapper.snk EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Benchmark", "src\Benchmark\Benchmark.csproj", "{B8051389-CB47-46FB-B234-9D49506704AA}" @@ -26,6 +26,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoMapper.UnitTests", "src EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoMapper.IntegrationTests", "src\IntegrationTests\AutoMapper.IntegrationTests.csproj", "{24B47F4C-0035-4F29-AAD9-4C47E1AAD98E}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoMapper.DI.Tests", "src\AutoMapper.DI.Tests\AutoMapper.DI.Tests.csproj", "{BEBD620A-8BAA-463F-BE0F-8319AD3C1644}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestApp", "src\TestApp\TestApp.csproj", "{35CED3AE-B825-4703-992D-A58B5BE646DC}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -96,6 +100,38 @@ Global {24B47F4C-0035-4F29-AAD9-4C47E1AAD98E}.Release|x64.Build.0 = Release|Any CPU {24B47F4C-0035-4F29-AAD9-4C47E1AAD98E}.Release|x86.ActiveCfg = Release|Any CPU {24B47F4C-0035-4F29-AAD9-4C47E1AAD98E}.Release|x86.Build.0 = Release|Any CPU + {BEBD620A-8BAA-463F-BE0F-8319AD3C1644}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BEBD620A-8BAA-463F-BE0F-8319AD3C1644}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BEBD620A-8BAA-463F-BE0F-8319AD3C1644}.Debug|ARM.ActiveCfg = Debug|Any CPU + {BEBD620A-8BAA-463F-BE0F-8319AD3C1644}.Debug|ARM.Build.0 = Debug|Any CPU + {BEBD620A-8BAA-463F-BE0F-8319AD3C1644}.Debug|x64.ActiveCfg = Debug|Any CPU + {BEBD620A-8BAA-463F-BE0F-8319AD3C1644}.Debug|x64.Build.0 = Debug|Any CPU + {BEBD620A-8BAA-463F-BE0F-8319AD3C1644}.Debug|x86.ActiveCfg = Debug|Any CPU + {BEBD620A-8BAA-463F-BE0F-8319AD3C1644}.Debug|x86.Build.0 = Debug|Any CPU + {BEBD620A-8BAA-463F-BE0F-8319AD3C1644}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BEBD620A-8BAA-463F-BE0F-8319AD3C1644}.Release|Any CPU.Build.0 = Release|Any CPU + {BEBD620A-8BAA-463F-BE0F-8319AD3C1644}.Release|ARM.ActiveCfg = Release|Any CPU + {BEBD620A-8BAA-463F-BE0F-8319AD3C1644}.Release|ARM.Build.0 = Release|Any CPU + {BEBD620A-8BAA-463F-BE0F-8319AD3C1644}.Release|x64.ActiveCfg = Release|Any CPU + {BEBD620A-8BAA-463F-BE0F-8319AD3C1644}.Release|x64.Build.0 = Release|Any CPU + {BEBD620A-8BAA-463F-BE0F-8319AD3C1644}.Release|x86.ActiveCfg = Release|Any CPU + {BEBD620A-8BAA-463F-BE0F-8319AD3C1644}.Release|x86.Build.0 = Release|Any CPU + {35CED3AE-B825-4703-992D-A58B5BE646DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {35CED3AE-B825-4703-992D-A58B5BE646DC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {35CED3AE-B825-4703-992D-A58B5BE646DC}.Debug|ARM.ActiveCfg = Debug|Any CPU + {35CED3AE-B825-4703-992D-A58B5BE646DC}.Debug|ARM.Build.0 = Debug|Any CPU + {35CED3AE-B825-4703-992D-A58B5BE646DC}.Debug|x64.ActiveCfg = Debug|Any CPU + {35CED3AE-B825-4703-992D-A58B5BE646DC}.Debug|x64.Build.0 = Debug|Any CPU + {35CED3AE-B825-4703-992D-A58B5BE646DC}.Debug|x86.ActiveCfg = Debug|Any CPU + {35CED3AE-B825-4703-992D-A58B5BE646DC}.Debug|x86.Build.0 = Debug|Any CPU + {35CED3AE-B825-4703-992D-A58B5BE646DC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {35CED3AE-B825-4703-992D-A58B5BE646DC}.Release|Any CPU.Build.0 = Release|Any CPU + {35CED3AE-B825-4703-992D-A58B5BE646DC}.Release|ARM.ActiveCfg = Release|Any CPU + {35CED3AE-B825-4703-992D-A58B5BE646DC}.Release|ARM.Build.0 = Release|Any CPU + {35CED3AE-B825-4703-992D-A58B5BE646DC}.Release|x64.ActiveCfg = Release|Any CPU + {35CED3AE-B825-4703-992D-A58B5BE646DC}.Release|x64.Build.0 = Release|Any CPU + {35CED3AE-B825-4703-992D-A58B5BE646DC}.Release|x86.ActiveCfg = Release|Any CPU + {35CED3AE-B825-4703-992D-A58B5BE646DC}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Build.ps1 b/Build.ps1 index f3c142e64f..b37ce69612 100644 --- a/Build.ps1 +++ b/Build.ps1 @@ -26,6 +26,18 @@ $artifacts = ".\artifacts" if(Test-Path $artifacts) { Remove-Item $artifacts -Force -Recurse } -exec { & dotnet test -c Release --results-directory $artifacts -l trx --verbosity=normal } +if ($env:GITHUB_ACTIONS -eq 'true' -and $env:RUNNER_OS -eq 'Windows') { + Write-Host "✅ Running inside GitHub Actions on a Windows runner" + $solution = "./AutoMapper.WindowsCI.slnf" +} +else { + Write-Host "🖥️ Running locally or on a different platform" + $solution = "./AutoMapper.sln" +} -exec { & dotnet pack .\src\AutoMapper\AutoMapper.csproj -c Release -o $artifacts --no-build } +exec { & dotnet test $solution --configuration Release --results-directory $artifacts --logger trx } + +# Only pack AutoMapper project on Windows runners in GitHub Actions +if ($env:GITHUB_ACTIONS -eq 'true' -and $env:RUNNER_OS -eq 'Windows') { + exec { & dotnet pack .\src\AutoMapper\AutoMapper.csproj --configuration Release --output $artifacts --no-build } +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ae47413075..8e2323c194 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,7 @@ Then we can decide if and how a feature or a change could be implemented and if Also read this first: [Being a good open source citizen](https://hackernoon.com/being-a-good-open-source-citizen-9060d0ab9732#.x3hocgw85) ## General feedback and discussions -Please start a discussion on the [core repo issue tracker](https://github.com/AutoMapper/AutoMapper/issues). +Please start a discussion on the [core repo issue tracker](https://github.com/LuckyPennySoftware/AutoMapper/issues). ## Platform AutoMapper is built using the RTM tooling that ships with the latest Visual Studio. This is the only configuration accepted. @@ -18,14 +18,10 @@ Run the PowerShell script `Build.ps1` from the command line. This builds and run ## Bugs and feature requests? Please log a new issue in the appropriate GitHub repo: -* [Core](https://github.com/AutoMapper/AutoMapper) +* [Core](https://github.com/LuckyPennySoftware/AutoMapper) * [EF6 Extensions](https://github.com/AutoMapper/AutoMapper.EF6) * [IDataReader/Record Extensions](https://github.com/AutoMapper/AutoMapper.Data) * [Collection Extensions](https://github.com/AutoMapper/AutoMapper.Collection) -* [Microsoft DI Extensions](https://github.com/AutoMapper/AutoMapper.Extensions.Microsoft.DependencyInjection) - -## Other discussions -https://gitter.im/AutoMapper/AutoMapper ## Filing issues The best way to get your bug fixed is to be as detailed as you can be about the problem. @@ -39,11 +35,10 @@ Here are questions you can answer before you file a bug to make sure you're not GitHub supports [markdown](https://github.github.com/github-flavored-markdown/), so when filing bugs make sure you check the formatting before clicking submit. ## Contributing code and content -You will need to sign a [Contributor License Agreement](https://cla.dotnetfoundation.org/) before submitting your pull request. Make sure you can build the code. Familiarize yourself with the project workflow and our coding conventions. If you don't know what a pull request is read this article: https://help.github.com/articles/using-pull-requests. -**We only accept PRs to the master branch.** +**We only accept PRs to the main branch.** Before submitting a feature or substantial code contribution please discuss it with the team and ensure it follows the product roadmap. Here's a list of blog posts that are worth reading before doing a pull request: diff --git a/Directory.Build.props b/Directory.Build.props index 52fa6c08a1..9b4a5ab2dd 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -9,6 +9,15 @@ enable + + $([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::get_OSX()))) + $([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::get_Windows()))) + + + + $(DefineConstants);FULL_OR_STANDARD + + diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md index b4ca933abe..9f2fa003eb 100644 --- a/ISSUE_TEMPLATE.md +++ b/ISSUE_TEMPLATE.md @@ -1,8 +1,8 @@ diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000000..8d5c9bce5d --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,10 @@ +By accessing code under the [Lucky Penny Software GitHub Organization](https://github.com/LuckyPennySoftware) (Lucky Penny Software) here, you are agreeing to the following licensing terms. +If you do not agree to these terms, do not access Lucky Penny Software code. + +Your license to Lucky Penny Software source code and/or binaries is governed by the Reciprocal Public License 1.5 (RPL1.5) license as described here: + +https://opensource.org/license/rpl-1-5/ + +If you do not wish to release the source of software you build using Lucky Penny Software source code and/or binaries under the terms above, you may use Lucky Penny Software source code and/or binaries under the License Agreement described here: + +https://luckypennysoftware.com/license \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt deleted file mode 100644 index ed254c2438..0000000000 --- a/LICENSE.txt +++ /dev/null @@ -1,9 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2010 Jimmy Bogard - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Push.ps1 b/Push.ps1 index 9663563b23..989042d40e 100644 --- a/Push.ps1 +++ b/Push.ps1 @@ -6,7 +6,7 @@ if ([string]::IsNullOrEmpty($Env:NUGET_API_KEY)) { } else { Get-ChildItem $artifacts -Filter "*.nupkg" | ForEach-Object { Write-Host "$($scriptName): Pushing $($_.Name)" - dotnet nuget push $_ --source $Env:NUGET_URL --api-key $Env:NUGET_API_KEY + dotnet nuget push $_ --source $Env:NUGET_URL --api-key $Env:NUGET_API_KEY --skip-duplicate if ($lastexitcode -ne 0) { throw ("Exec: " + $errorMessage) } diff --git a/README.md b/README.md index 9b288ebad0..dee46cc8d4 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@ -![AutoMapper](https://camo.githubusercontent.com/603a9fdf1c6578e4df423ecdb784cb5d634e016850c10ba0798970fd48c55d41/68747470733a2f2f73332e616d617a6f6e6177732e636f6d2f6175746f6d61707065722f6c6f676f2e706e67) +![AutoMapper](https://s3.amazonaws.com/automapper/logo.png) -[![CI](https://github.com/automapper/automapper/workflows/CI/badge.svg)](https://github.com/AutoMapper/AutoMapper/actions?query=workflow%3ACI) +[![CI](https://github.com/luckypennysoftware/automapper/workflows/CI/badge.svg)](https://github.com/luckypennysoftware/AutoMapper/actions?query=workflow%3ACI) [![NuGet](http://img.shields.io/nuget/vpre/AutoMapper.svg?label=NuGet)](https://www.nuget.org/packages/AutoMapper/) -[![MyGet (dev)](https://img.shields.io/myget/automapperdev/vpre/AutoMapper.svg?label=MyGet)](https://myget.org/feed/automapperdev/package/nuget/AutoMapper) +[![Documentation Status](https://readthedocs.org/projects/automapper/badge/?version=stable)](https://docs.automapper.io/en/stable/?badge=stable) + ### What is AutoMapper? @@ -10,7 +11,6 @@ AutoMapper is a simple little library built to solve a deceptively complex probl This is the main repository for AutoMapper, but there's more: -* [Microsoft DI Extensions](https://github.com/AutoMapper/AutoMapper.Extensions.Microsoft.DependencyInjection) * [Collection Extensions](https://github.com/AutoMapper/AutoMapper.Collection) * [Expression Mapping](https://github.com/AutoMapper/AutoMapper.Extensions.ExpressionMapping) * [EF6 Extensions](https://github.com/AutoMapper/AutoMapper.EF6) @@ -23,15 +23,23 @@ First, configure AutoMapper to know what types you want to map, in the startup o ```csharp var configuration = new MapperConfiguration(cfg => +{ + cfg.CreateMap(); + cfg.CreateMap(); +}, loggerFactory); + +// or more typically, using IServiceCollection +services.AddAutoMapper(cfg => { cfg.CreateMap(); cfg.CreateMap(); }); + // only during development, validate your mappings; remove it before release #if DEBUG configuration.AssertConfigurationIsValid(); #endif -// use DI (http://docs.automapper.org/en/latest/Dependency-injection.html) or create the mapper yourself +// use DI (http://docs.automapper.io/en/latest/Dependency-injection.html) or create the mapper yourself var mapper = configuration.CreateMapper(); ``` Then in your application code, execute the mappings: @@ -41,7 +49,7 @@ var fooDto = mapper.Map(foo); var barDto = mapper.Map(bar); ``` -Check out the [getting started guide](https://automapper.readthedocs.io/en/latest/Getting-started.html). When you're done there, the [wiki](https://automapper.readthedocs.io/en/latest/) goes in to the nitty-gritty details. If you have questions, you can post them to [Stack Overflow](https://stackoverflow.com/questions/tagged/automapper) or in our [Gitter](https://gitter.im/AutoMapper/AutoMapper). +Check out the [getting started guide](https://automapper.readthedocs.io/en/latest/Getting-started.html). When you're done there, the [wiki](https://automapper.readthedocs.io/en/latest/) goes in to the nitty-gritty details. If you have questions, you can post them to [Stack Overflow](https://stackoverflow.com/questions/tagged/automapper). ### Where can I get it? @@ -57,19 +65,21 @@ dotnet add package AutoMapper ### Do you have an issue? -First check if it's already fixed by trying the [MyGet build](https://automapper.readthedocs.io/en/latest/The-MyGet-build.html). - You might want to know exactly what [your mapping does](https://automapper.readthedocs.io/en/latest/Understanding-your-mapping.html) at runtime. If you're still running into problems, file an issue above. -### License, etc. +If you are a paying customer, you can contact support via your account. -This project has adopted the code of conduct defined by the Contributor Covenant to clarify expected behavior in our community. -For more information see the [.NET Foundation Code of Conduct](https://dotnetfoundation.org/code-of-conduct). +### How do I set the license key? -AutoMapper is Copyright © 2009 [Jimmy Bogard](https://jimmybogard.com) and other contributors under the [MIT license](LICENSE.txt). +You can set the license key when registering AutoMapper: -### .NET Foundation +```csharp +services.AddAutoMapper(cfg => +{ + cfg.LicenseKey = ""; +}) +``` -This project is supported by the [.NET Foundation](https://dotnetfoundation.org). +You can register for your license key at [AutoMapper.io](https://automapper.io) \ No newline at end of file diff --git a/docs/API-Changes.md b/docs/API-Changes.md deleted file mode 100644 index d3875bccf3..0000000000 --- a/docs/API-Changes.md +++ /dev/null @@ -1,5 +0,0 @@ -# API Changes - -Starting with version 9.0, you can find out [what changed](https://raw.githubusercontent.com/AutoMapper/AutoMapper/master/src/AutoMapper/ApiCompatBaseline.txt) in the public API from the last major version release. -From the [releases page](https://github.com/AutoMapper/AutoMapper/releases) you can reach the source code for that release and the version of ApiCompatBaseline.txt in that tree will tell you what changed. -A major version release is compared with the previous major version release (so 9.0.0 with 8.0.0) and a minor version release with the current major version release (so 9.1.1 with 9.0.0). \ No newline at end of file diff --git a/docs/Makefile b/docs/Makefile index 2f136bfb63..269cadcf83 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,12 +1,12 @@ # Minimal makefile for Sphinx documentation # -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = python -msphinx -SPHINXPROJ = AutoMapper -SOURCEDIR = . -BUILDDIR = _build +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build # Put it first so that "make" without argument is like "make help". help: diff --git a/docs/The-MyGet-build.md b/docs/The-MyGet-build.md deleted file mode 100644 index b84e784230..0000000000 --- a/docs/The-MyGet-build.md +++ /dev/null @@ -1,13 +0,0 @@ -# The MyGet Build - -AutoMapper uses MyGet to publish development builds based on the master branch. This means that the MyGet build sometimes contains fixes that are not available in the current NuGet package. Please try the latest MyGet build before reporting issues, in case your issue has already been fixed but not released. - -The AutoMapper MyGet gallery is available [here](https://myget.org/feed/automapperdev/package/nuget/AutoMapper). Be sure to include prereleases. - -## Installing the Package - -If you want to install the latest MyGet package into a project, you can use the following command: - -``` -Install-Package AutoMapper -Source https://www.myget.org/F/automapperdev/api/v3/index.json -IncludePrerelease -``` diff --git a/docs/conf.py b/docs/conf.py deleted file mode 100644 index cc465859ff..0000000000 --- a/docs/conf.py +++ /dev/null @@ -1,180 +0,0 @@ -# -*- coding: utf-8 -*- -# -# AutoMapper documentation build configuration file, created by -# sphinx-quickstart on Thu Oct 05 09:44:33 2017. -# -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# -# import os -# import sys -# sys.path.insert(0, os.path.abspath('.')) - -from recommonmark.parser import CommonMarkParser - -# -- General configuration ------------------------------------------------ - -# If your documentation needs a minimal Sphinx version, state it here. -# -# needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = [] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -source_parsers = { - '.md': CommonMarkParser -} - -# The suffix(es) of source filenames. -# You can specify multiple suffix as a list of string: -# -# source_suffix = ['.rst', '.md'] -source_suffix = ['.rst', '.md'] - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = u'AutoMapper' -copyright = u'2017, Jimmy Bogard' -author = u'Jimmy Bogard' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = u'' -# The full version, including alpha/beta/rc tags. -release = u'' - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# -# This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. -language = None - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This patterns also effect to html_static_path and html_extra_path -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# If true, `todo` and `todoList` produce output, else they produce nothing. -todo_include_todos = False - - -# -- Options for HTML output ---------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# -html_theme = 'sphinx_rtd_theme' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -# -# html_theme_options = {} - -html_theme_options = { - 'logo_only': True, - 'display_version': False -} - -html_logo = 'img/logo.png' - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - -# Custom sidebar templates, must be a dictionary that maps document names -# to template names. -# -# This is required for the alabaster theme -# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars -html_sidebars = { - '**': [ - 'about.html', - 'navigation.html', - 'relations.html', # needs 'show_related': True theme option to display - 'searchbox.html', - 'donate.html', - ] -} - - -# -- Options for HTMLHelp output ------------------------------------------ - -# Output file base name for HTML help builder. -htmlhelp_basename = 'AutoMapperdoc' - - -# -- Options for LaTeX output --------------------------------------------- - -latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - # - # 'papersize': 'letterpaper', - - # The font size ('10pt', '11pt' or '12pt'). - # - # 'pointsize': '10pt', - - # Additional stuff for the LaTeX preamble. - # - # 'preamble': '', - - # Latex figure (float) alignment - # - # 'figure_align': 'htbp', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - (master_doc, 'AutoMapper.tex', u'AutoMapper Documentation', - u'Jimmy Bogard', 'manual'), -] - - -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'automapper', u'AutoMapper Documentation', - [author], 1) -] - - -# -- Options for Texinfo output ------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - (master_doc, 'AutoMapper', u'AutoMapper Documentation', - author, 'AutoMapper', 'One line description of project.', - 'Miscellaneous'), -] diff --git a/docs/make.bat b/docs/make.bat index 1568929e2d..53941892e6 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -5,32 +5,31 @@ pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=python -msphinx + set SPHINXBUILD=sphinx-build ) -set SOURCEDIR=. -set BUILDDIR=_build -set SPHINXPROJ=AutoMapper +set SOURCEDIR=source +set BUILDDIR=build if "%1" == "" goto help %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. - echo.The Sphinx module was not found. Make sure you have Sphinx installed, - echo.then set the SPHINXBUILD environment variable to point to the full - echo.path of the 'sphinx-build' executable. Alternatively you may add the - echo.Sphinx directory to PATH. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% goto end :help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% :end -popd +popd \ No newline at end of file diff --git a/docs/requirements.txt b/docs/requirements.txt index 4da41f9b71..30d45e6795 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1 +1,3 @@ -sphinx >= 1.6.0 +sphinx==7.1.2 +sphinx-rtd-theme==1.3.0rc1 +myst_parser==2.0.0 \ No newline at end of file diff --git a/docs/10.0-Upgrade-Guide.md b/docs/source/10.0-Upgrade-Guide.md similarity index 89% rename from docs/10.0-Upgrade-Guide.md rename to docs/source/10.0-Upgrade-Guide.md index 15de37111e..16c51bb409 100644 --- a/docs/10.0-Upgrade-Guide.md +++ b/docs/source/10.0-Upgrade-Guide.md @@ -1,10 +1,10 @@ # 10.0 Upgrade Guide -[Release notes](https://github.com/AutoMapper/AutoMapper/releases/tag/v10.0.0). +[Release notes](https://github.com/luckypennysoftware/AutoMapper/releases/tag/v10.0.0). ## All collections are mapped by default, even if they have no setter -You'll have to explicitly ignore those you don't want mapped. See also [this](https://github.com/AutoMapper/AutoMapper/issues/3449#issuecomment-651522397). +You'll have to explicitly ignore those you don't want mapped. See also [this](https://github.com/luckypennysoftware/AutoMapper/issues/3449#issuecomment-651522397). ## Matching constructor parameters will be mapped from the source, even if they are optional diff --git a/docs/11.0-Upgrade-Guide.md b/docs/source/11.0-Upgrade-Guide.md similarity index 84% rename from docs/11.0-Upgrade-Guide.md rename to docs/source/11.0-Upgrade-Guide.md index 9b32aab080..c2c2c38ea5 100644 --- a/docs/11.0-Upgrade-Guide.md +++ b/docs/source/11.0-Upgrade-Guide.md @@ -1,12 +1,12 @@ # 11.0 Upgrade Guide -[Release notes](https://github.com/AutoMapper/AutoMapper/releases/tag/v11.0.0). +[Release notes](https://github.com/luckypennysoftware/AutoMapper/releases/tag/v11.0.0). ## AutoMapper now targets .Net Standard 2.1 and doesn't work on .Net Framework ## `ForAllMaps`, `ForAllPropertyMaps`, `Advanced` and other "missing" APIs -Some APIs were hidden for normal usage. To light them up, you need to add an `using` for `AutoMapper.Internal` and call the [`Internal` extension method](https://github.com/AutoMapper/AutoMapper/blob/9f2f16067ab201a5a8b9bc982f3a37e8790da7a0/src/AutoMapper/Internal/InternalApi.cs#L15) on the configuration object. +Some APIs were hidden for normal usage. To light them up, you need to add an `using` for `AutoMapper.Internal` and call the [`Internal` extension method](https://github.com/LuckyPennySoftware/AutoMapper/blob/9f2f16067ab201a5a8b9bc982f3a37e8790da7a0/src/AutoMapper/Internal/InternalApi.cs#L15) on the configuration object. Most users don't need these advanced methods. Some expose internals and are not subject to the usual semantic versioning rules. To avoid such tight coupling to AutoMapper, you should try to stick to the public API. ## Mapping _into_ existing collections @@ -20,7 +20,7 @@ If you don't use `Map`, just `ProjectTo`, you should use `CreateProjection` inst ## `System.ComponentModel.TypeConverter` is no longer supported -It was removed for performance reasons. So it's best not to use it anymore. But if you must, there is [a sample](https://github.com/AutoMapper/AutoMapper/search?q=TypeConverterMapper) in the test project. +It was removed for performance reasons. So it's best not to use it anymore. But if you must, there is [a sample](https://github.com/LuckyPennySoftware/AutoMapper/search?q=TypeConverterMapper) in the test project. ## Generating interface proxies is disabled by default @@ -28,7 +28,7 @@ That was misleading for a lot of people. You can opt-in per map with `AsProxy` ( ## `MapToAttribute` and `IgnoreMapAttribute` were removed -These were older attributes, unrelated to the newer attributes API. You can switch to the fluent API or implement the attributes in your own code. Check the tests for sample code ([here](https://github.com/AutoMapper/AutoMapper/search?q=MapToAttribute) and [here](https://github.com/AutoMapper/AutoMapper/search?q=IgnoreMapAttribute)). +These were older attributes, unrelated to the newer attributes API. You can switch to the fluent API or implement the attributes in your own code. Check the tests for sample code ([here](https://github.com/LuckyPennySoftware/AutoMapper/search?q=MapToAttribute) and [here](https://github.com/LuckyPennySoftware/AutoMapper/search?q=IgnoreMapAttribute)). ## Global pre and postfixes are now applied in all maps diff --git a/docs/12.0-Upgrade-Guide.md b/docs/source/12.0-Upgrade-Guide.md similarity index 93% rename from docs/12.0-Upgrade-Guide.md rename to docs/source/12.0-Upgrade-Guide.md index bbaa700dac..0b2f2c2bda 100644 --- a/docs/12.0-Upgrade-Guide.md +++ b/docs/source/12.0-Upgrade-Guide.md @@ -1,6 +1,6 @@ # 12.0 Upgrade Guide -[Release notes](https://github.com/AutoMapper/AutoMapper/releases/tag/v12.0.0). +[Release notes](https://github.com/LuckyPennySoftware/AutoMapper/releases/tag/v12.0.0). ## Equivalent settings overwrite each other diff --git a/docs/source/13.0-Upgrade-Guide.md b/docs/source/13.0-Upgrade-Guide.md new file mode 100644 index 0000000000..fb70426950 --- /dev/null +++ b/docs/source/13.0-Upgrade-Guide.md @@ -0,0 +1,23 @@ +# 13.0 Upgrade Guide + +[Release notes](https://github.com/LuckyPennySoftware/AutoMapper/releases/tag/v13.0.0). + +## AutoMapper now targets .Net 6 + +## `AddAutoMapper` is part of the core package and the DI package is discontinued + +## `AllowAdditiveTypeMapCreation` was removed + +Be sure to call `CreateMap` once for a source type, destination type pair. If you want to reuse configuration, use mapping inheritance. + +## ProjectTo runtime polymorphic mapping with Include/IncludeBase + +We consider this an off the beaten path feature and we don't expose it through `CreateProjection`. You can use [an extension method](https://github.com/LuckyPennySoftware/AutoMapper/search?l=C%23&q=Advanced) or `CreateMap`. + +## `Context.State` similar to `Context.Items` + +The same pattern the framework uses to pass state to delegates. Note that `State` and `Items` are mutually exclusive per `Map` call. + +## Custom Equals/GetHashCode for source objects + +To avoid broken implementations, we no longer call those when checking for identical source objects, we hard code to checking object references. diff --git a/docs/source/15.0-Upgrade-Guide.md b/docs/source/15.0-Upgrade-Guide.md new file mode 100644 index 0000000000..d2bd479823 --- /dev/null +++ b/docs/source/15.0-Upgrade-Guide.md @@ -0,0 +1,45 @@ +# 15.0 Upgrade Guide + +[Release notes](https://github.com/LuckyPennySoftware/AutoMapper/releases/tag/v15.0.0). + +## AutoMapper now targets .NET 8, 9 and .NET Standard 2.0 + +## AutoMapper now requires a license + +You can set your license via the configuration: + +```c# +services.AddAutoMapper(cfg => { + cfg.LicenseKey = ""; +}); +``` + +You can register for your license at [https://automapper.io](https://automapper.io). + +## Breaking Changes + +### `AddAutoMapper` + +With the requirement to supply a license, the `AddAutoMapper` overloads all require the `Action` parameter: + +```c# +// Previous +services.AddAutoMapper(typeof(Program)); + +// Current +services.AddAutoMapper(cfg => cfg.LicenseKey = "", typeof(Program)); +``` + +This method parameter is first for all `AddAutoMapper` overloads. + +### `MapperConfiguration` + +The constructor to `MapperConfiguration` now requires an `ILoggerFactory`: + +```c# +public MapperConfiguration( + MapperConfigurationExpression configurationExpression, + ILoggerFactory loggerFactory) +``` + +This parameter is used for diagnostics. diff --git a/docs/5.0-Upgrade-Guide.md b/docs/source/5.0-Upgrade-Guide.md similarity index 97% rename from docs/5.0-Upgrade-Guide.md rename to docs/source/5.0-Upgrade-Guide.md index 2e12b9afb5..da377b1b09 100644 --- a/docs/5.0-Upgrade-Guide.md +++ b/docs/source/5.0-Upgrade-Guide.md @@ -88,7 +88,7 @@ Starting from 6.1.0 PreserveReferences is set automatically at config time whene ## UseDestinationValue -UseDestinationValue tells AutoMapper not to create a new object for some member, but to use the existing property of the destination object. It used to be true by default. Consider whether this applies to your case. Check [recent issues](https://github.com/AutoMapper/AutoMapper/search?o=desc&q=UseDestinationValue&s=created&type=Issues&utf8=%E2%9C%93). +UseDestinationValue tells AutoMapper not to create a new object for some member, but to use the existing property of the destination object. It used to be true by default. Consider whether this applies to your case. Check [recent issues](https://github.com/luckypennysoftware/AutoMapper/search?o=desc&q=UseDestinationValue&s=created&type=Issues&utf8=%E2%9C%93). ```c# cfg.CreateMap() diff --git a/docs/8.0-Upgrade-Guide.md b/docs/source/8.0-Upgrade-Guide.md similarity index 100% rename from docs/8.0-Upgrade-Guide.md rename to docs/source/8.0-Upgrade-Guide.md diff --git a/docs/8.1.1-Upgrade-Guide.md b/docs/source/8.1.1-Upgrade-Guide.md similarity index 100% rename from docs/8.1.1-Upgrade-Guide.md rename to docs/source/8.1.1-Upgrade-Guide.md diff --git a/docs/9.0-Upgrade-Guide.md b/docs/source/9.0-Upgrade-Guide.md similarity index 100% rename from docs/9.0-Upgrade-Guide.md rename to docs/source/9.0-Upgrade-Guide.md diff --git a/docs/source/API-Changes.md b/docs/source/API-Changes.md new file mode 100644 index 0000000000..1a9637f4bb --- /dev/null +++ b/docs/source/API-Changes.md @@ -0,0 +1,5 @@ +# API Changes + +Starting with version 9.0, you can find out [what changed](https://raw.githubusercontent.com/LuckyPennySoftware/AutoMapper/main/src/AutoMapper/ApiCompatBaseline.txt) in the public API from the last major version release. +From the [releases page](https://github.com/LuckyPennySoftware/AutoMapper/releases) you can reach the source code for that release and the version of ApiCompatBaseline.txt in that tree will tell you what changed. +A major version release is compared with the previous major version release (so 9.0.0 with 8.0.0) and a minor version release with the current major version release (so 9.1.1 with 9.0.0). \ No newline at end of file diff --git a/docs/Attribute-mapping.md b/docs/source/Attribute-mapping.md similarity index 99% rename from docs/Attribute-mapping.md rename to docs/source/Attribute-mapping.md index d667ef110c..9332c37e02 100644 --- a/docs/Attribute-mapping.md +++ b/docs/source/Attribute-mapping.md @@ -7,7 +7,7 @@ In addition to fluent configuration is the ability to declare and configure maps In order to search for maps to configure, use the `AddMaps` method: ```c# -var configuration = new MapperConfiguration(cfg => cfg.AddMaps("MyAssembly")); +var configuration = new MapperConfiguration(cfg => cfg.AddMaps("MyAssembly"), loggerFactory); var mapper = new Mapper(configuration); ``` diff --git a/docs/Before-and-after-map-actions.md b/docs/source/Before-and-after-map-actions.md similarity index 83% rename from docs/Before-and-after-map-actions.md rename to docs/source/Before-and-after-map-actions.md index 91822a656e..a8bc6bdd4a 100644 --- a/docs/Before-and-after-map-actions.md +++ b/docs/source/Before-and-after-map-actions.md @@ -7,7 +7,7 @@ var configuration = new MapperConfiguration(cfg => { cfg.CreateMap() .BeforeMap((src, dest) => src.Value = src.Value + 10) .AfterMap((src, dest) => dest.Name = "John"); -}); +}, loggerFactory); ``` Or you can create before/after map callbacks during mapping: @@ -42,10 +42,10 @@ var configuration = new MapperConfiguration(cfg => { }); ``` -### Asp.Net Core and `AutoMapper.Extensions.Microsoft.DependencyInjection` -If you are using Asp.Net Core and the `AutoMapper.Extensions.Microsoft.DependencyInjection` package, this is also a good way of using Dependency Injection. You can't inject dependencies into `Profile` classes, but you can do it in `IMappingAction` implementations. +### Dependency Injection +You can't inject dependencies into `Profile` classes, but you can do it in `IMappingAction` implementations. -The following example shows how to connect an `IMappingAction` accessing the current `HttpContext` to a `Profile` after map action, leveraging Dependency Injection: +The following example shows how to connect an `IMappingAction` accessing the current `HttpContext` to a `Profile` after map action, leveraging dependency injection: ``` csharp public class SetTraceIdentifierAction : IMappingAction @@ -80,10 +80,8 @@ public class Startup { public void ConfigureServices(IServiceCollection services) { - services.AddAutoMapper(typeof(Startup).Assembly); + services.AddAutoMapper(_ => { }, typeof(Startup).Assembly); } //.. } -``` - -*See `AutoMapper.Extensions.Microsoft.DependencyInjection` for more info.* +``` \ No newline at end of file diff --git a/docs/Conditional-mapping.md b/docs/source/Conditional-mapping.md similarity index 97% rename from docs/Conditional-mapping.md rename to docs/source/Conditional-mapping.md index 02a6f6c8a4..20d2b3eb7a 100644 --- a/docs/Conditional-mapping.md +++ b/docs/source/Conditional-mapping.md @@ -19,7 +19,7 @@ In the following mapping the property baz will only be mapped if it is greater t var configuration = new MapperConfiguration(cfg => { cfg.CreateMap() .ForMember(dest => dest.baz, opt => opt.Condition(src => (src.baz >= 0))); -}); +}, loggerFactory); ``` If you have a resolver, see [here](Custom-value-resolvers.html#resolvers-and-conditions) for a concrete example. @@ -36,7 +36,7 @@ var configuration = new MapperConfiguration(cfg => { // Expensive resolution process that can be avoided with a PreCondition }); }); -}); +}, loggerFactory); ``` You can [see the steps](Understanding-your-mapping.html) yourself. diff --git a/docs/Configuration-validation.md b/docs/source/Configuration-validation.md similarity index 91% rename from docs/Configuration-validation.md rename to docs/source/Configuration-validation.md index 2f0efc7386..cbba3f153c 100644 --- a/docs/Configuration-validation.md +++ b/docs/source/Configuration-validation.md @@ -17,7 +17,7 @@ public class Destination In the Destination type, we probably fat-fingered the destination property. Other typical issues are source member renames. To test our configuration, we simply create a unit test that sets up the configuration and executes the AssertConfigurationIsValid method: ```c# var configuration = new MapperConfiguration(cfg => - cfg.CreateMap()); + cfg.CreateMap(), loggerFactory); configuration.AssertConfigurationIsValid(); ``` @@ -36,7 +36,7 @@ With the third option, we have a member on the destination type that we will fil var configuration = new MapperConfiguration(cfg => cfg.CreateMap() .ForMember(dest => dest.SomeValuefff, opt => opt.Ignore()) -); +, loggerFactory); ``` ## Selecting members to validate @@ -47,12 +47,12 @@ By default, AutoMapper uses the destination type to validate members. It assumes var configuration = new MapperConfiguration(cfg => cfg.CreateMap(MemberList.Source); cfg.CreateMap(MemberList.None); -); +, loggerFactory); ``` -To skip validation altogether for this map, use `MemberList.None`. +To skip validation altogether for this map, use `MemberList.None`. That's the default for `ReverseMap`. ## Custom validations -You can add custom validations through an extension point. See [here](https://github.com/AutoMapper/AutoMapper/blob/bdc0120497d192a2741183415543f6119f50a982/src/UnitTests/CustomValidations.cs#L42). +You can add custom validations through an extension point. See [here](https://github.com/LuckyPennySoftware/AutoMapper/blob/main/src/UnitTests/CustomValidations.cs). diff --git a/docs/Configuration.md b/docs/source/Configuration.md similarity index 94% rename from docs/Configuration.md rename to docs/source/Configuration.md index 318890ec64..d501a57bd7 100644 --- a/docs/Configuration.md +++ b/docs/source/Configuration.md @@ -6,7 +6,7 @@ Create a `MapperConfiguration` instance and initialize configuration via the con var config = new MapperConfiguration(cfg => { cfg.CreateMap(); cfg.AddProfile(); -}); +}, loggerFactory); ``` The `MapperConfiguration` instance can be stored statically, in a static field or in a dependency injection container. Once created it cannot be changed/modified. @@ -15,7 +15,7 @@ The `MapperConfiguration` instance can be stored statically, in a static field o var configuration = new MapperConfiguration(cfg => { cfg.CreateMap(); cfg.AddProfile(); -}); +}, loggerFactory); ``` Starting with 9.0, the static API is no longer available. @@ -65,24 +65,24 @@ or by automatically scanning for profiles: // ... using instance approach: var config = new MapperConfiguration(cfg => { cfg.AddMaps(myAssembly); -}); -var configuration = new MapperConfiguration(cfg => cfg.AddMaps(myAssembly)); +}, loggerFactory); +var configuration = new MapperConfiguration(cfg => cfg.AddMaps(myAssembly), loggerFactory); // Can also use assembly names: -var configuration = new MapperConfiguration(cfg => +var configuration = new MapperConfiguration(cfg => { cfg.AddMaps(new [] { "Foo.UI", "Foo.Core" }); -); +}, loggerFactory); // Or marker types for assemblies: -var configuration = new MapperConfiguration(cfg => +var configuration = new MapperConfiguration(cfg => { cfg.AddMaps(new [] { typeof(HomeController), typeof(Entity) }); -); +}, loggerFactory); ``` AutoMapper will scan the designated assemblies for classes inheriting from Profile and add them to the configuration. @@ -95,7 +95,7 @@ You can set the source and destination naming conventions var configuration = new MapperConfiguration(cfg => { cfg.SourceMemberNamingConvention = LowerUnderscoreNamingConvention.Instance; cfg.DestinationMemberNamingConvention = PascalCaseNamingConvention.Instance; -}); +}, loggerFactory); ``` This will map the following properties to each other: @@ -143,7 +143,7 @@ var configuration = new MapperConfiguration(c => c.ReplaceMemberName("Ä", "A"); c.ReplaceMemberName("í", "i"); c.ReplaceMemberName("Airlina", "Airline"); -}); +}, loggerFactory); ``` ## Recognizing pre/postfixes @@ -162,7 +162,7 @@ public class Dest { var configuration = new MapperConfiguration(cfg => { cfg.RecognizePrefixes("frm"); cfg.CreateMap(); -}); +}, loggerFactory); configuration.AssertConfigurationIsValid(); ``` @@ -172,7 +172,7 @@ By default AutoMapper recognizes the prefix "Get", if you need to clear the pref var configuration = new MapperConfiguration(cfg => { cfg.ClearPrefixes(); cfg.RecognizePrefixes("tmp"); -}); +}, loggerFactory); ``` ## Global property/field filtering @@ -188,7 +188,7 @@ var configuration = new MapperConfiguration(cfg => // map properties with a public or private getter cfg.ShouldMapProperty = pi => pi.GetMethod != null && (pi.GetMethod.IsPublic || pi.GetMethod.IsPrivate); -}); +}, loggerFactory); ``` ## Configuring visibility @@ -201,7 +201,7 @@ var configuration = new MapperConfiguration(cfg => // map properties with public or internal getters cfg.ShouldMapProperty = p => p.GetMethod.IsPublic || p.GetMethod.IsAssembly; cfg.CreateMap(); -}); +}, loggerFactory); ``` Map configurations will now recognize internal/private members. @@ -211,7 +211,7 @@ Map configurations will now recognize internal/private members. Because expression compilation can be a bit resource intensive, AutoMapper lazily compiles the type map plans on first map. However, this behavior is not always desirable, so you can tell AutoMapper to compile its mappings directly: ```c# -var configuration = new MapperConfiguration(cfg => {}); +var configuration = new MapperConfiguration(cfg => {}, loggerFactory); configuration.CompileMappings(); ``` diff --git a/docs/Construction.md b/docs/source/Construction.md similarity index 89% rename from docs/Construction.md rename to docs/source/Construction.md index 36af6d41ac..945181e64e 100644 --- a/docs/Construction.md +++ b/docs/source/Construction.md @@ -15,7 +15,7 @@ public class SourceDto { get { return _value; } } } -var configuration = new MapperConfiguration(cfg => cfg.CreateMap()); +var configuration = new MapperConfiguration(cfg => cfg.CreateMap(), loggerFactory); ``` If the destination constructor parameter names don't match, you can modify them at config time: @@ -36,7 +36,7 @@ public class SourceDto { var configuration = new MapperConfiguration(cfg => cfg.CreateMap() .ForCtorParam("valueParamSomeOtherName", opt => opt.MapFrom(src => src.Value)) -); +, loggerFactory); ``` This works for both LINQ projections and in-memory mapping. @@ -44,13 +44,13 @@ This works for both LINQ projections and in-memory mapping. You can also disable constructor mapping: ```c# -var configuration = new MapperConfiguration(cfg => cfg.DisableConstructorMapping()); +var configuration = new MapperConfiguration(cfg => cfg.DisableConstructorMapping(), loggerFactory); ``` You can configure which constructors are considered for the destination object: ```c# // use only public constructors -var configuration = new MapperConfiguration(cfg => cfg.ShouldUseConstructor = constructor => constructor.IsPublic); +var configuration = new MapperConfiguration(cfg => cfg.ShouldUseConstructor = constructor => constructor.IsPublic, loggerFactory); ``` When mapping to records, consider using only public constructors. \ No newline at end of file diff --git a/docs/Custom-type-converters.md b/docs/source/Custom-type-converters.md similarity index 99% rename from docs/Custom-type-converters.md rename to docs/source/Custom-type-converters.md index ff3a6b21aa..8b77facd2c 100644 --- a/docs/Custom-type-converters.md +++ b/docs/source/Custom-type-converters.md @@ -52,7 +52,7 @@ public void Example() cfg.CreateMap().ConvertUsing(new DateTimeTypeConverter()); cfg.CreateMap().ConvertUsing(); cfg.CreateMap(); - }); + }, loggerFactory); configuration.AssertConfigurationIsValid(); var source = new Source diff --git a/docs/Custom-value-resolvers.md b/docs/source/Custom-value-resolvers.md similarity index 96% rename from docs/Custom-value-resolvers.md rename to docs/source/Custom-value-resolvers.md index 58a603d42d..fde7612b40 100644 --- a/docs/Custom-value-resolvers.md +++ b/docs/source/Custom-value-resolvers.md @@ -47,7 +47,7 @@ In the below example, we'll use the first option, telling AutoMapper the custom ```c# var configuration = new MapperConfiguration(cfg => cfg.CreateMap() - .ForMember(dest => dest.Total, opt => opt.MapFrom())); + .ForMember(dest => dest.Total, opt => opt.MapFrom()), loggerFactory); configuration.AssertConfigurationIsValid(); var source = new Source @@ -83,7 +83,7 @@ If we don't want AutoMapper to use reflection to create the instance, we can sup var configuration = new MapperConfiguration(cfg => cfg.CreateMap() .ForMember(dest => dest.Total, opt => opt.MapFrom(new CustomResolver()) - )); + ), loggerFactory); ``` AutoMapper will use that specific object, helpful in scenarios where the resolver might have constructor arguments or need to be constructed by an IoC container. @@ -104,7 +104,7 @@ cfg.CreateMap() cfg.CreateMap() .ForMember(dest => dest.OtherTotal, opt => opt.MapFrom(src => src.OtherSubTotal)); -}); +}, loggerFactory); public class CustomResolver : IMemberValueResolver { public decimal Resolve(object source, object destination, decimal sourceMember, decimal destinationMember, ResolutionContext context) { @@ -127,10 +127,11 @@ This is how to setup the mapping for this custom resolver cfg.CreateMap() .ForMember(dest => dest.Foo, opt => opt.MapFrom((src, dest, destMember, context) => context.Items["Foo"])); ``` +Starting with version 13.0, you can use `context.State` instead, in a similar way. Note that `State` and `Items` are mutually exclusive per `Map` call. ### ForPath -Similar to ForMember, from 6.1.0 there is ForPath. Check out [the tests](https://github.com/AutoMapper/AutoMapper/search?utf8=%E2%9C%93&q=ForPath&type=) for examples. +Similar to ForMember, from 6.1.0 there is ForPath. Check out [the tests](https://github.com/LuckyPennySoftware/AutoMapper/search?utf8=%E2%9C%93&q=ForPath&type=) for examples. ### Resolvers and conditions diff --git a/docs/Dependency-injection.md b/docs/source/Dependency-injection.md similarity index 85% rename from docs/Dependency-injection.md rename to docs/source/Dependency-injection.md index 7b52e4ee44..806d8fa8b7 100644 --- a/docs/Dependency-injection.md +++ b/docs/source/Dependency-injection.md @@ -6,13 +6,15 @@ There is a [NuGet package](https://www.nuget.org/packages/AutoMapper.Extensions.Microsoft.DependencyInjection/) to be used with the default injection mechanism described [here](https://github.com/AutoMapper/AutoMapper.Extensions.Microsoft.DependencyInjection) and used in [this project](https://github.com/jbogard/ContosoUniversityCore/blob/master/src/ContosoUniversityCore/Startup.cs). +Starting with version 13.0, `AddAutoMapper` is part of the core package and the DI package is discontinued. + You define the configuration using [profiles](Configuration.html#profile-instances). And then you let AutoMapper know in what assemblies are those profiles defined by calling the `IServiceCollection` extension method `AddAutoMapper` at startup: ```c# -services.AddAutoMapper(profileAssembly1, profileAssembly2 /*, ...*/); +services.AddAutoMapper(cfg => { }, profileAssembly1, profileAssembly2 /*, ...*/); ``` or marker types: ```c# -services.AddAutoMapper(typeof(ProfileTypeFromAssembly1), typeof(ProfileTypeFromAssembly2) /*, ...*/); +services.AddAutoMapper(cfg => { }, typeof(ProfileTypeFromAssembly1), typeof(ProfileTypeFromAssembly2) /*, ...*/); ``` Now you can inject AutoMapper at runtime into your services/controllers: ```c# @@ -30,7 +32,7 @@ There is a third-party [NuGet package](https://www.nuget.org/packages/AutoMapper Also, check [this blog](https://dotnetfalcon.com/autofac-support-for-automapper/). -### [Other DI engines](https://github.com/AutoMapper/AutoMapper/wiki/DI-examples) +### [Other DI engines](https://github.com/LuckyPennySoftware/AutoMapper/wiki/DI-examples) ## Low level API-s @@ -42,7 +44,7 @@ var configuration = new MapperConfiguration(cfg => cfg.ConstructServicesUsing(ObjectFactory.GetInstance); cfg.CreateMap(); -}); +}, loggerFactory); ``` Or dynamic service location, to be used in the case of instance-based containers (including child/nested containers): diff --git a/docs/Dynamic-and-ExpandoObject-Mapping.md b/docs/source/Dynamic-and-ExpandoObject-Mapping.md similarity index 92% rename from docs/Dynamic-and-ExpandoObject-Mapping.md rename to docs/source/Dynamic-and-ExpandoObject-Mapping.md index e457932d71..b795ba4311 100644 --- a/docs/Dynamic-and-ExpandoObject-Mapping.md +++ b/docs/source/Dynamic-and-ExpandoObject-Mapping.md @@ -12,7 +12,7 @@ dynamic foo = new MyDynamicObject(); foo.Bar = 5; foo.Baz = 6; -var configuration = new MapperConfiguration(cfg => {}); +var configuration = new MapperConfiguration(cfg => {}, loggerFactory); var result = mapper.Map(foo); result.Bar.ShouldEqual(5); diff --git a/docs/Enum-Mapping.md b/docs/source/Enum-Mapping.md similarity index 99% rename from docs/Enum-Mapping.md rename to docs/source/Enum-Mapping.md index e1f911266f..2508572749 100644 --- a/docs/Enum-Mapping.md +++ b/docs/source/Enum-Mapping.md @@ -95,7 +95,7 @@ public class MappingConfigurationsTests configuration.EnableEnumMappingValidation(); configuration.AddMaps(typeof(AssemblyInfo).GetTypeInfo().Assembly); - }); + }, loggerFactory); // Assert config.AssertConfigurationIsValid(); diff --git a/docs/Expression-Translation-(UseAsDataSource).md b/docs/source/Expression-Translation-(UseAsDataSource).md similarity index 98% rename from docs/Expression-Translation-(UseAsDataSource).md rename to docs/source/Expression-Translation-(UseAsDataSource).md index 37b70efc28..d07b160aba 100644 --- a/docs/Expression-Translation-(UseAsDataSource).md +++ b/docs/source/Expression-Translation-(UseAsDataSource).md @@ -38,7 +38,7 @@ var configuration = new MapperConfiguration(cfg => .ForMember(ol => ol.Item, conf => conf.MapFrom(dto => dto)); cfg.CreateMap() .ForMember(i => i.Name, conf => conf.MapFrom(dto => dto.Item)); -}); +}, loggerFactory); ``` When mapping from DTO Expression @@ -137,7 +137,7 @@ With `.ProjectTo` this is quite simple, as there is no sense in directly r ```c# var configuration = new MapperConfiguration(cfg => cfg.CreateMap() - .ForMember(dto => dto.Item, conf => conf.MapFrom(ol => ol.Item.Name))); + .ForMember(dto => dto.Item, conf => conf.MapFrom(ol => ol.Item.Name)), loggerFactory); public List GetLinesForOrder(int orderId) { @@ -161,7 +161,7 @@ Using it, you can do the following: ```c# var configuration = new MapperConfiguration(cfg => cfg.CreateMap() - .ForMember(dto => dto.Item, conf => conf.MapFrom(ol => ol.Item.Name))); + .ForMember(dto => dto.Item, conf => conf.MapFrom(ol => ol.Item.Name)), loggerFactory); public IQueryable GetLinesForOrder(int orderId) { diff --git a/docs/Flattening.md b/docs/source/Flattening.md similarity index 96% rename from docs/Flattening.md rename to docs/source/Flattening.md index f1d7b3c675..a3e914af28 100644 --- a/docs/Flattening.md +++ b/docs/source/Flattening.md @@ -86,7 +86,7 @@ order.AddOrderLineItem(bosco, 15); // Configure AutoMapper -var configuration = new MapperConfiguration(cfg => cfg.CreateMap()); +var configuration = new MapperConfiguration(cfg => cfg.CreateMap(), loggerFactory); // Perform mapping @@ -157,4 +157,4 @@ ForPath(destination => destination.IncludedMember, member => member.MapFrom(sour ``` and the other way around. If that's not what you want, you can avoid `ReverseMap` (explicitly create the reverse map) or you can override the default settings (using `Ignore` or `IncludeMembers` without parameters respectively). -For details, check [the tests](https://github.com/AutoMapper/AutoMapper/blob/master/src/UnitTests/IMappingExpression/IncludeMembers.cs). +For details, check [the tests](https://github.com/LuckyPennySoftware/AutoMapper/blob/main/src/UnitTests/IMappingExpression/IncludeMembers.cs). diff --git a/docs/Getting-started.md b/docs/source/Getting-started.md similarity index 92% rename from docs/Getting-started.md rename to docs/source/Getting-started.md index e73558f5ab..46220cb03b 100644 --- a/docs/Getting-started.md +++ b/docs/source/Getting-started.md @@ -17,7 +17,7 @@ AutoMapper will ignore null reference exceptions when mapping your source to you Once you have your types you can create a map for the two types using a `MapperConfiguration` and CreateMap. You only need one `MapperConfiguration` instance typically per AppDomain and should be instantiated during startup. More examples of initial setup can be seen in [Setup](Setup.html). ```c# -var config = new MapperConfiguration(cfg => cfg.CreateMap()); +var config = new MapperConfiguration(cfg => cfg.CreateMap(), loggerFactory); ``` The type on the left is the source type, and the type on the right is the destination type. To perform a mapping, call one of the `Map` overloads: @@ -53,3 +53,15 @@ var config = AutoMapperConfiguration.Configure(); config.AssertConfigurationIsValid(); ``` + +## How do I set the license key? + +You can set your license via the configuration: + +```c# +services.AddAutoMapper(cfg => { + cfg.LicenseKey = ""; +}); +``` + +You can register for your license at [https://automapper.io](https://automapper.io). \ No newline at end of file diff --git a/docs/source/License-configuration.md b/docs/source/License-configuration.md new file mode 100644 index 0000000000..0c46846222 --- /dev/null +++ b/docs/source/License-configuration.md @@ -0,0 +1,47 @@ +AutoMapper is [dual licensed](https://github.com/LuckyPennySoftware/AutoMapper/blob/main/LICENSE.md). To configure the commercial license, either the license key can be set using `Microsoft.Extensions.DependencyInjection` integration: + +```c# +services.AddAutoMapper(cfg => { + cfg.LicenseKey = "License key here"; + + // Other configuration +}); +``` + +Or on non-MS.Ext.DI scenarios, where you're using AutoMapper directly, you can set the license key in the constructor for the `MappingConfiguration`: + +```c# +var mapperConfiguration = new MapperConfiguration(cfg => { + cfg.LicenseKey = "License Key Here"; +}, loggerFactory); +``` + +You can obtain a valid license from the [AutoMapper website](https://automapper.io). + +### License Enforcement + +Licensing is enforced via log messages at various levels: + +- INFO: Valid license message +- WARNING: Missing license message +- ERROR: Invalid/expired license message + +There is no other license enforcement besides log messages. No central license server, no outbound HTTP calls, no degrading or disabling of features. + +The log messages are logged using standard `Microsoft.Extensions.Logging` loggers under the category name `LuckyPennySoftware.AutoMapper.License`. + +### Client Redistribution Scenarios + +In the case where AutoMapper is used on a client, including: + +- Blazor WASM +- WPF/MAUI/Desktop apps +- Redistributed clients + +The license key should NOT be set as this would result in secrets transmitted to the client. Instead, omit the license key configuration and mute the license message category name using: + +```csharp +builder.Logging.AddFilter("LuckyPennySoftware.AutoMapper.License", LogLevel.None); +``` + +This will depend on your logging setup. A missing/invalid license key does not affect runtime behavior in any way. \ No newline at end of file diff --git a/docs/Lists-and-arrays.md b/docs/source/Lists-and-arrays.md similarity index 97% rename from docs/Lists-and-arrays.md rename to docs/source/Lists-and-arrays.md index 8763481e2e..ee4b1c7fda 100644 --- a/docs/Lists-and-arrays.md +++ b/docs/source/Lists-and-arrays.md @@ -17,7 +17,7 @@ public class Destination All the basic generic collection types are supported: ```c# -var configuration = new MapperConfiguration(cfg => cfg.CreateMap()); +var configuration = new MapperConfiguration(cfg => cfg.CreateMap(), loggerFactory); var sources = new[] { @@ -58,7 +58,7 @@ This behavior can be changed by setting the `AllowNullCollections` property to t var configuration = new MapperConfiguration(cfg => { cfg.AllowNullCollections = true; cfg.CreateMap(); -}); +}, loggerFactory); ``` The setting can be applied globally and can be overridden per profile and per member with `AllowNull` and `DoNotAllowNull`. @@ -95,7 +95,7 @@ var configuration = new MapperConfiguration(c=> { c.CreateMap() .Include(); c.CreateMap(); -}); +}, loggerFactory); var sources = new[] { diff --git a/docs/Mapping-inheritance.md b/docs/source/Mapping-inheritance.md similarity index 98% rename from docs/Mapping-inheritance.md rename to docs/source/Mapping-inheritance.md index 0f559a3db2..ced7b6b7e1 100644 --- a/docs/Mapping-inheritance.md +++ b/docs/source/Mapping-inheritance.md @@ -58,7 +58,7 @@ var configuration = new MapperConfiguration(cfg => { .Include(); cfg.CreateMap(); cfg.CreateMap(); -}); +}, loggerFactory); // Perform Mapping var order = new OnlineOrder(); @@ -80,7 +80,7 @@ var configuration = new MapperConfiguration(cfg => { .IncludeBase(); cfg.CreateMap() .IncludeBase(); -}); +}, loggerFactory); ``` ## As @@ -128,7 +128,7 @@ var configuration = new MapperConfiguration(cfg => { .ForMember(o=>o.Referrer, m=>m.Ignore()); cfg.CreateMap(); cfg.CreateMap(); -}); +}, loggerFactory); // Perform Mapping var order = new OnlineOrder { Referrer = "google" }; diff --git a/docs/Nested-mappings.md b/docs/source/Nested-mappings.md similarity index 96% rename from docs/Nested-mappings.md rename to docs/source/Nested-mappings.md index 237d2cba44..5b9e383d04 100644 --- a/docs/Nested-mappings.md +++ b/docs/source/Nested-mappings.md @@ -1,60 +1,60 @@ -# Nested Mappings - -As the mapping engine executes the mapping, it can use one of a variety of methods to resolve a destination member value. One of these methods is to use another type map, where the source member type and destination member type are also configured in the mapping configuration. This allows us to not only flatten our source types, but create complex destination types as well. For example, our source type might contain another complex type: - -```c# -public class OuterSource -{ - public int Value { get; set; } - public InnerSource Inner { get; set; } -} - -public class InnerSource -{ - public int OtherValue { get; set; } -} -``` - -We _could_ simply flatten the OuterSource.Inner.OtherValue to one InnerOtherValue property, but we might also want to create a corresponding complex type for the Inner property: - -```c# -public class OuterDest -{ - public int Value { get; set; } - public InnerDest Inner { get; set; } -} - -public class InnerDest -{ - public int OtherValue { get; set; } -} -``` - -In that case, we would need to configure the additional source/destination type mappings: - -```c# -var config = new MapperConfiguration(cfg => { - cfg.CreateMap(); - cfg.CreateMap(); -}); -config.AssertConfigurationIsValid(); - -var source = new OuterSource - { - Value = 5, - Inner = new InnerSource {OtherValue = 15} - }; -var mapper = config.CreateMapper(); -var dest = mapper.Map(source); - -dest.Value.ShouldEqual(5); -dest.Inner.ShouldNotBeNull(); -dest.Inner.OtherValue.ShouldEqual(15); -``` - -A few things to note here: - -* Order of configuring types does not matter -* Call to Map does not need to specify any inner type mappings, only the type map to use for the source value passed in - -With both flattening and nested mappings, we can create a variety of destination shapes to suit whatever our needs may be. +# Nested Mappings + +As the mapping engine executes the mapping, it can use one of a variety of methods to resolve a destination member value. One of these methods is to use another type map, where the source member type and destination member type are also configured in the mapping configuration. This allows us to not only flatten our source types, but create complex destination types as well. For example, our source type might contain another complex type: + +```c# +public class OuterSource +{ + public int Value { get; set; } + public InnerSource Inner { get; set; } +} + +public class InnerSource +{ + public int OtherValue { get; set; } +} +``` + +We _could_ simply flatten the OuterSource.Inner.OtherValue to one InnerOtherValue property, but we might also want to create a corresponding complex type for the Inner property: + +```c# +public class OuterDest +{ + public int Value { get; set; } + public InnerDest Inner { get; set; } +} + +public class InnerDest +{ + public int OtherValue { get; set; } +} +``` + +In that case, we would need to configure the additional source/destination type mappings: + +```c# +var config = new MapperConfiguration(cfg => { + cfg.CreateMap(); + cfg.CreateMap(); +}, loggerFactory); +config.AssertConfigurationIsValid(); + +var source = new OuterSource + { + Value = 5, + Inner = new InnerSource {OtherValue = 15} + }; +var mapper = config.CreateMapper(); +var dest = mapper.Map(source); + +dest.Value.ShouldEqual(5); +dest.Inner.ShouldNotBeNull(); +dest.Inner.OtherValue.ShouldEqual(15); +``` + +A few things to note here: + +* Order of configuring types does not matter +* Call to Map does not need to specify any inner type mappings, only the type map to use for the source value passed in + +With both flattening and nested mappings, we can create a variety of destination shapes to suit whatever our needs may be. diff --git a/docs/Null-substitution.md b/docs/source/Null-substitution.md similarity index 94% rename from docs/Null-substitution.md rename to docs/source/Null-substitution.md index 9ae3f481d7..7dafe0e768 100644 --- a/docs/Null-substitution.md +++ b/docs/source/Null-substitution.md @@ -4,7 +4,7 @@ Null substitution allows you to supply an alternate value for a destination memb ```c# var config = new MapperConfiguration(cfg => cfg.CreateMap() - .ForMember(destination => destination.Value, opt => opt.NullSubstitute("Other Value"))); + .ForMember(destination => destination.Value, opt => opt.NullSubstitute("Other Value")), loggerFactory); var source = new Source { Value = null }; var mapper = config.CreateMapper(); diff --git a/docs/Open-Generics.md b/docs/source/Open-Generics.md similarity index 86% rename from docs/Open-Generics.md rename to docs/source/Open-Generics.md index b9505f7814..606d7534b5 100644 --- a/docs/Open-Generics.md +++ b/docs/source/Open-Generics.md @@ -12,7 +12,7 @@ public class Destination { } // Create the mapping -var configuration = new MapperConfiguration(cfg => cfg.CreateMap(typeof(Source<>), typeof(Destination<>))); +var configuration = new MapperConfiguration(cfg => cfg.CreateMap(typeof(Source<>), typeof(Destination<>)), loggerFactory); ``` You don't need to create maps for closed generic types. AutoMapper will apply any configuration from the open generic mapping to the closed mapping at runtime: @@ -32,16 +32,16 @@ You can also create an open generic type converter: ```c# var configuration = new MapperConfiguration(cfg => - cfg.CreateMap(typeof(Source<>), typeof(Destination<>)).ConvertUsing(typeof(Converter<>))); + cfg.CreateMap(typeof(Source<>), typeof(Destination<>)).ConvertUsing(typeof(Converter<>)), loggerFactory); ``` AutoMapper also supports open generic type converters with any number of generic arguments: ```c# var configuration = new MapperConfiguration(cfg => - cfg.CreateMap(typeof(Source<>), typeof(Destination<>)).ConvertUsing(typeof(Converter<,>))); + cfg.CreateMap(typeof(Source<>), typeof(Destination<>)).ConvertUsing(typeof(Converter<,>)), loggerFactory); ``` The closed type from `Source` will be the first generic argument, and the closed type of `Destination` will be the second argument to close `Converter<,>`. -The same idea applies to value resolvers. Check [the tests](https://github.com/AutoMapper/AutoMapper/blob/e8249d582d384ea3b72eec31408126a0b69619bc/src/UnitTests/OpenGenerics.cs#L11). +The same idea applies to value resolvers. Check [the tests](https://github.com/LuckyPennySoftware/AutoMapper/blob/e8249d582d384ea3b72eec31408126a0b69619bc/src/UnitTests/OpenGenerics.cs#L11). diff --git a/docs/Projection.md b/docs/source/Projection.md similarity index 98% rename from docs/Projection.md rename to docs/source/Projection.md index c9b51fa936..70fb21921d 100644 --- a/docs/Projection.md +++ b/docs/source/Projection.md @@ -36,7 +36,7 @@ var configuration = new MapperConfiguration(cfg => cfg.CreateMap() .ForMember(dest => dest.EventDate, opt => opt.MapFrom(src => src.Date.Date)) .ForMember(dest => dest.EventHour, opt => opt.MapFrom(src => src.Date.Hour)) - .ForMember(dest => dest.EventMinute, opt => opt.MapFrom(src => src.Date.Minute))); + .ForMember(dest => dest.EventMinute, opt => opt.MapFrom(src => src.Date.Minute)), loggerFactory); // Perform mapping CalendarEventForm form = mapper.Map(calendarEvent); diff --git a/docs/Queryable-Extensions.md b/docs/source/Queryable-Extensions.md similarity index 91% rename from docs/Queryable-Extensions.md rename to docs/source/Queryable-Extensions.md index 500356d3c2..7eab0f3a43 100644 --- a/docs/Queryable-Extensions.md +++ b/docs/source/Queryable-Extensions.md @@ -1,232 +1,242 @@ -# Queryable Extensions - -When using an ORM such as NHibernate or Entity Framework with AutoMapper's standard `mapper.Map` functions, you may notice that the ORM will query all the fields of all the objects within a graph when AutoMapper is attempting to map the results to a destination type. - -If your ORM exposes `IQueryable`s, you can use AutoMapper's QueryableExtensions helper methods to address this key pain. - -Using Entity Framework for an example, say that you have an entity `OrderLine` with a relationship with an entity `Item`. If you want to map this to an `OrderLineDTO` with the `Item`'s `Name` property, the standard `mapper.Map` call will result in Entity Framework querying the entire `OrderLine` and `Item` table. - -Use this approach instead. - -Given the following entities: - -```c# -public class OrderLine -{ - public int Id { get; set; } - public int OrderId { get; set; } - public Item Item { get; set; } - public decimal Quantity { get; set; } -} - -public class Item -{ - public int Id { get; set; } - public string Name { get; set; } -} -``` - -And the following DTO: - -```c# -public class OrderLineDTO -{ - public int Id { get; set; } - public int OrderId { get; set; } - public string Item { get; set; } - public decimal Quantity { get; set; } -} -``` - -You can use the Queryable Extensions like so: - -```c# -var configuration = new MapperConfiguration(cfg => - cfg.CreateProjection() - .ForMember(dto => dto.Item, conf => conf.MapFrom(ol => ol.Item.Name))); - -public List GetLinesForOrder(int orderId) -{ - using (var context = new orderEntities()) - { - return context.OrderLines.Where(ol => ol.OrderId == orderId) - .ProjectTo(configuration).ToList(); - } -} -``` - -The `.ProjectTo()` will tell AutoMapper's mapping engine to emit a `select` clause to the IQueryable that will inform entity framework that it only needs to query the Name column of the Item table, same as if you manually projected your `IQueryable` to an `OrderLineDTO` with a `Select` clause. - -`ProjectTo` must be the last call in the chain. ORMs work with entities, not DTOs. So apply any filtering and sorting on entities and, as the last step, project to DTOs. - -Note that for this feature to work, all type conversions must be explicitly handled in your Mapping. For example, you can not rely on the `ToString()` override of the `Item` class to inform entity framework to only select from the `Name` column, and any data type changes, such as `Double` to `Decimal` must be explicitly handled as well. - -### The instance API - -Starting with 8.0 there are similar ProjectTo methods on IMapper that feel more natural when you use IMapper with DI. - -### Preventing lazy loading/SELECT N+1 problems - -Because the LINQ projection built by AutoMapper is translated directly to a SQL query by the query provider, the mapping occurs at the SQL/ADO.NET level, and not touching your entities. All data is eagerly fetched and loaded into your DTOs. - -Nested collections use a Select to project child DTOs: - -```c# -from i in db.Instructors -orderby i.LastName -select new InstructorIndexData.InstructorModel -{ - ID = i.ID, - FirstMidName = i.FirstMidName, - LastName = i.LastName, - HireDate = i.HireDate, - OfficeAssignmentLocation = i.OfficeAssignment.Location, - Courses = i.Courses.Select(c => new InstructorIndexData.InstructorCourseModel - { - CourseID = c.CourseID, - CourseTitle = c.Title - }).ToList() -}; -``` - +# Queryable Extensions + +When using an ORM such as NHibernate or Entity Framework with AutoMapper's standard `mapper.Map` functions, you may notice that the ORM will query all the fields of all the objects within a graph when AutoMapper is attempting to map the results to a destination type. + +If your ORM exposes `IQueryable`s, you can use AutoMapper's QueryableExtensions helper methods to address this key pain. + +Using Entity Framework for an example, say that you have an entity `OrderLine` with a relationship with an entity `Item`. If you want to map this to an `OrderLineDTO` with the `Item`'s `Name` property, the standard `mapper.Map` call will result in Entity Framework querying the entire `OrderLine` and `Item` table. + +Use this approach instead. + +Given the following entities: + +```c# +public class OrderLine +{ + public int Id { get; set; } + public int OrderId { get; set; } + public Item Item { get; set; } + public decimal Quantity { get; set; } +} + +public class Item +{ + public int Id { get; set; } + public string Name { get; set; } +} +``` + +And the following DTO: + +```c# +public class OrderLineDTO +{ + public int Id { get; set; } + public int OrderId { get; set; } + public string Item { get; set; } + public decimal Quantity { get; set; } +} +``` + +You can use the Queryable Extensions like so: + +```c# +var configuration = new MapperConfiguration(cfg => + cfg.CreateProjection() + .ForMember(dto => dto.Item, conf => conf.MapFrom(ol => ol.Item.Name)), loggerFactory); + +public List GetLinesForOrder(int orderId) +{ + using (var context = new orderEntities()) + { + return context.OrderLines.Where(ol => ol.OrderId == orderId) + .ProjectTo(configuration).ToList(); + } +} +``` + +The `.ProjectTo()` will tell AutoMapper's mapping engine to emit a `select` clause to the IQueryable that will inform entity framework that it only needs to query the Name column of the Item table, same as if you manually projected your `IQueryable` to an `OrderLineDTO` with a `Select` clause. + +### Query Provider Limitations + +`ProjectTo` must be the last call in the LINQ method chain. ORMs work with entities, not DTOs. Apply any filtering and sorting on entities and, as the last step, project to DTOs. Query providers are highly complex and making the `ProjectTo` call last ensures the query provider works as closely as designed to build valid queries against the underlying query target (SQL, Mongo QL etc.). + +Note that for this feature to work, all type conversions must be explicitly handled in your Mapping. For example, you can not rely on the `ToString()` override of the `Item` class to inform entity framework to only select from the `Name` column, and any data type changes, such as `Double` to `Decimal` must be explicitly handled as well. + +### The instance API + +Starting with 8.0 there are similar ProjectTo methods on IMapper that feel more natural when you use IMapper with DI. + +### Preventing lazy loading/SELECT N+1 problems + +Because the LINQ projection built by AutoMapper is translated directly to a SQL query by the query provider, the mapping occurs at the SQL/ADO.NET level, and not touching your entities. All data is eagerly fetched and loaded into your DTOs. + +Nested collections use a Select to project child DTOs: + +```c# +from i in db.Instructors +orderby i.LastName +select new InstructorIndexData.InstructorModel +{ + ID = i.ID, + FirstMidName = i.FirstMidName, + LastName = i.LastName, + HireDate = i.HireDate, + OfficeAssignmentLocation = i.OfficeAssignment.Location, + Courses = i.Courses.Select(c => new InstructorIndexData.InstructorCourseModel + { + CourseID = c.CourseID, + CourseTitle = c.Title + }).ToList() +}; +``` + This map through AutoMapper will result in a SELECT N+1 problem, as each child `Course` will be queried one at a time, unless specified through your ORM to eagerly fetch. With LINQ projection, no special configuration or specification is needed with your ORM. The ORM uses the LINQ projection to build the exact SQL query needed. That means that you don't need to use explicit eager loading (`Include`) with `ProjectTo`. If you need something like filtered `Include`, have the filter in your map: -```c# - - CreateProjection().ForMember(d => d.Collection, o => o.MapFrom(s => s.Collection.Where(i => ...)); -``` - -### Custom projection - -In the case where members names don't line up, or you want to create calculated property, you can use MapFrom (the expression-based overload) to supply a custom expression for a destination member: - -```c# -var configuration = new MapperConfiguration(cfg => cfg.CreateProjection() - .ForMember(d => d.FullName, opt => opt.MapFrom(c => c.FirstName + " " + c.LastName)) - .ForMember(d => d.TotalContacts, opt => opt.MapFrom(c => c.Contacts.Count())); -``` - -AutoMapper passes the supplied expression with the built projection. As long as your query provider can interpret the supplied expression, everything will be passed down all the way to the database. - -If the expression is rejected from your query provider (Entity Framework, NHibernate, etc.), you might need to tweak your expression until you find one that is accepted. - -### Custom Type Conversion - -Occasionally, you need to completely replace a type conversion from a source to a destination type. In normal runtime mapping, this is accomplished via the ConvertUsing method. To perform the analog in LINQ projection, use the ConvertUsing method: - -```c# -cfg.CreateProjection().ConvertUsing(src => new Dest { Value = 10 }); -``` - -The expression-based `ConvertUsing` is slightly more limited than Func-based `ConvertUsing` overloads as only what is allowed in an Expression and the underlying LINQ provider will work. - -### Custom destination type constructors - -If your destination type has a custom constructor but you don't want to override the entire mapping, use the ConstructUsing expression-based method overload: - -```c# -cfg.CreateProjection() - .ConstructUsing(src => new Dest(src.Value + 10)); -``` - -AutoMapper will automatically match up destination constructor parameters to source members based on matching names, so only use this method if AutoMapper can't match up the destination constructor properly, or if you need extra customization during construction. - -### String conversion - -AutoMapper will automatically add `ToString()` when the destination member type is a string and the source member type is not. - -```c# -public class Order { - public OrderTypeEnum OrderType { get; set; } -} -public class OrderDto { - public string OrderType { get; set; } -} -var orders = dbContext.Orders.ProjectTo(configuration).ToList(); -orders[0].OrderType.ShouldEqual("Online"); -``` - -### Explicit expansion - -In some scenarios, such as OData, a generic DTO is returned through an IQueryable controller action. Without explicit instructions, AutoMapper will expand all members in the result. To control which members are expanded during projection, set ExplicitExpansion in the configuration and then pass in the members you want to explicitly expand: - -```c# -dbContext.Orders.ProjectTo(configuration, - dest => dest.Customer, - dest => dest.LineItems); -// or string-based -dbContext.Orders.ProjectTo(configuration, - null, - "Customer", - "LineItems"); -// for collections -dbContext.Orders.ProjectTo(configuration, - null, - dest => dest.LineItems.Select(item => item.Product)); -``` -For more information, see [the tests](https://github.com/AutoMapper/AutoMapper/search?p=1&q=ExplicitExpansion&utf8=%E2%9C%93). - -### Aggregations - -LINQ can support aggregate queries, and AutoMapper supports LINQ extension methods. In the custom projection example, if we renamed the `TotalContacts` property to `ContactsCount`, AutoMapper would match to the `Count()` extension method and the LINQ provider would translate the count into a correlated subquery to aggregate child records. - -AutoMapper can also support complex aggregations and nested restrictions, if the LINQ provider supports it: - -```c# -cfg.CreateProjection() - .ForMember(m => m.EnrollmentsStartingWithA, - opt => opt.MapFrom(c => c.Enrollments.Where(e => e.Student.LastName.StartsWith("A")).Count())); -``` - -This query returns the total number of students, for each course, whose last name starts with the letter 'A'. - -### Parameterization - -Occasionally, projections need runtime parameters for their values. Consider a projection that needs to pull in the current username as part of its data. Instead of using post-mapping code, we can parameterize our MapFrom configuration: - -```c# -string currentUserName = null; -cfg.CreateProjection() - .ForMember(m => m.CurrentUserName, opt => opt.MapFrom(src => currentUserName)); -``` - -When we project, we'll substitute our parameter at runtime: - -```c# -dbContext.Courses.ProjectTo(Config, new { currentUserName = Request.User.Name }); -``` - -This works by capturing the name of the closure's field name in the original expression, then using an anonymous object/dictionary to apply the value to the parameter value before the query is sent to the query provider. - -You may also use a dictionary to build the projection values: - -```c# -dbContext.Courses.ProjectTo(Config, new Dictionary { {"currentUserName", Request.User.Name} }); -``` - -However, using a dictionary will result in hard-coded values in the query instead of a parameterized query, so use with caution. - -### Supported mapping options - -Not all mapping options can be supported, as the expression generated must be interpreted by a LINQ provider. Only what is supported by LINQ providers is supported by AutoMapper: -* MapFrom (Expression-based) -* ConvertUsing (Expression-based) -* Ignore -* NullSubstitute +```c# + + CreateProjection().ForMember(d => d.Collection, o => o.MapFrom(s => s.Collection.Where(i => ...)); +``` + +### Custom projection + +In the case where members names don't line up, or you want to create calculated property, you can use MapFrom (the expression-based overload) to supply a custom expression for a destination member: + +```c# +var configuration = new MapperConfiguration(cfg => cfg.CreateProjection() + .ForMember(d => d.FullName, opt => opt.MapFrom(c => c.FirstName + " " + c.LastName)) + .ForMember(d => d.TotalContacts, opt => opt.MapFrom(c => c.Contacts.Count())), loggerFactory); +``` + +AutoMapper passes the supplied expression with the built projection. As long as your query provider can interpret the supplied expression, everything will be passed down all the way to the database. + +If the expression is rejected from your query provider (Entity Framework, NHibernate, etc.), you might need to tweak your expression until you find one that is accepted. + +### Custom Type Conversion + +Occasionally, you need to completely replace a type conversion from a source to a destination type. In normal runtime mapping, this is accomplished via the ConvertUsing method. To perform the analog in LINQ projection, use the ConvertUsing method: + +```c# +cfg.CreateProjection().ConvertUsing(src => new Dest { Value = 10 }); +``` + +The expression-based `ConvertUsing` is slightly more limited than Func-based `ConvertUsing` overloads as only what is allowed in an Expression and the underlying LINQ provider will work. + +### Custom destination type constructors + +If your destination type has a custom constructor but you don't want to override the entire mapping, use the ConstructUsing expression-based method overload: + +```c# +cfg.CreateProjection() + .ConstructUsing(src => new Dest(src.Value + 10)); +``` + +AutoMapper will automatically match up destination constructor parameters to source members based on matching names, so only use this method if AutoMapper can't match up the destination constructor properly, or if you need extra customization during construction. + +### String conversion + +AutoMapper will automatically add `ToString()` when the destination member type is a string and the source member type is not. + +```c# +public class Order { + public OrderTypeEnum OrderType { get; set; } +} +public class OrderDto { + public string OrderType { get; set; } +} +var orders = dbContext.Orders.ProjectTo(configuration).ToList(); +orders[0].OrderType.ShouldEqual("Online"); +``` + +### Explicit expansion + +In some scenarios, such as OData, a generic DTO is returned through an IQueryable controller action. Without explicit instructions, AutoMapper will expand all members in the result. To control which members are expanded during projection, set ExplicitExpansion in the configuration and then pass in the members you want to explicitly expand: + +```c# +dbContext.Orders.ProjectTo(configuration, + dest => dest.Customer, + dest => dest.LineItems); +// or string-based +dbContext.Orders.ProjectTo(configuration, + null, + "Customer", + "LineItems"); +// for collections +dbContext.Orders.ProjectTo(configuration, + null, + dest => dest.LineItems.Select(item => item.Product)); +``` +For more information, see [the tests](https://github.com/LuckyPennySoftware/AutoMapper/search?p=1&q=ExplicitExpansion&utf8=%E2%9C%93). + +### Aggregations + +LINQ can support aggregate queries, and AutoMapper supports LINQ extension methods. In the custom projection example, if we renamed the `TotalContacts` property to `ContactsCount`, AutoMapper would match to the `Count()` extension method and the LINQ provider would translate the count into a correlated subquery to aggregate child records. + +AutoMapper can also support complex aggregations and nested restrictions, if the LINQ provider supports it: + +```c# +cfg.CreateProjection() + .ForMember(m => m.EnrollmentsStartingWithA, + opt => opt.MapFrom(c => c.Enrollments.Where(e => e.Student.LastName.StartsWith("A")).Count())); +``` + +This query returns the total number of students, for each course, whose last name starts with the letter 'A'. + +### Parameterization + +Occasionally, projections need runtime parameters for their values. Consider a projection that needs to pull in the current username as part of its data. Instead of using post-mapping code, we can parameterize our MapFrom configuration: + +```c# +string currentUserName = null; +cfg.CreateProjection() + .ForMember(m => m.CurrentUserName, opt => opt.MapFrom(src => currentUserName)); +``` + +When we project, we'll substitute our parameter at runtime: + +```c# +dbContext.Courses.ProjectTo(Config, new { currentUserName = Request.User.Name }); +``` + +This works by capturing the name of the closure's field name in the original expression, then using an anonymous object/dictionary to apply the value to the parameter value before the query is sent to the query provider. + +You may also use a dictionary to build the projection values: + +```c# +dbContext.Courses.ProjectTo(Config, new Dictionary { {"currentUserName", Request.User.Name} }); +``` + +However, using a dictionary will result in hard-coded values in the query instead of a parameterized query, so use with caution. + +### Recursive models + +Ideally, you would avoid models that reference themselves (do some research). But if you must, you need to enable them: + +```c# +configuration.Internal().RecursiveQueriesMaxDepth = someRandomNumber; +``` + +### Supported mapping options + +Not all mapping options can be supported, as the expression generated must be interpreted by a LINQ provider. Only what is supported by LINQ providers is supported by AutoMapper: +* MapFrom (Expression-based) +* ConvertUsing (Expression-based) +* Ignore +* NullSubstitute * Value transformers -* IncludeMembers - -Not supported: -* Condition -* SetMappingOrder -* UseDestinationValue -* MapFrom (Func-based) -* Before/AfterMap -* Custom resolvers -* Custom type converters -* ForPath +* IncludeMembers +* Runtime polymorphic mapping with Include/IncludeBase + +Not supported: +* Condition +* SetMappingOrder +* UseDestinationValue +* MapFrom (Func-based) +* Before/AfterMap +* Custom resolvers +* Custom type converters +* ForPath * Value converters -* Runtime polymorphic mapping with Include/IncludeBase * **Any calculated property on your domain object** \ No newline at end of file diff --git a/docs/Reverse-Mapping-and-Unflattening.md b/docs/source/Reverse-Mapping-and-Unflattening.md similarity index 99% rename from docs/Reverse-Mapping-and-Unflattening.md rename to docs/source/Reverse-Mapping-and-Unflattening.md index 4140b0fcf2..68f03e7771 100644 --- a/docs/Reverse-Mapping-and-Unflattening.md +++ b/docs/source/Reverse-Mapping-and-Unflattening.md @@ -28,7 +28,7 @@ We can map both directions, including unflattening: var configuration = new MapperConfiguration(cfg => { cfg.CreateMap() .ReverseMap(); -}); +}, loggerFactory); ``` By calling `ReverseMap`, AutoMapper creates a reverse mapping configuration that includes unflattening: diff --git a/docs/Setup.md b/docs/source/Setup.md similarity index 95% rename from docs/Setup.md rename to docs/source/Setup.md index 68d98c4070..d2b192425b 100644 --- a/docs/Setup.md +++ b/docs/source/Setup.md @@ -4,7 +4,7 @@ var config = new MapperConfiguration(cfg => { cfg.AddProfile(); cfg.CreateMap(); -}); +}, loggerFactory); var mapper = config.CreateMapper(); // or @@ -23,7 +23,7 @@ cfg.CreateMap(); cfg.AddProfile(); MyBootstrapper.InitAutoMapper(cfg); -var mapperConfig = new MapperConfiguration(cfg); +var mapperConfig = new MapperConfiguration(cfg, loggerFactory); IMapper mapper = new Mapper(mapperConfig); ``` diff --git a/docs/Understanding-your-mapping.md b/docs/source/Understanding-your-mapping.md similarity index 97% rename from docs/Understanding-your-mapping.md rename to docs/source/Understanding-your-mapping.md index 9d1d51cf33..31bc7e6fa1 100644 --- a/docs/Understanding-your-mapping.md +++ b/docs/source/Understanding-your-mapping.md @@ -3,7 +3,7 @@ AutoMapper creates an execution plan for your mapping. That execution plan can be viewed as [an expression tree](https://msdn.microsoft.com/en-us/library/mt654263.aspx?f=255&MSPPError=-2147217396) during debugging. You can get a better view of the resulting code by installing [the ReadableExpressions VS extension](https://marketplace.visualstudio.com/items?itemName=vs-publisher-1232914.ReadableExpressionsVisualizers). If you need to see the code outside VS, you can use [the ReadableExpressions package directly](https://www.nuget.org/packages/AgileObjects.ReadableExpressions). [This DotNetFiddle](https://dotnetfiddle.net/aJYTGZ) has a live demo using the NuGet package, and [this article](https://agileobjects.co.uk/view-automapper-execution-plan-readableexpressions) describes using the VS extension. ```c# -var configuration = new MapperConfiguration(cfg => cfg.CreateMap()); +var configuration = new MapperConfiguration(cfg => cfg.CreateMap(), loggerFactory); var executionPlan = configuration.BuildExecutionPlan(typeof(Foo), typeof(Bar)); ``` diff --git a/docs/Value-converters.md b/docs/source/Value-converters.md similarity index 97% rename from docs/Value-converters.md rename to docs/source/Value-converters.md index 430a55399f..5c07e3e219 100644 --- a/docs/Value-converters.md +++ b/docs/source/Value-converters.md @@ -22,7 +22,7 @@ In simplified syntax: .ForMember(d => d.Amount, opt => opt.ConvertUsing(new CurrencyFormatter())); cfg.CreateMap() .ForMember(d => d.Total, opt => opt.ConvertUsing(new CurrencyFormatter())); - }); + }, loggerFactory); ``` You can customize the source member when the source member name does not match: @@ -38,7 +38,7 @@ You can customize the source member when the source member name does not match: .ForMember(d => d.Amount, opt => opt.ConvertUsing(new CurrencyFormatter(), src => src.OrderAmount)); cfg.CreateMap() .ForMember(d => d.Total, opt => opt.ConvertUsing(new CurrencyFormatter(), src => src.LITotal)); - }); + }, loggerFactory); ``` If you need the value converters instantiated by the [service locator](Dependency-injection.html), you can specify the type instead: @@ -54,7 +54,7 @@ If you need the value converters instantiated by the [service locator](Dependenc .ForMember(d => d.Amount, opt => opt.ConvertUsing()); cfg.CreateMap() .ForMember(d => d.Total, opt => opt.ConvertUsing()); - }); + }, loggerFactory); ``` If you do not know the types or member names at runtime, use the various overloads that accept `System.Type` and `string`-based members: @@ -70,7 +70,7 @@ If you do not know the types or member names at runtime, use the various overloa .ForMember("Amount", opt => opt.ConvertUsing(new CurrencyFormatter(), "OrderAmount")); cfg.CreateMap(typeof(OrderLineItem), typeof(OrderLineItemDto)) .ForMember("Total", opt => opt.ConvertUsing(new CurrencyFormatter(), "LITotal")); - }); + }, loggerFactory); ``` Value converters are only used for in-memory mapping execution. They will not work for [`ProjectTo`](Queryable-Extensions.html). diff --git a/docs/Value-transformers.md b/docs/source/Value-transformers.md similarity index 96% rename from docs/Value-transformers.md rename to docs/source/Value-transformers.md index 3735df1d5b..d6b05fd68f 100644 --- a/docs/Value-transformers.md +++ b/docs/source/Value-transformers.md @@ -12,7 +12,7 @@ You can create value transformers at several different levels: ```c# var configuration = new MapperConfiguration(cfg => { cfg.ValueTransformers.Add(val => val + "!!!"); -}); +}, loggerFactory); var source = new Source { Value = "Hello" }; var dest = mapper.Map(source); diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000000..f255fabae6 --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,39 @@ +# Configuration file for the Sphinx documentation builder. + +# -- Project information + +project = 'AutoMapper' +copyright = '2024, Jimmy Bogard' +author = 'Jimmy Bogard' + +# -- General configuration + +extensions = [ + 'sphinx.ext.duration', + 'sphinx.ext.doctest', + 'sphinx.ext.autodoc', + 'sphinx.ext.autosummary', + 'sphinx.ext.intersphinx', + 'myst_parser' +] + +intersphinx_mapping = { + 'python': ('https://docs.python.org/3/', None), + 'sphinx': ('https://www.sphinx-doc.org/en/master/', None), +} +intersphinx_disabled_domains = ['std'] + +templates_path = ['_templates'] + +# -- Options for HTML output + +html_theme = 'sphinx_rtd_theme' +html_theme_options = { + 'logo_only': True, + 'display_version': False +} +html_logo = 'img/logo.png' + + +# -- Options for EPUB output +epub_show_urls = 'footnote' \ No newline at end of file diff --git a/docs/img/logo.png b/docs/source/img/logo.png similarity index 100% rename from docs/img/logo.png rename to docs/source/img/logo.png diff --git a/docs/index.rst b/docs/source/index.rst similarity index 83% rename from docs/index.rst rename to docs/source/index.rst index 24f0f4d003..0729234987 100644 --- a/docs/index.rst +++ b/docs/source/index.rst @@ -11,10 +11,6 @@ other simple objects, whose design is better suited for serialization, communication, messaging, or simply an anti-corruption layer between the domain and application layer. -AutoMapper supports the following platforms: - -* `.NET Standard 2.1+ `_ - New to AutoMapper? Check out the :doc:`Getting-started` page first. .. _user-docs: @@ -24,9 +20,8 @@ New to AutoMapper? Check out the :doc:`Getting-started` page first. :caption: Overview Getting-started - Quickstart Understanding-your-mapping - The-MyGet-build + License-configuration .. _feature-docs: @@ -72,6 +67,8 @@ New to AutoMapper? Check out the :doc:`Getting-started` page first. :caption: Upgrading API-Changes + 15.0-Upgrade-Guide + 13.0-Upgrade-Guide 12.0-Upgrade-Guide 11.0-Upgrade-Guide 10.0-Upgrade-Guide @@ -89,7 +86,3 @@ Housekeeping ============ The latest builds can be found at `NuGet `_ - -The dev builds can be found at `MyGet `_ - -The discussion group is hosted on `Google Groups `_ diff --git a/src/AutoMapper.DI.Tests/AppDomainResolutionTests.cs b/src/AutoMapper.DI.Tests/AppDomainResolutionTests.cs new file mode 100644 index 0000000000..e8f76b9377 --- /dev/null +++ b/src/AutoMapper.DI.Tests/AppDomainResolutionTests.cs @@ -0,0 +1,42 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace AutoMapper.Extensions.Microsoft.DependencyInjection.Tests +{ + using System; + using AutoMapper.Internal; + using Shouldly; + using Xunit; + + public class AppDomainResolutionTests + { + private readonly IServiceProvider _provider; + + public AppDomainResolutionTests() + { + IServiceCollection services = new ServiceCollection(); + services.AddSingleton(NullLoggerFactory.Instance); + services.AddAutoMapper(_ => { }, typeof(AppDomainResolutionTests)); + _provider = services.BuildServiceProvider(); + } + + [Fact] + public void ShouldResolveConfiguration() + { + _provider.GetService().ShouldNotBeNull(); + } + + [Fact] + public void ShouldConfigureProfiles() + { + _provider.GetService().Internal().GetAllTypeMaps().Count.ShouldBe(4); + } + + [Fact] + public void ShouldResolveMapper() + { + _provider.GetService().ShouldNotBeNull(); + } + } +} \ No newline at end of file diff --git a/src/AutoMapper.DI.Tests/AssemblyResolutionTests.cs b/src/AutoMapper.DI.Tests/AssemblyResolutionTests.cs new file mode 100644 index 0000000000..9cb6d5a053 --- /dev/null +++ b/src/AutoMapper.DI.Tests/AssemblyResolutionTests.cs @@ -0,0 +1,55 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace AutoMapper.Extensions.Microsoft.DependencyInjection.Tests +{ + using System; + using System.Reflection; + using AutoMapper.Internal; + using Shouldly; + using Xunit; + + public class AssemblyResolutionTests + { + private static readonly IServiceProvider _provider; + + static AssemblyResolutionTests() + { + _provider = BuildServiceProvider(); + } + + private static ServiceProvider BuildServiceProvider() + { + IServiceCollection services = new ServiceCollection(); + services.AddSingleton(NullLoggerFactory.Instance); + services.AddAutoMapper(_ => { }, typeof(Source).GetTypeInfo().Assembly); + var serviceProvider = services.BuildServiceProvider(); + return serviceProvider; + } + + [Fact] + public void ShouldResolveConfiguration() + { + _provider.GetService().ShouldNotBeNull(); + } + + [Fact] + public void ShouldConfigureProfiles() + { + _provider.GetService().Internal().GetAllTypeMaps().Count.ShouldBe(4); + } + + [Fact] + public void ShouldResolveMapper() + { + _provider.GetService().ShouldNotBeNull(); + } + + [Fact] + public void CanRegisterTwiceWithoutProblems() + { + new Action(() => BuildServiceProvider()).ShouldNotThrow(); + } + } +} \ No newline at end of file diff --git a/src/AutoMapper.DI.Tests/AttributeTests.cs b/src/AutoMapper.DI.Tests/AttributeTests.cs new file mode 100644 index 0000000000..960f1fc24d --- /dev/null +++ b/src/AutoMapper.DI.Tests/AttributeTests.cs @@ -0,0 +1,30 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Shouldly; +using Xunit; + +namespace AutoMapper.Extensions.Microsoft.DependencyInjection.Tests +{ + public class AttributeTests + { + [Fact] + public void Should_not_register_static_instance_when_configured() + { + IServiceCollection services = new ServiceCollection(); + services.AddSingleton(NullLoggerFactory.Instance); + services.AddAutoMapper(_ => { }, typeof(Source3)); + + var serviceProvider = services.BuildServiceProvider(); + + var mapper = serviceProvider.GetService(); + + var source = new Source3 {Value = 3}; + + var dest = mapper.Map(source); + + dest.Value.ShouldBe(source.Value); + } + } +} \ No newline at end of file diff --git a/src/AutoMapper.DI.Tests/AutoMapper.DI.Tests.csproj b/src/AutoMapper.DI.Tests/AutoMapper.DI.Tests.csproj new file mode 100644 index 0000000000..1ae0467efc --- /dev/null +++ b/src/AutoMapper.DI.Tests/AutoMapper.DI.Tests.csproj @@ -0,0 +1,26 @@ + + + + net10.0 + true + AutoMapper.Extensions.Microsoft.DependencyInjection.Tests + AutoMapper.Extensions.Microsoft.DependencyInjection.Tests + true + false + false + false + + + + + + + + + + + + + + + diff --git a/src/AutoMapper.DI.Tests/DependencyTests.cs b/src/AutoMapper.DI.Tests/DependencyTests.cs new file mode 100644 index 0000000000..c897c4e862 --- /dev/null +++ b/src/AutoMapper.DI.Tests/DependencyTests.cs @@ -0,0 +1,44 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace AutoMapper.Extensions.Microsoft.DependencyInjection.Tests +{ + using System; + using global::Microsoft.Extensions.DependencyInjection; + using Shouldly; + using Xunit; + + public class DependencyTests + { + private readonly IServiceProvider _provider; + + public DependencyTests() + { + IServiceCollection services = new ServiceCollection(); + services.AddTransient(sp => new FooService(5)); + services.AddSingleton(NullLoggerFactory.Instance); + services.AddAutoMapper(_ => { }, typeof(Source), typeof(Profile)); + _provider = services.BuildServiceProvider(); + + _provider.GetService().AssertConfigurationIsValid(); + } + + [Fact] + public void ShouldResolveWithDependency() + { + var mapper = _provider.GetService(); + var dest = mapper.Map(new Source2()); + + dest.ResolvedValue.ShouldBe(5); + } + + [Fact] + public void ShouldConvertWithDependency() + { + var mapper = _provider.GetService(); + var dest = mapper.Map(new Source2 { ConvertedValue = 5}); + + dest.ConvertedValue.ShouldBe(10); + } + } +} diff --git a/src/AutoMapper.DI.Tests/Integrations/ServiceLifetimeTests.cs b/src/AutoMapper.DI.Tests/Integrations/ServiceLifetimeTests.cs new file mode 100644 index 0000000000..984e2d49e4 --- /dev/null +++ b/src/AutoMapper.DI.Tests/Integrations/ServiceLifetimeTests.cs @@ -0,0 +1,70 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Shouldly; +using Xunit; + +namespace AutoMapper.Extensions.Microsoft.DependencyInjection.Tests.Integrations +{ + public class ServiceLifetimeTests + { + internal interface ISingletonService + { + Bar DoTheThing(Foo theObj); + } + + internal class TestSingletonService : ISingletonService + { + private readonly IMapper _mapper; + + public TestSingletonService(IMapper mapper) + { + _mapper = mapper; + } + + public Bar DoTheThing(Foo theObj) + { + var bar = _mapper.Map(theObj); + return bar; + } + } + + internal class Foo + { + public int TheValue { get; set; } + } + + internal class Bar + { + public int TheValue { get; set; } + } + + + [Fact] + public void CanUseDefaultInjectedIMapperInSingletonService() + { + //arrange + var services = new ServiceCollection(); + services.TryAddSingleton(); + services.AddSingleton(NullLoggerFactory.Instance); + services.AddAutoMapper(opt => + { + opt.CreateMap().ReverseMap(); + }, GetType().Assembly); + var sp = services.BuildServiceProvider(); + Bar actual; + + //act + using (var scope = sp.CreateScope()) + { + var service = scope.ServiceProvider.GetService(); + actual = service.DoTheThing(new Foo{TheValue = 1}); + } + + //assert + actual.ShouldNotBeNull(); + actual.TheValue.ShouldBe(1); + } + } +} \ No newline at end of file diff --git a/src/AutoMapper.DI.Tests/MultipleRegistrationTests.cs b/src/AutoMapper.DI.Tests/MultipleRegistrationTests.cs new file mode 100644 index 0000000000..592bfddea2 --- /dev/null +++ b/src/AutoMapper.DI.Tests/MultipleRegistrationTests.cs @@ -0,0 +1,45 @@ +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Shouldly; +using Xunit; + +namespace AutoMapper.Extensions.Microsoft.DependencyInjection.Tests +{ + public class MultipleRegistrationTests + { + [Fact] + public void Can_register_multiple_times() + { + var services = new ServiceCollection(); + services.AddSingleton(NullLoggerFactory.Instance); + + services.AddAutoMapper(cfg => { }); + services.AddAutoMapper(cfg => { }); + services.AddAutoMapper(cfg => { }); + + var serviceProvider = services.BuildServiceProvider(); + + serviceProvider.GetService().ShouldNotBeNull(); + } + + [Fact] + public void Can_register_assembly_multiple_times() + { + var services = new ServiceCollection(); + services.AddSingleton(NullLoggerFactory.Instance); + + services.AddAutoMapper(_ => { }, typeof(MultipleRegistrationTests)); + services.AddAutoMapper(_ => { }, typeof(MultipleRegistrationTests)); + services.AddAutoMapper(_ => { }, typeof(MultipleRegistrationTests)); + services.AddTransient(); + + var serviceProvider = services.BuildServiceProvider(); + + serviceProvider.GetService().ShouldNotBeNull(); + serviceProvider.GetService().ShouldNotBeNull(); + serviceProvider.GetServices().Count().ShouldBe(1); + } + } +} \ No newline at end of file diff --git a/src/AutoMapper.DI.Tests/Profiles.cs b/src/AutoMapper.DI.Tests/Profiles.cs new file mode 100644 index 0000000000..5e9b14e61e --- /dev/null +++ b/src/AutoMapper.DI.Tests/Profiles.cs @@ -0,0 +1,153 @@ +using System; + +namespace AutoMapper.Extensions.Microsoft.DependencyInjection.Tests +{ + public class Source + { + + } + + public class Dest + { + + } + + public class Source2 + { + public int ConvertedValue { get; set; } + } + + public class Dest2 + { + public int ResolvedValue { get; set; } + public int ConvertedValue { get; set; } + } + + public class Source3 + { + public int Value { get; set; } + } + + [AutoMap(typeof(Source3))] + public class Dest3 + { + public int Value { get; set; } + } + + public class Profile1 : Profile + { + public Profile1() + { + CreateMap(); + } + } + + public abstract class AbstractProfile : Profile { } + + internal class Profile2 : Profile + { + public Profile2() + { + CreateMap() + .ForMember(d => d.ResolvedValue, opt => opt.MapFrom()) + .ForMember(d => d.ConvertedValue, opt => opt.ConvertUsing()); + CreateMap(typeof(Enum), typeof(EnumDescriptor<>)).ConvertUsing(typeof(EnumDescriptorTypeConverter<>)); + } + } + + public class DependencyResolver : IValueResolver + { + private readonly ISomeService _service; + + public DependencyResolver(ISomeService service) + { + _service = service; + } + + public int Resolve(object source, object destination, int destMember, ResolutionContext context) + { + return _service.Modify(destMember); + } + } + + public interface ISomeService + { + int Modify(int value); + } + + public class MutableService : ISomeService + { + public int Value { get; set; } + + public int Modify(int value) => value + Value; + } + + public class FooService : ISomeService + { + private readonly int _value; + + public FooService(int value) + { + _value = value; + } + + public int Modify(int value) => value + _value; + } + + internal class FooMappingAction : IMappingAction + { + public void Process(object source, object destination, ResolutionContext context) { } + } + + internal class FooValueResolver: IValueResolver + { + public object Resolve(object source, object destination, object destMember, ResolutionContext context) + { + return null; + } + } + + internal class FooMemberValueResolver : IMemberValueResolver + { + public object Resolve(object source, object destination, object sourceMember, object destMember, ResolutionContext context) + { + return null; + } + } + + internal class FooTypeConverter : ITypeConverter + { + public object Convert(object source, object destination, ResolutionContext context) + { + return null; + } + } + + public class EnumDescriptor where TSource : Enum + { + public int Value { get; set; } + } + + public class EnumDescriptorTypeConverter : ITypeConverter> + where TSource : Enum + { + public EnumDescriptor Convert(Enum source, EnumDescriptor destination, ResolutionContext context) => + new EnumDescriptor{ Value = int.MaxValue }; + } + + internal class FooValueConverter : IValueConverter + { + public int Convert(int sourceMember, ResolutionContext context) + => sourceMember + 1; + } + + internal class DependencyValueConverter : IValueConverter + { + private readonly ISomeService _service; + + public DependencyValueConverter(ISomeService service) => _service = service; + + public int Convert(int sourceMember, ResolutionContext context) + => _service.Modify(sourceMember); + } +} \ No newline at end of file diff --git a/src/AutoMapper.DI.Tests/Properties/AssemblyInfo.cs b/src/AutoMapper.DI.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..c4d34326a6 --- /dev/null +++ b/src/AutoMapper.DI.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,21 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("AutoMapper.Extensions.Microsoft.DependencyInjection.Tests")] +[assembly: AssemblyTrademark("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("a93a7f85-292a-4130-891d-4307d3f60c30")] + +[assembly: Xunit.CollectionBehavior(DisableTestParallelization = true)] diff --git a/src/AutoMapper.DI.Tests/ScopeTests.cs b/src/AutoMapper.DI.Tests/ScopeTests.cs new file mode 100644 index 0000000000..c7d5848e57 --- /dev/null +++ b/src/AutoMapper.DI.Tests/ScopeTests.cs @@ -0,0 +1,82 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Shouldly; +using Xunit; + +namespace AutoMapper.Extensions.Microsoft.DependencyInjection.Tests +{ + public class ScopeTests + { + [Fact] + public void Can_depend_on_scoped_services_as_transient_default() + { + var services = new ServiceCollection(); + services.AddSingleton(NullLoggerFactory.Instance); + services.AddAutoMapper(_ => { }, typeof(Source).Assembly); + services.AddScoped(); + + var provider = services.BuildServiceProvider(); + + using (var scope = provider.CreateScope()) + { + var mutableService = (MutableService)scope.ServiceProvider.GetService(); + mutableService.Value = 10; + + var mapper = scope.ServiceProvider.GetService(); + + var dest = mapper.Map(new Source2 {ConvertedValue = 5}); + + dest.ConvertedValue.ShouldBe(15); + } + } + + [Fact] + public void Can_depend_on_scoped_services_as_scoped() + { + var services = new ServiceCollection(); + services.AddSingleton(NullLoggerFactory.Instance); + services.AddAutoMapper(_ => + { + }, [typeof(Source).Assembly], ServiceLifetime.Scoped); + services.AddScoped(); + + var provider = services.BuildServiceProvider(); + + using (var scope = provider.CreateScope()) + { + var mutableService = (MutableService)scope.ServiceProvider.GetService(); + mutableService.Value = 10; + + var mapper = scope.ServiceProvider.GetService(); + + var dest = mapper.Map(new Source2 {ConvertedValue = 5}); + + dest.ConvertedValue.ShouldBe(15); + } + } + + [Fact] + public void Cannot_correctly_resolve_scoped_services_as_singleton() + { + var services = new ServiceCollection(); + services.AddSingleton(NullLoggerFactory.Instance); + services.AddAutoMapper(_ => { }, [typeof(Source).Assembly], ServiceLifetime.Singleton); + services.AddScoped(); + + var provider = services.BuildServiceProvider(); + + using (var scope = provider.CreateScope()) + { + var mutableService = (MutableService)scope.ServiceProvider.GetService(); + mutableService.Value = 10; + + var mapper = scope.ServiceProvider.GetService(); + + var dest = mapper.Map(new Source2 {ConvertedValue = 5}); + + dest.ConvertedValue.ShouldBe(5); + } + } + } +} \ No newline at end of file diff --git a/src/AutoMapper.DI.Tests/ServiceLifetimeTests.cs b/src/AutoMapper.DI.Tests/ServiceLifetimeTests.cs new file mode 100644 index 0000000000..1c50a92016 --- /dev/null +++ b/src/AutoMapper.DI.Tests/ServiceLifetimeTests.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using Xunit; + +namespace AutoMapper.Extensions.Microsoft.DependencyInjection.Tests +{ + public class ServiceLifetimeTests + { + //Implicitly Transient + [Fact] + public void AddAutoMapperExtensionDefaultWithAssemblySingleDelegateArgCollection() + { + //arrange + var serviceCollection = new ServiceCollection(); + + //act + serviceCollection.AddAutoMapper(_ => + { + }); + var serviceDescriptor = serviceCollection.FirstOrDefault(sd => sd.ServiceType == typeof(IMapper)); + + //assert + serviceDescriptor.ShouldNotBeNull(); + serviceDescriptor.Lifetime.ShouldBe(ServiceLifetime.Transient); + } + + [Fact] + public void AddAutoMapperExtensionDefaultWithServiceLifetime() + { + //arrange + var serviceCollection = new ServiceCollection(); + + //act + serviceCollection.AddAutoMapper(_ => + { + }, new List(), ServiceLifetime.Singleton); + var serviceDescriptor = serviceCollection.FirstOrDefault(sd => sd.ServiceType == typeof(IMapper)); + + //assert + serviceDescriptor.ShouldNotBeNull(); + serviceDescriptor.Lifetime.ShouldBe(ServiceLifetime.Singleton); + } + } +} \ No newline at end of file diff --git a/src/AutoMapper.DI.Tests/ServiceProviderTests.cs b/src/AutoMapper.DI.Tests/ServiceProviderTests.cs new file mode 100644 index 0000000000..4cd9ee4dba --- /dev/null +++ b/src/AutoMapper.DI.Tests/ServiceProviderTests.cs @@ -0,0 +1,48 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace AutoMapper.Extensions.Microsoft.DependencyInjection.Tests +{ + using System; + using global::Microsoft.Extensions.DependencyInjection; + using Shouldly; + using Xunit; + + public class ServiceProviderTests + { + private readonly IServiceProvider _provider; + + public ServiceProviderTests() + { + IServiceCollection services = new ServiceCollection(); + services.AddTransient(sp => new FooService(5)); + services.AddSingleton(NullLoggerFactory.Instance); + services.AddAutoMapper((sp, _) => + { + var service = sp.GetRequiredService(); + service.Modify(5); + }, typeof(Source), typeof(Profile)); + _provider = services.BuildServiceProvider(); + + _provider.GetService().AssertConfigurationIsValid(); + } + + [Fact] + public void ShouldResolveWithDependency() + { + var mapper = _provider.GetService(); + var dest = mapper.Map(new Source2()); + + dest.ResolvedValue.ShouldBe(5); + } + + [Fact] + public void ShouldConvertWithDependency() + { + var mapper = _provider.GetService(); + var dest = mapper.Map(new Source2 { ConvertedValue = 5}); + + dest.ConvertedValue.ShouldBe(10); + } + } +} diff --git a/src/AutoMapper.DI.Tests/TypeResolutionTests.cs b/src/AutoMapper.DI.Tests/TypeResolutionTests.cs new file mode 100644 index 0000000000..924d62d2ab --- /dev/null +++ b/src/AutoMapper.DI.Tests/TypeResolutionTests.cs @@ -0,0 +1,82 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace AutoMapper.Extensions.Microsoft.DependencyInjection.Tests +{ + using System; + using AutoMapper.Internal; + using Shouldly; + using Xunit; + + public class TypeResolutionTests + { + private readonly IServiceProvider _provider; + + public TypeResolutionTests() + { + IServiceCollection services = new ServiceCollection(); + services.AddSingleton(NullLoggerFactory.Instance); + services.AddAutoMapper(_ => + { + }, typeof(Source)); + + _provider = services.BuildServiceProvider(); + } + + [Fact] + public void ShouldResolveConfiguration() + { + _provider.GetService().ShouldNotBeNull(); + } + + [Fact] + public void ShouldConfigureProfiles() + { + _provider.GetService().Internal().GetAllTypeMaps().Count.ShouldBe(4); + } + + [Fact] + public void ShouldResolveMapper() + { + _provider.GetService().ShouldNotBeNull(); + } + + [Fact] + public void ShouldResolveMappingAction() + { + _provider.GetService().ShouldNotBeNull(); + } + + [Fact] + public void ShouldResolveValueResolver() + { + _provider.GetService().ShouldNotBeNull(); + } + + [Fact] + public void ShouldResolveMemberValueResolver() + { + _provider.GetService().ShouldNotBeNull(); + } + + [Fact] + public void ShouldResolveTypeConverter() + { + _provider.GetService().ShouldNotBeNull(); + } + + [Fact] + public void ShouldResolveGenericTypeConverter() + { + _provider.GetService>().ShouldNotBeNull(); + _provider.GetService().Map>(ConsoleColor.Green).Value.ShouldBe(int.MaxValue); + } + + [Fact] + public void ShouldResolveValueConverter() + { + _provider.GetService().ShouldNotBeNull(); + } + } +} \ No newline at end of file diff --git a/src/AutoMapper/ApiCompat/PreBuild.ps1 b/src/AutoMapper/ApiCompat/PreBuild.ps1 index 88e35158b4..0e6824f4ea 100644 --- a/src/AutoMapper/ApiCompat/PreBuild.ps1 +++ b/src/AutoMapper/ApiCompat/PreBuild.ps1 @@ -10,4 +10,4 @@ if($versionNumbers[1] -eq "0" -AND $versionNumbers[2] -eq "0") $oldVersion = $oldVersion.ToString() +".0.0" echo $oldVersion & ..\..\nuget install AutoMapper -Version $oldVersion -OutputDirectory ..\LastMajorVersionBinary -& copy ..\LastMajorVersionBinary\AutoMapper.$oldVersion\lib\netstandard2.1\AutoMapper.dll ..\LastMajorVersionBinary +& copy ..\LastMajorVersionBinary\AutoMapper.$oldVersion\lib\net*.0\AutoMapper.dll ..\LastMajorVersionBinary diff --git a/src/AutoMapper/ApiCompat/PreBuild.sh b/src/AutoMapper/ApiCompat/PreBuild.sh index 3f611f5766..978cf69357 100644 --- a/src/AutoMapper/ApiCompat/PreBuild.sh +++ b/src/AutoMapper/ApiCompat/PreBuild.sh @@ -1,15 +1,30 @@ #!/bin/bash -version=$1 -echo $version -readarray -d . -t versionNumbers <<< $version -if [[ ${versionNumbers[1]} -eq "0" && ${versionNumbers[2]} -eq "0" ]] -then - oldVersion=$(({versionNumbers[0]} - 1)) + +version="$1" +echo "$version" + +# Split version string into components +IFS='.' +set -- "$version" +versionNumbers=("$@") + +major="${versionNumbers[0]}" +minor="${versionNumbers[1]}" +patch="${versionNumbers[2]}" + +# Determine previous major version if minor and patch are 0 +if [ "$minor" -eq 0 ] && [ "$patch" -eq 0 ]; then + oldVersion=$((major - 1)) else - oldVersion=${versionNumbers[0]} + oldVersion=$major fi + oldVersion="$oldVersion.0.0" -echo $oldVersion +echo "$oldVersion" + +# Fetch and extract AutoMapper package rm -rf ../LastMajorVersionBinary -curl https://globalcdn.nuget.org/packages/automapper.$oldVersion.nupkg --create-dirs -o ../LastMajorVersionBinary/automapper.$oldVersion.nupkg -unzip -j ../LastMajorVersionBinary/automapper.$oldVersion.nupkg lib/netstandard2.1/AutoMapper.dll -d ../LastMajorVersionBinary +curl -sSL "https://globalcdn.nuget.org/packages/automapper.$oldVersion.nupkg" \ + --create-dirs -o "../LastMajorVersionBinary/automapper.$oldVersion.nupkg" + +unzip -j "../LastMajorVersionBinary/automapper.$oldVersion.nupkg" "lib/net*.0/AutoMapper.dll" -d ../LastMajorVersionBinary \ No newline at end of file diff --git a/src/AutoMapper/ApiCompatBaseline.txt b/src/AutoMapper/ApiCompatBaseline.txt index 90da3f7e58..a048699c6f 100644 --- a/src/AutoMapper/ApiCompatBaseline.txt +++ b/src/AutoMapper/ApiCompatBaseline.txt @@ -1,6 +1,37 @@ Compat issues with assembly AutoMapper: CannotChangeAttribute : Attribute 'System.AttributeUsageAttribute' on 'AutoMapper.AutoMapAttribute' changed from '[AttributeUsageAttribute(1036, AllowMultiple=true)]' in the contract to '[AttributeUsageAttribute(AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Struct, AllowMultiple=true)]' in the implementation. -InterfacesShouldHaveSameMembers : Interface member 'public void AutoMapper.Configuration.ICtorParamConfigurationExpression.ExplicitExpansion()' is present in the implementation but not in the contract. +CannotSealType : Type 'AutoMapper.AutoMapperConfigurationException.TypeMapConfigErrors' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract. +TypeCannotChangeClassification : Type 'AutoMapper.AutoMapperConfigurationException.TypeMapConfigErrors' is a 'struct' in the implementation but is a 'class' in the contract. +CannotSealType : Type 'AutoMapper.DuplicateTypeMapConfigurationException.TypeMapConfigErrors' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract. +TypeCannotChangeClassification : Type 'AutoMapper.DuplicateTypeMapConfigurationException.TypeMapConfigErrors' is a 'struct' in the implementation but is a 'class' in the contract. +CannotRemoveAttribute : Attribute 'System.Runtime.CompilerServices.NullableContextAttribute' exists on 'AutoMapper.IMapper' in the contract but not the implementation. +CannotRemoveAttribute : Attribute 'System.Runtime.CompilerServices.NullableContextAttribute' exists on 'AutoMapper.IMapper.Map(System.Object, System.Object, System.Type, System.Type, System.Action>)' in the contract but not the implementation. +CannotRemoveAttribute : Attribute 'System.Runtime.CompilerServices.NullableAttribute' exists on parameter 'opts' on member 'AutoMapper.IMapper.Map(System.Object, System.Object, System.Type, System.Type, System.Action>)' in the contract but not the implementation. +CannotRemoveAttribute : Attribute 'System.Runtime.CompilerServices.NullableContextAttribute' exists on 'AutoMapper.IMapper.Map(System.Object, System.Type, System.Type, System.Action>)' in the contract but not the implementation. +CannotRemoveAttribute : Attribute 'System.Runtime.CompilerServices.NullableAttribute' exists on parameter 'destinationType' on member 'AutoMapper.IMapper.Map(System.Object, System.Type, System.Type, System.Action>)' in the contract but not the implementation. +CannotRemoveAttribute : Attribute 'System.Runtime.CompilerServices.NullableAttribute' exists on parameter 'opts' on member 'AutoMapper.IMapper.Map(System.Object, System.Type, System.Type, System.Action>)' in the contract but not the implementation. +CannotRemoveAttribute : Attribute 'System.Runtime.CompilerServices.NullableContextAttribute' exists on 'AutoMapper.IMapper.Map(System.Object, System.Action>)' in the contract but not the implementation. +CannotRemoveAttribute : Attribute 'System.Runtime.CompilerServices.NullableAttribute' exists on parameter 'opts' on member 'AutoMapper.IMapper.Map(System.Object, System.Action>)' in the contract but not the implementation. +CannotRemoveAttribute : Attribute 'System.Runtime.CompilerServices.NullableContextAttribute' exists on 'AutoMapper.IMapper.Map(TSource, System.Action>)' in the contract but not the implementation. +CannotRemoveAttribute : Attribute 'System.Runtime.CompilerServices.NullableAttribute' exists on parameter 'opts' on member 'AutoMapper.IMapper.Map(TSource, System.Action>)' in the contract but not the implementation. +CannotRemoveAttribute : Attribute 'System.Runtime.CompilerServices.NullableContextAttribute' exists on 'AutoMapper.IMapper.Map(TSource, TDestination, System.Action>)' in the contract but not the implementation. +CannotRemoveAttribute : Attribute 'System.Runtime.CompilerServices.NullableAttribute' exists on parameter 'opts' on member 'AutoMapper.IMapper.Map(TSource, TDestination, System.Action>)' in the contract but not the implementation. +CannotRemoveAttribute : Attribute 'System.Runtime.CompilerServices.NullableAttribute' exists on parameter 'parameters' on member 'AutoMapper.IMapper.ProjectTo(System.Linq.IQueryable, System.Type, System.Collections.Generic.IDictionary, System.String[])' in the contract but not the implementation. +CannotRemoveAttribute : Attribute 'System.Runtime.CompilerServices.NullableAttribute' exists on parameter 'parameters' on member 'AutoMapper.IMapper.ProjectTo(System.Linq.IQueryable, System.Collections.Generic.IDictionary, System.String[])' in the contract but not the implementation. +CannotRemoveAttribute : Attribute 'System.Runtime.CompilerServices.NullableAttribute' exists on generic param 'TDestination' on member 'AutoMapper.IMapper.ProjectTo(System.Linq.IQueryable, System.Collections.Generic.IDictionary, System.String[])' in the contract but not the implementation. +CannotRemoveAttribute : Attribute 'System.Runtime.CompilerServices.NullableAttribute' exists on parameter 'parameters' on member 'AutoMapper.IMapper.ProjectTo(System.Linq.IQueryable, System.Object, System.Linq.Expressions.Expression>[])' in the contract but not the implementation. +CannotRemoveAttribute : Attribute 'System.Runtime.CompilerServices.NullableAttribute' exists on generic param 'TDestination' on member 'AutoMapper.IMapper.ProjectTo(System.Linq.IQueryable, System.Object, System.Linq.Expressions.Expression>[])' in the contract but not the implementation. +CannotRemoveAttribute : Attribute 'System.Runtime.CompilerServices.NullableContextAttribute' exists on 'AutoMapper.IMapperBase' in the contract but not the implementation. +CannotRemoveAttribute : Attribute 'System.Runtime.CompilerServices.NullableAttribute' exists on parameter 'destinationType' on member 'AutoMapper.IMapperBase.Map(System.Object, System.Type, System.Type)' in the contract but not the implementation. +CannotRemoveAttribute : Attribute 'System.Runtime.CompilerServices.NullableAttribute' exists on generic param 'TDestination' on member 'AutoMapper.ResolutionContext.AutoMapper.IMapperBase.Map(System.Object)' in the contract but not the implementation. +CannotRemoveAttribute : Attribute 'System.Runtime.CompilerServices.NullableAttribute' exists on generic param 'TSource' on member 'AutoMapper.ResolutionContext.AutoMapper.IMapperBase.Map(TSource)' in the contract but not the implementation. +CannotRemoveAttribute : Attribute 'System.Runtime.CompilerServices.NullableAttribute' exists on generic param 'TDestination' on member 'AutoMapper.ResolutionContext.AutoMapper.IMapperBase.Map(TSource)' in the contract but not the implementation. +CannotRemoveAttribute : Attribute 'System.Runtime.CompilerServices.NullableAttribute' exists on generic param 'TSource' on member 'AutoMapper.ResolutionContext.AutoMapper.IMapperBase.Map(TSource, TDestination)' in the contract but not the implementation. +CannotRemoveAttribute : Attribute 'System.Runtime.CompilerServices.NullableAttribute' exists on generic param 'TDestination' on member 'AutoMapper.ResolutionContext.AutoMapper.IMapperBase.Map(TSource, TDestination)' in the contract but not the implementation. +MembersMustExist : Member 'public void AutoMapper.Configuration.MappingExpression..ctor(AutoMapper.Internal.TypePair, AutoMapper.MemberList)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public void AutoMapper.Configuration.MappingExpression..ctor(AutoMapper.MemberList, System.Type, System.Type)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'protected void AutoMapper.Configuration.MappingExpressionBase..ctor(AutoMapper.MemberList, System.Type, System.Type)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'public void AutoMapper.Configuration.ValidationContext..ctor(AutoMapper.Internal.TypePair, AutoMapper.MemberMap, AutoMapper.TypeMap, AutoMapper.Internal.Mappers.IObjectMapper)' does not exist in the implementation but it does exist in the contract. CannotChangeAttribute : Attribute 'System.AttributeUsageAttribute' on 'AutoMapper.Configuration.Annotations.IgnoreAttribute' changed from '[AttributeUsageAttribute(384)]' in the contract to '[AttributeUsageAttribute(AttributeTargets.Field | AttributeTargets.Property)]' in the implementation. CannotChangeAttribute : Attribute 'System.AttributeUsageAttribute' on 'AutoMapper.Configuration.Annotations.MapAtRuntimeAttribute' changed from '[AttributeUsageAttribute(384)]' in the contract to '[AttributeUsageAttribute(AttributeTargets.Field | AttributeTargets.Property)]' in the implementation. CannotChangeAttribute : Attribute 'System.AttributeUsageAttribute' on 'AutoMapper.Configuration.Annotations.MappingOrderAttribute' changed from '[AttributeUsageAttribute(384)]' in the contract to '[AttributeUsageAttribute(AttributeTargets.Field | AttributeTargets.Property)]' in the implementation. @@ -9,5 +40,34 @@ CannotChangeAttribute : Attribute 'System.AttributeUsageAttribute' on 'AutoMappe CannotChangeAttribute : Attribute 'System.AttributeUsageAttribute' on 'AutoMapper.Configuration.Annotations.UseExistingValueAttribute' changed from '[AttributeUsageAttribute(384)]' in the contract to '[AttributeUsageAttribute(AttributeTargets.Field | AttributeTargets.Property)]' in the implementation. CannotChangeAttribute : Attribute 'System.AttributeUsageAttribute' on 'AutoMapper.Configuration.Annotations.ValueConverterAttribute' changed from '[AttributeUsageAttribute(384)]' in the contract to '[AttributeUsageAttribute(AttributeTargets.Field | AttributeTargets.Property)]' in the implementation. CannotChangeAttribute : Attribute 'System.AttributeUsageAttribute' on 'AutoMapper.Configuration.Annotations.ValueResolverAttribute' changed from '[AttributeUsageAttribute(384)]' in the contract to '[AttributeUsageAttribute(AttributeTargets.Field | AttributeTargets.Property)]' in the implementation. -TypeCannotChangeClassification : Type 'AutoMapper.Execution.TypeMapPlanBuilder' is a 'ref struct' in the implementation but is a 'struct' in the contract. -Total Issues: 11 +CannotSealType : Type 'AutoMapper.Internal.Mappers.AssignableMapper' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract. +CannotSealType : Type 'AutoMapper.Internal.Mappers.CollectionMapper' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract. +CannotSealType : Type 'AutoMapper.Internal.Mappers.ConstructorMapper' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract. +CannotSealType : Type 'AutoMapper.Internal.Mappers.ConversionOperatorMapper' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract. +CannotSealType : Type 'AutoMapper.Internal.Mappers.ConvertMapper' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract. +CannotSealType : Type 'AutoMapper.Internal.Mappers.EnumToEnumMapper' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract. +CannotSealType : Type 'AutoMapper.Internal.Mappers.FromDynamicMapper' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract. +CannotSealType : Type 'AutoMapper.Internal.Mappers.FromStringDictionaryMapper' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract. +CannotSealType : Type 'AutoMapper.Internal.Mappers.KeyValueMapper' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract. +CannotSealType : Type 'AutoMapper.Internal.Mappers.MultidimensionalArrayFiller' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract. +TypeCannotChangeClassification : Type 'AutoMapper.Internal.Mappers.MultidimensionalArrayFiller' is a 'struct' in the implementation but is a 'class' in the contract. +CannotSealType : Type 'AutoMapper.Internal.Mappers.NullableDestinationMapper' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract. +CannotSealType : Type 'AutoMapper.Internal.Mappers.NullableSourceMapper' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract. +CannotSealType : Type 'AutoMapper.Internal.Mappers.ParseStringMapper' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract. +CannotSealType : Type 'AutoMapper.Internal.Mappers.StringToEnumMapper' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract. +CannotSealType : Type 'AutoMapper.Internal.Mappers.ToDynamicMapper' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract. +CannotSealType : Type 'AutoMapper.Internal.Mappers.ToStringDictionaryMapper' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract. +CannotSealType : Type 'AutoMapper.Internal.Mappers.ToStringMapper' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract. +CannotSealType : Type 'AutoMapper.Internal.Mappers.UnderlyingTypeEnumMapper' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract. +CannotRemoveAttribute : Attribute 'System.Runtime.CompilerServices.NullableContextAttribute' exists on 'AutoMapper.QueryableExtensions.Extensions.ProjectTo(System.Linq.IQueryable, System.Type, AutoMapper.IConfigurationProvider)' in the contract but not the implementation. +CannotRemoveAttribute : Attribute 'System.Runtime.CompilerServices.NullableContextAttribute' exists on 'AutoMapper.QueryableExtensions.Extensions.ProjectTo(System.Linq.IQueryable, System.Type, AutoMapper.IConfigurationProvider, System.Collections.Generic.IDictionary, System.String[])' in the contract but not the implementation. +CannotRemoveAttribute : Attribute 'System.Runtime.CompilerServices.NullableAttribute' exists on parameter 'parameters' on member 'AutoMapper.QueryableExtensions.Extensions.ProjectTo(System.Linq.IQueryable, System.Type, AutoMapper.IConfigurationProvider, System.Collections.Generic.IDictionary, System.String[])' in the contract but not the implementation. +CannotRemoveAttribute : Attribute 'System.Runtime.CompilerServices.NullableContextAttribute' exists on 'AutoMapper.QueryableExtensions.Extensions.ProjectTo(System.Linq.IQueryable, AutoMapper.IConfigurationProvider, System.Collections.Generic.IDictionary, System.String[])' in the contract but not the implementation. +CannotRemoveAttribute : Attribute 'System.Runtime.CompilerServices.NullableAttribute' exists on parameter 'parameters' on member 'AutoMapper.QueryableExtensions.Extensions.ProjectTo(System.Linq.IQueryable, AutoMapper.IConfigurationProvider, System.Collections.Generic.IDictionary, System.String[])' in the contract but not the implementation. +CannotRemoveAttribute : Attribute 'System.Runtime.CompilerServices.NullableAttribute' exists on generic param 'TDestination' on member 'AutoMapper.QueryableExtensions.Extensions.ProjectTo(System.Linq.IQueryable, AutoMapper.IConfigurationProvider, System.Collections.Generic.IDictionary, System.String[])' in the contract but not the implementation. +CannotRemoveAttribute : Attribute 'System.Runtime.CompilerServices.NullableContextAttribute' exists on 'AutoMapper.QueryableExtensions.Extensions.ProjectTo(System.Linq.IQueryable, AutoMapper.IConfigurationProvider, System.Linq.Expressions.Expression>[])' in the contract but not the implementation. +CannotRemoveAttribute : Attribute 'System.Runtime.CompilerServices.NullableAttribute' exists on generic param 'TDestination' on member 'AutoMapper.QueryableExtensions.Extensions.ProjectTo(System.Linq.IQueryable, AutoMapper.IConfigurationProvider, System.Linq.Expressions.Expression>[])' in the contract but not the implementation. +CannotRemoveAttribute : Attribute 'System.Runtime.CompilerServices.NullableContextAttribute' exists on 'AutoMapper.QueryableExtensions.Extensions.ProjectTo(System.Linq.IQueryable, AutoMapper.IConfigurationProvider, System.Object, System.Linq.Expressions.Expression>[])' in the contract but not the implementation. +CannotRemoveAttribute : Attribute 'System.Runtime.CompilerServices.NullableAttribute' exists on parameter 'parameters' on member 'AutoMapper.QueryableExtensions.Extensions.ProjectTo(System.Linq.IQueryable, AutoMapper.IConfigurationProvider, System.Object, System.Linq.Expressions.Expression>[])' in the contract but not the implementation. +CannotRemoveAttribute : Attribute 'System.Runtime.CompilerServices.NullableAttribute' exists on generic param 'TDestination' on member 'AutoMapper.QueryableExtensions.Extensions.ProjectTo(System.Linq.IQueryable, AutoMapper.IConfigurationProvider, System.Object, System.Linq.Expressions.Expression>[])' in the contract but not the implementation. +Total Issues: 71 diff --git a/src/AutoMapper/AssemblyInfo.cs b/src/AutoMapper/AssemblyInfo.cs index 922c8e4e0c..87fb95d420 100644 --- a/src/AutoMapper/AssemblyInfo.cs +++ b/src/AutoMapper/AssemblyInfo.cs @@ -1,8 +1,6 @@ using System.Resources; using System.Runtime.InteropServices; -[assembly: CLSCompliant(true)] -[assembly: ComVisible(false)] [assembly: NeutralResourcesLanguage("en")] namespace System.Runtime.CompilerServices; diff --git a/src/AutoMapper/AutoMapper.csproj b/src/AutoMapper/AutoMapper.csproj index 41db321e10..4474733c0f 100644 --- a/src/AutoMapper/AutoMapper.csproj +++ b/src/AutoMapper/AutoMapper.csproj @@ -3,55 +3,72 @@ A convention-based object-object mapper. A convention-based object-object mapper. - netstandard2.1 + netstandard2.0;net8.0;net9.0;net10.0 + 14.0 true AutoMapper ..\..\AutoMapper.snk true - true AutoMapper icon.png - https://automapper.org + https://automapper.io README.md + True + LICENSE.md - preview + preview.0 v - MIT true + true snupkg true true true - true --exclude-non-browsable --exclude-compiler-generated + + $(TargetFrameworks);net471 + + - + - - + + + - - - - + + + + + + + + + - + + + + + + + + + - - - - + + \ No newline at end of file diff --git a/src/AutoMapper/AutoMapperMappingException.cs b/src/AutoMapper/AutoMapperMappingException.cs index 2be71cd072..02f5dbca3c 100644 --- a/src/AutoMapper/AutoMapperMappingException.cs +++ b/src/AutoMapper/AutoMapperMappingException.cs @@ -67,45 +67,29 @@ public override string StackTrace { return string.Join(Environment.NewLine, base.StackTrace - .Split(new[] {Environment.NewLine}, StringSplitOptions.None) + .Split([Environment.NewLine], StringSplitOptions.None) .Where(str => !str.TrimStart().StartsWith("at AutoMapper."))); } } #endif } -public class DuplicateTypeMapConfigurationException : Exception +public class DuplicateTypeMapConfigurationException(DuplicateTypeMapConfigurationException.TypeMapConfigErrors[] errors) : Exception { - public TypeMapConfigErrors[] Errors { get; } - - public DuplicateTypeMapConfigurationException(TypeMapConfigErrors[] errors) + public TypeMapConfigErrors[] Errors { get; } = errors; + public override string Message { get; } = GetErrors(errors); + static string GetErrors(TypeMapConfigErrors[] errors) { - Errors = errors; - var builder = new StringBuilder(); - builder.AppendLine("The following type maps were found in multiple profiles:"); - foreach (var error in Errors) + StringBuilder builder = new(); + builder.AppendLine("Duplicate CreateMap calls:"); + foreach(var error in errors) { builder.AppendLine($"{error.Types.SourceType.FullName} to {error.Types.DestinationType.FullName} defined in profiles:"); builder.AppendLine(string.Join(Environment.NewLine, error.ProfileNames)); } - builder.AppendLine("This can cause configuration collisions and inconsistent mapping."); - builder.AppendLine("Consolidate the CreateMap calls into one profile, or set the root Internal().AllowAdditiveTypeMapCreation configuration value to 'true'."); - - Message = builder.ToString(); - } - - public class TypeMapConfigErrors - { - public string[] ProfileNames { get; } - public TypePair Types { get; } - - public TypeMapConfigErrors(TypePair types, string[] profileNames) - { - Types = types; - ProfileNames = profileNames; - } + builder.AppendLine("This can cause configuration collisions and inconsistent mappings. Use a single CreateMap call per type pair."); + return builder.ToString(); } - - public override string Message { get; } + public readonly record struct TypeMapConfigErrors(TypePair Types, string[] ProfileNames); } public class AutoMapperConfigurationException : Exception { @@ -113,19 +97,7 @@ public class AutoMapperConfigurationException : Exception public TypePair? Types { get; } public MemberMap MemberMap { get; set; } - public class TypeMapConfigErrors - { - public TypeMap TypeMap { get; } - public string[] UnmappedPropertyNames { get; } - public bool CanConstruct { get; } - - public TypeMapConfigErrors(TypeMap typeMap, string[] unmappedPropertyNames, bool canConstruct) - { - TypeMap = typeMap; - UnmappedPropertyNames = unmappedPropertyNames; - CanConstruct = canConstruct; - } - } + public readonly record struct TypeMapConfigErrors(TypeMap TypeMap, string[] UnmappedPropertyNames, bool CanConstruct); public AutoMapperConfigurationException(string message) : base(message) @@ -172,8 +144,7 @@ public override string Message } if (Errors != null) { - var message = - new StringBuilder( + StringBuilder message = new( "\nUnmapped members were found. Review the types and members below.\nAdd a custom mapping expression, ignore, add a custom resolver, or modify the source/destination type\nFor no matching constructor, add a no-arg ctor, add optional arguments, or map all of the constructor parameters\n"); foreach (var error in Errors) @@ -181,7 +152,7 @@ public override string Message var len = error.TypeMap.SourceType.FullName.Length + error.TypeMap.DestinationType.FullName.Length + 5; - message.AppendLine(new string('=', len)); + message.AppendLine(new('=', len)); message.AppendLine(error.TypeMap.SourceType.Name + " -> " + error.TypeMap.DestinationType.Name + " (" + error.TypeMap.ConfiguredMemberList + " member list)"); @@ -216,7 +187,7 @@ public override string StackTrace if (Errors != null) return string.Join(Environment.NewLine, base.StackTrace - .Split(new[] { Environment.NewLine }, StringSplitOptions.None) + .Split([Environment.NewLine], StringSplitOptions.None) .Where(str => !str.TrimStart().StartsWith("at AutoMapper.")) .ToArray()); diff --git a/src/AutoMapper/Configuration/Annotations/AutoMapAttribute.cs b/src/AutoMapper/Configuration/Annotations/AutoMapAttribute.cs index 903867d05f..c73db236f4 100644 --- a/src/AutoMapper/Configuration/Annotations/AutoMapAttribute.cs +++ b/src/AutoMapper/Configuration/Annotations/AutoMapAttribute.cs @@ -5,12 +5,9 @@ /// Discovered during scanning assembly scanning for configuration when calling /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Struct, AllowMultiple = true)] -public sealed class AutoMapAttribute : Attribute +public sealed class AutoMapAttribute(Type sourceType) : Attribute { - public AutoMapAttribute(Type sourceType) - => SourceType = sourceType; - - public Type SourceType { get; } + public Type SourceType { get; } = sourceType; public bool ReverseMap { get; set; } /// diff --git a/src/AutoMapper/Configuration/IMemberConfigurationProvider.cs b/src/AutoMapper/Configuration/Annotations/IMemberConfigurationProvider.cs similarity index 100% rename from src/AutoMapper/Configuration/IMemberConfigurationProvider.cs rename to src/AutoMapper/Configuration/Annotations/IMemberConfigurationProvider.cs diff --git a/src/AutoMapper/Configuration/Annotations/MappingOrderAttribute.cs b/src/AutoMapper/Configuration/Annotations/MappingOrderAttribute.cs index d028b1e906..2dbb536762 100644 --- a/src/AutoMapper/Configuration/Annotations/MappingOrderAttribute.cs +++ b/src/AutoMapper/Configuration/Annotations/MappingOrderAttribute.cs @@ -7,14 +7,9 @@ /// Must be used in combination with /// [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] -public sealed class MappingOrderAttribute : Attribute, IMemberConfigurationProvider +public sealed class MappingOrderAttribute(int value) : Attribute, IMemberConfigurationProvider { - public int Value { get; } - - public MappingOrderAttribute(int value) - { - Value = value; - } + public int Value { get; } = value; public void ApplyConfiguration(IMemberConfigurationExpression memberConfigurationExpression) { diff --git a/src/AutoMapper/Configuration/Annotations/NullSubstituteAttribute.cs b/src/AutoMapper/Configuration/Annotations/NullSubstituteAttribute.cs index 9de69f389c..0a30f3949e 100644 --- a/src/AutoMapper/Configuration/Annotations/NullSubstituteAttribute.cs +++ b/src/AutoMapper/Configuration/Annotations/NullSubstituteAttribute.cs @@ -7,17 +7,12 @@ /// Must be used in combination with /// [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] -public sealed class NullSubstituteAttribute : Attribute, IMemberConfigurationProvider +public sealed class NullSubstituteAttribute(object value) : Attribute, IMemberConfigurationProvider { /// /// Value to use if source value is null /// - public object Value { get; } - - public NullSubstituteAttribute(object value) - { - Value = value; - } + public object Value { get; } = value; public void ApplyConfiguration(IMemberConfigurationExpression memberConfigurationExpression) { diff --git a/src/AutoMapper/Configuration/Annotations/SourceMemberAttribute.cs b/src/AutoMapper/Configuration/Annotations/SourceMemberAttribute.cs index a73ac3756a..daca85749f 100644 --- a/src/AutoMapper/Configuration/Annotations/SourceMemberAttribute.cs +++ b/src/AutoMapper/Configuration/Annotations/SourceMemberAttribute.cs @@ -7,11 +7,9 @@ /// Must be used in combination with /// [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] -public sealed class SourceMemberAttribute : Attribute, IMemberConfigurationProvider +public sealed class SourceMemberAttribute(string name) : Attribute, IMemberConfigurationProvider { - public string Name { get; } - - public SourceMemberAttribute(string name) => Name = name; + public string Name { get; } = name; public void ApplyConfiguration(IMemberConfigurationExpression memberConfigurationExpression) { diff --git a/src/AutoMapper/Configuration/Annotations/ValueConverterAttribute.cs b/src/AutoMapper/Configuration/Annotations/ValueConverterAttribute.cs index bf0a87e552..dda9d6230a 100644 --- a/src/AutoMapper/Configuration/Annotations/ValueConverterAttribute.cs +++ b/src/AutoMapper/Configuration/Annotations/ValueConverterAttribute.cs @@ -8,17 +8,12 @@ /// Must be used in combination with /// [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] -public sealed class ValueConverterAttribute : Attribute, IMemberConfigurationProvider +public sealed class ValueConverterAttribute(Type type) : Attribute, IMemberConfigurationProvider { /// /// type /// - public Type Type { get; } - - public ValueConverterAttribute(Type type) - { - Type = type; - } + public Type Type { get; } = type; public void ApplyConfiguration(IMemberConfigurationExpression memberConfigurationExpression) { diff --git a/src/AutoMapper/Configuration/Annotations/ValueResolverAttribute.cs b/src/AutoMapper/Configuration/Annotations/ValueResolverAttribute.cs index dea7196d35..8d6f9c911a 100644 --- a/src/AutoMapper/Configuration/Annotations/ValueResolverAttribute.cs +++ b/src/AutoMapper/Configuration/Annotations/ValueResolverAttribute.cs @@ -8,17 +8,12 @@ /// Must be used in combination with /// [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] -public sealed class ValueResolverAttribute : Attribute, IMemberConfigurationProvider +public sealed class ValueResolverAttribute(Type type) : Attribute, IMemberConfigurationProvider { /// /// or type /// - public Type Type { get; } - - public ValueResolverAttribute(Type type) - { - Type = type; - } + public Type Type { get; } = type; public void ApplyConfiguration(IMemberConfigurationExpression memberConfigurationExpression) { diff --git a/src/AutoMapper/Configuration/ConfigurationValidator.cs b/src/AutoMapper/Configuration/ConfigurationValidator.cs index ecc5090767..5e2647e44b 100644 --- a/src/AutoMapper/Configuration/ConfigurationValidator.cs +++ b/src/AutoMapper/Configuration/ConfigurationValidator.cs @@ -1,62 +1,42 @@ using AutoMapper.Internal.Mappers; namespace AutoMapper.Configuration; - [EditorBrowsable(EditorBrowsableState.Never)] -public readonly record struct ConfigurationValidator(IGlobalConfigurationExpression Expression) +public class ConfigurationValidator(IGlobalConfiguration config) { - private void Validate(ValidationContext context) + IGlobalConfigurationExpression Expression => ((MapperConfiguration)config).ConfigurationExpression; + public void AssertConfigurationExpressionIsValid(TypeMap[] typeMaps) { - foreach (var validator in Expression.Validators) + var duplicateTypeMapConfigs = Expression.Profiles.Append((Profile)Expression) + .SelectMany(p => p.TypeMapConfigs, (profile, typeMap) => (profile, typeMap)) + .GroupBy(x => x.typeMap.Types) + .Where(g => g.Count() > 1) + .Select(g => (TypePair: g.Key, ProfileNames: g.Select(tmc => tmc.profile.ProfileName).ToArray())) + .Select(g => new DuplicateTypeMapConfigurationException.TypeMapConfigErrors(g.TypePair, g.ProfileNames)) + .ToArray(); + if (duplicateTypeMapConfigs.Length != 0) { - validator(context); + throw new DuplicateTypeMapConfigurationException(duplicateTypeMapConfigs); } + AssertConfigurationIsValid(typeMaps); } - public void AssertConfigurationExpressionIsValid(IGlobalConfiguration config, IEnumerable typeMaps) + public void AssertConfigurationIsValid(TypeMap[] typeMaps) { - if (!Expression.AllowAdditiveTypeMapCreation) - { - var duplicateTypeMapConfigs = Expression.Profiles.Append((Profile)Expression) - .SelectMany(p => p.TypeMapConfigs, (profile, typeMap) => (profile, typeMap)) - .GroupBy(x => x.typeMap.Types) - .Where(g => g.Count() > 1) - .Select(g => (TypePair : g.Key, ProfileNames : g.Select(tmc => tmc.profile.ProfileName).ToArray())) - .Select(g => new DuplicateTypeMapConfigurationException.TypeMapConfigErrors(g.TypePair, g.ProfileNames)) - .ToArray(); - if (duplicateTypeMapConfigs.Any()) - { - throw new DuplicateTypeMapConfigurationException(duplicateTypeMapConfigs); - } - } - AssertConfigurationIsValid(config, typeMaps); - } - public void AssertConfigurationIsValid(IGlobalConfiguration config, IEnumerable typeMaps) - { - var maps = typeMaps as TypeMap[] ?? typeMaps.ToArray(); + List configExceptions = []; var badTypeMaps = - (from typeMap in maps - where typeMap.ShouldCheckForValid - let unmappedPropertyNames = typeMap.GetUnmappedPropertyNames() - let canConstruct = typeMap.PassesCtorValidation - where unmappedPropertyNames.Length > 0 || !canConstruct - select new AutoMapperConfigurationException.TypeMapConfigErrors(typeMap, unmappedPropertyNames, canConstruct) - ).ToArray(); - - if (badTypeMaps.Any()) + (from typeMap in typeMaps + where typeMap.ShouldCheckForValid + let unmappedPropertyNames = typeMap.GetUnmappedPropertyNames() + let canConstruct = typeMap.PassesCtorValidation + where unmappedPropertyNames.Length > 0 || !canConstruct + select new AutoMapperConfigurationException.TypeMapConfigErrors(typeMap, unmappedPropertyNames, canConstruct)).ToArray(); + if (badTypeMaps.Length > 0) { - throw new AutoMapperConfigurationException(badTypeMaps); + configExceptions.Add(new AutoMapperConfigurationException(badTypeMaps)); } - var typeMapsChecked = new HashSet(); - var configExceptions = new List(); - foreach (var typeMap in maps) + HashSet typeMapsChecked = []; + foreach (var typeMap in typeMaps) { - try - { - DryRunTypeMap(config, typeMapsChecked, typeMap.Types, typeMap, null); - } - catch (Exception e) - { - configExceptions.Add(e); - } + DryRunTypeMap(typeMap.Types, typeMap, null); } if (configExceptions.Count > 1) { @@ -66,63 +46,71 @@ where unmappedPropertyNames.Length > 0 || !canConstruct { throw configExceptions[0]; } - } - private void DryRunTypeMap(IGlobalConfiguration config, HashSet typeMapsChecked, TypePair types, TypeMap typeMap, MemberMap memberMap) - { - if(typeMap == null) + void DryRunTypeMap(TypePair types, TypeMap typeMap, MemberMap memberMap) { - if (types.ContainsGenericParameters) + if (typeMap == null) { - return; + if (types.ContainsGenericParameters) + { + return; + } + typeMap = config.ResolveTypeMap(types); } - typeMap = config.ResolveTypeMap(types.SourceType, types.DestinationType); - } - if (typeMap != null) - { - if (typeMapsChecked.Contains(typeMap)) + if (typeMap != null) { - return; + if (typeMapsChecked.Add(typeMap) && Validate(new(types, memberMap, configExceptions, typeMap)) && typeMap.ShouldCheckForValid) + { + CheckPropertyMaps(typeMap); + } } - typeMapsChecked.Add(typeMap); - var context = new ValidationContext(types, memberMap, typeMap); - Validate(context); - if(!typeMap.ShouldCheckForValid) + else { - return; + var mapperToUse = config.FindMapper(types); + if (mapperToUse == null) + { + configExceptions.Add(new AutoMapperConfigurationException(memberMap.TypeMap.Types) { MemberMap = memberMap }); + return; + } + if (Validate(new(types, memberMap, configExceptions, ObjectMapper: mapperToUse)) && mapperToUse.GetAssociatedTypes(types) is TypePair newTypes && + newTypes != types) + { + DryRunTypeMap(newTypes, null, memberMap); + } } - CheckPropertyMaps(config, typeMapsChecked, typeMap); } - else + void CheckPropertyMaps(TypeMap typeMap) { - var mapperToUse = config.FindMapper(types); - if (mapperToUse == null) - { - throw new AutoMapperConfigurationException(memberMap.TypeMap.Types) { MemberMap = memberMap }; - } - var context = new ValidationContext(types, memberMap, ObjectMapper: mapperToUse); - Validate(context); - if (mapperToUse.GetAssociatedTypes(types) is TypePair newTypes && newTypes != types) + foreach (var memberMap in typeMap.MemberMaps) { - DryRunTypeMap(config, typeMapsChecked, newTypes, null, memberMap); + if (memberMap.Ignored || (memberMap is PropertyMap && typeMap.ConstructorParameterMatches(memberMap.DestinationName))) + { + continue; + } + var sourceType = memberMap.SourceType; + // when we don't know what the source type is, bail + if (sourceType.IsGenericParameter || sourceType == typeof(object)) + { + continue; + } + DryRunTypeMap(new(sourceType, memberMap.DestinationType), null, memberMap); } } - } - private void CheckPropertyMaps(IGlobalConfiguration config, HashSet typeMapsChecked, TypeMap typeMap) - { - foreach (var memberMap in typeMap.MemberMaps) + bool Validate(ValidationContext context) { - if(memberMap.Ignored || (memberMap is PropertyMap && typeMap.ConstructorParameterMatches(memberMap.DestinationName))) + try { - continue; + foreach (var validator in Expression.Validators) + { + validator(context); + } } - var sourceType = memberMap.SourceType; - // when we don't know what the source type is, bail - if (sourceType.IsGenericParameter || sourceType == typeof(object)) + catch (Exception e) { - return; + configExceptions.Add(e); + return false; } - DryRunTypeMap(config, typeMapsChecked, new(sourceType, memberMap.DestinationType), null, memberMap); + return true; } } } -public readonly record struct ValidationContext(TypePair Types, MemberMap MemberMap, TypeMap TypeMap = null, IObjectMapper ObjectMapper = null); \ No newline at end of file +public readonly record struct ValidationContext(TypePair Types, MemberMap MemberMap, List Exceptions, TypeMap TypeMap = null, IObjectMapper ObjectMapper = null); \ No newline at end of file diff --git a/src/AutoMapper/Configuration/Conventions.cs b/src/AutoMapper/Configuration/Conventions.cs index 72c7734a15..08cb6f6684 100644 --- a/src/AutoMapper/Configuration/Conventions.cs +++ b/src/AutoMapper/Configuration/Conventions.cs @@ -5,12 +5,12 @@ public interface ISourceToDestinationNameMapper void Merge(ISourceToDestinationNameMapper other); } [EditorBrowsable(EditorBrowsableState.Never)] -public class MemberConfiguration +public sealed class MemberConfiguration { NameSplitMember _nameSplitMember; - public INamingConvention SourceNamingConvention { get; set; } = PascalCaseNamingConvention.Instance; - public INamingConvention DestinationNamingConvention { get; set; } = PascalCaseNamingConvention.Instance; - public List NameToMemberMappers { get; } = new(); + public INamingConvention SourceNamingConvention { get; set; } + public INamingConvention DestinationNamingConvention { get; set; } + public List NameToMemberMappers { get; } = []; public bool IsMatch(ProfileMap options, TypeDetails sourceTypeDetails, Type destType, Type destMemberType, string nameToSearch, List resolvers, bool isReverseMap) { var matchingMemberInfo = GetSourceMember(sourceTypeDetails, destType, destMemberType, nameToSearch); @@ -45,10 +45,6 @@ public void Seal() } public void Merge(MemberConfiguration other) { - if (other == null) - { - return; - } var initialCount = NameToMemberMappers.Count; for (int index = 0; index < other.NameToMemberMappers.Count; index++) { @@ -64,12 +60,14 @@ public void Merge(MemberConfiguration other) } NameToMemberMappers.Add(otherMapper); } + SourceNamingConvention ??= other.SourceNamingConvention; + DestinationNamingConvention ??= other.DestinationNamingConvention; } } -public class PrePostfixName : ISourceToDestinationNameMapper +public sealed class PrePostfixName : ISourceToDestinationNameMapper { - public List DestinationPrefixes { get; } = new(); - public List DestinationPostfixes { get; } = new(); + public List DestinationPrefixes { get; } = []; + public List DestinationPostfixes { get; } = []; public MemberInfo GetSourceMember(TypeDetails sourceTypeDetails, Type destType, Type destMemberType, string nameToSearch) { MemberInfo member; @@ -89,17 +87,17 @@ public void Merge(ISourceToDestinationNameMapper other) DestinationPostfixes.TryAdd(typedOther.DestinationPostfixes); } } -public class ReplaceName : ISourceToDestinationNameMapper +public sealed class ReplaceName : ISourceToDestinationNameMapper { - public List MemberNameReplacers { get; } = new(); + public List MemberNameReplacers { get; } = []; public MemberInfo GetSourceMember(TypeDetails sourceTypeDetails, Type destType, Type destMemberType, string nameToSearch) { var possibleSourceNames = PossibleNames(nameToSearch); - if (possibleSourceNames.Length == 0) + if (possibleSourceNames.Count == 0) { return null; } - var possibleDestNames = sourceTypeDetails.ReadAccessors.Select(mi => (mi, possibles : PossibleNames(mi.Name))).ToArray(); + var possibleDestNames = Array.ConvertAll(sourceTypeDetails.ReadAccessors, mi => (mi, possibles : PossibleNames(mi.Name))); foreach (var sourceName in possibleSourceNames) { foreach (var (mi, possibles) in possibleDestNames) @@ -112,15 +110,9 @@ public MemberInfo GetSourceMember(TypeDetails sourceTypeDetails, Type destType, } return null; } - public void Merge(ISourceToDestinationNameMapper other) - { - var typedOther = (ReplaceName)other; - MemberNameReplacers.TryAdd(typedOther.MemberNameReplacers); - } - private string[] PossibleNames(string nameToSearch) => - MemberNameReplacers.Select(r => nameToSearch.Replace(r.OriginalValue, r.NewValue)) - .Concat(new[] { MemberNameReplacers.Aggregate(nameToSearch, (s, r) => s.Replace(r.OriginalValue, r.NewValue)), nameToSearch }) - .ToArray(); + public void Merge(ISourceToDestinationNameMapper other) => MemberNameReplacers.TryAdd(((ReplaceName)other).MemberNameReplacers); + private List PossibleNames(string nameToSearch) => [..MemberNameReplacers.Select(r => nameToSearch.Replace(r.OriginalValue, r.NewValue)), + MemberNameReplacers.Aggregate(nameToSearch, (s, r) => s.Replace(r.OriginalValue, r.NewValue)), nameToSearch]; } [EditorBrowsable(EditorBrowsableState.Never)] public readonly record struct MemberNameReplacer(string OriginalValue, string NewValue); diff --git a/src/AutoMapper/Configuration/CtorParamConfigurationExpression.cs b/src/AutoMapper/Configuration/CtorParamConfigurationExpression.cs index 999e5c529f..0cca9fcf35 100644 --- a/src/AutoMapper/Configuration/CtorParamConfigurationExpression.cs +++ b/src/AutoMapper/Configuration/CtorParamConfigurationExpression.cs @@ -1,6 +1,4 @@ -using System.Runtime.CompilerServices; -namespace AutoMapper.Configuration; - +namespace AutoMapper.Configuration; public interface ICtorParamConfigurationExpression { /// @@ -11,7 +9,8 @@ public interface ICtorParamConfigurationExpression /// /// Ignore this member for LINQ projections unless explicitly expanded during projection /// - void ExplicitExpansion(); + /// Is explicitExpansion active + void ExplicitExpansion(bool value = true); } public interface ICtorParamConfigurationExpression : ICtorParamConfigurationExpression { @@ -35,36 +34,24 @@ public interface ICtorParameterConfiguration void Configure(TypeMap typeMap); } [EditorBrowsable(EditorBrowsableState.Never)] -public class CtorParamConfigurationExpression : ICtorParamConfigurationExpression, ICtorParameterConfiguration +public sealed class CtorParamConfigurationExpression(string ctorParamName, Type sourceType) : ICtorParamConfigurationExpression, ICtorParameterConfiguration { - public string CtorParamName { get; } - public Type SourceType { get; } - - private readonly List> _ctorParamActions = new List>(); - - public CtorParamConfigurationExpression(string ctorParamName, Type sourceType) - { - CtorParamName = ctorParamName; - SourceType = sourceType; - } - + public string CtorParamName { get; } = ctorParamName; + public Type SourceType { get; } = sourceType; + private readonly List> _ctorParamActions = []; public void MapFrom(Expression> sourceMember) => _ctorParamActions.Add(cpm => cpm.MapFrom(sourceMember)); - public void MapFrom(Func resolver) { Expression> resolverExpression = (src, dest, destMember, ctxt) => resolver(src, ctxt); _ctorParamActions.Add(cpm => cpm.SetResolver(new FuncResolver(resolverExpression))); } - public void MapFrom(string sourceMembersPath) { var sourceMembers = ReflectionHelper.GetMemberPath(SourceType, sourceMembersPath); _ctorParamActions.Add(cpm => cpm.MapFrom(sourceMembersPath, sourceMembers)); } - - public void ExplicitExpansion() => _ctorParamActions.Add(cpm => cpm.ExplicitExpansion = true); - + public void ExplicitExpansion(bool value) => _ctorParamActions.Add(cpm => cpm.ExplicitExpansion = value); public void Configure(TypeMap typeMap) { var ctorMap = typeMap.ConstructorMap; diff --git a/src/AutoMapper/Configuration/IMappingOperationOptions.cs b/src/AutoMapper/Configuration/IMappingOperationOptions.cs index 8f45ef8c14..f4c1b81022 100644 --- a/src/AutoMapper/Configuration/IMappingOperationOptions.cs +++ b/src/AutoMapper/Configuration/IMappingOperationOptions.cs @@ -1,5 +1,4 @@ namespace AutoMapper; - using StringDictionary = Dictionary; /// /// Options for a single map operation @@ -7,24 +6,26 @@ public interface IMappingOperationOptions { Func ServiceCtor { get; } - /// /// Construct services using this callback. Use this for child/nested containers /// /// void ConstructServicesUsing(Func constructor); - /// - /// Add context items to be accessed at map time inside an or + /// Add state to be accessed at map time inside an or . + /// Mutually exclusive with per Map call. /// - Dictionary Items { get; } - + object State { get; set; } + /// + /// Add context items to be accessed at map time inside an or . + /// Mutually exclusive with per Map call. + /// + StringDictionary Items { get; } /// /// Execute a custom function to the source and/or destination types before member mapping /// /// Callback for the source/destination types void BeforeMap(Action beforeFunction); - /// /// Execute a custom function to the source and/or destination types after member mapping /// @@ -38,21 +39,19 @@ public interface IMappingOperationOptions : IMappingOpera /// /// Callback for the source/destination types void BeforeMap(Action beforeFunction); - /// /// Execute a custom function to the source and/or destination types after member mapping /// /// Callback for the source/destination types void AfterMap(Action afterFunction); } -public class MappingOperationOptions : IMappingOperationOptions +public sealed class MappingOperationOptions(Func serviceCtor) : IMappingOperationOptions { - private StringDictionary _items; - public MappingOperationOptions(Func serviceCtor) => ServiceCtor = serviceCtor; - public Func ServiceCtor { get; private set; } - public Dictionary Items => _items ??= new StringDictionary(); - public Action BeforeMapAction { get; protected set; } - public Action AfterMapAction { get; protected set; } + public Func ServiceCtor { get; private set; } = serviceCtor; + public StringDictionary Items => (StringDictionary) (State ??= new StringDictionary()); + public object State { get; set; } + public Action BeforeMapAction { get; private set; } + public Action AfterMapAction { get; private set; } public void BeforeMap(Action beforeFunction) => BeforeMapAction = beforeFunction; public void AfterMap(Action afterFunction) => AfterMapAction = afterFunction; public void ConstructServicesUsing(Func constructor) diff --git a/src/AutoMapper/Configuration/IMemberConfigurationExpression.cs b/src/AutoMapper/Configuration/IMemberConfigurationExpression.cs index 5d52a81442..4f5f685249 100644 --- a/src/AutoMapper/Configuration/IMemberConfigurationExpression.cs +++ b/src/AutoMapper/Configuration/IMemberConfigurationExpression.cs @@ -290,7 +290,8 @@ public interface IProjectionMemberConfiguration /// /// Ignore this member for LINQ projections unless explicitly expanded during projection /// - void ExplicitExpansion(); + /// Is explicitExpansion active + void ExplicitExpansion(bool value = true); /// /// Apply a transformation function after any resolved destination member value with the given type /// diff --git a/src/AutoMapper/Configuration/INamingConvention.cs b/src/AutoMapper/Configuration/INamingConvention.cs index 4059b9a12a..7aa8809b18 100644 --- a/src/AutoMapper/Configuration/INamingConvention.cs +++ b/src/AutoMapper/Configuration/INamingConvention.cs @@ -11,7 +11,7 @@ public interface INamingConvention public sealed class ExactMatchNamingConvention : INamingConvention { public static readonly ExactMatchNamingConvention Instance = new(); - public string[] Split(string _) => Array.Empty(); + public string[] Split(string _) => []; public string SeparatorCharacter => null; } public sealed class PascalCaseNamingConvention : INamingConvention @@ -26,22 +26,22 @@ public string[] Split(string input) { if (char.IsUpper(input[index])) { - result ??= new(); + result ??= []; result.Add(input[lower..index]); lower = index; } } if (result == null) { - return Array.Empty(); + return []; } result.Add(input[lower..]); - return result.ToArray(); + return [..result]; } } public sealed class LowerUnderscoreNamingConvention : INamingConvention { public static readonly LowerUnderscoreNamingConvention Instance = new(); public string SeparatorCharacter => "_"; - public string[] Split(string input) => input.Split('_', StringSplitOptions.RemoveEmptyEntries); + public string[] Split(string input) => input.Split(['_'], StringSplitOptions.RemoveEmptyEntries); } \ No newline at end of file diff --git a/src/AutoMapper/Configuration/MapperConfiguration.cs b/src/AutoMapper/Configuration/MapperConfiguration.cs index 69a6c37eea..4ee5a7b06d 100644 --- a/src/AutoMapper/Configuration/MapperConfiguration.cs +++ b/src/AutoMapper/Configuration/MapperConfiguration.cs @@ -1,3 +1,7 @@ +using AutoMapper.Licensing; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + namespace AutoMapper; using Features; using Internal.Mappers; @@ -34,7 +38,7 @@ public interface IConfigurationProvider /// void CompileMappings(); } -public class MapperConfiguration : IGlobalConfiguration +public sealed class MapperConfiguration : IGlobalConfiguration { private static readonly MethodInfo MappingError = typeof(MapperConfiguration).GetMethod(nameof(GetMappingError)); private readonly IObjectMapper[] _mappers; @@ -43,29 +47,33 @@ public class MapperConfiguration : IGlobalConfiguration private readonly LockingConcurrentDictionary _runtimeMaps; private LazyValue _projectionBuilder; private readonly LockingConcurrentDictionary _executionPlans; - private readonly ConfigurationValidator _validator; + private readonly MapperConfigurationExpression _configurationExpression; + private readonly ILoggerFactory _loggerFactory; private readonly Features _features = new(); private readonly bool _hasOpenMaps; - private readonly HashSet _typeMapsPath = new(); - private readonly List _sourceMembers = new(); - private readonly List _variables = new(); - private readonly ParameterExpression[] _parameters = new[] { null, null, ContextParameter }; - private readonly CatchBlock[] _catches = new CatchBlock[1]; - private readonly List _expressions = new(); + private readonly HashSet _typeMapsPath = []; + private readonly List _sourceMembers = []; + private readonly List _variables = []; + private readonly ParameterExpression[] _parameters = [null, null, ContextParameter]; + private readonly CatchBlock[] _catches = [null]; + private readonly List _expressions = []; private readonly Dictionary _defaults; private readonly ParameterReplaceVisitor _parameterReplaceVisitor = new(); private readonly ConvertParameterReplaceVisitor _convertParameterReplaceVisitor = new(); - private readonly List _typesInheritance = new(); - public MapperConfiguration(MapperConfigurationExpression configurationExpression) + private readonly List _typesInheritance = []; + private readonly LicenseAccessor _licenseAccessor; + + public MapperConfiguration(MapperConfigurationExpression configurationExpression, ILoggerFactory loggerFactory) { + _configurationExpression = configurationExpression; + _loggerFactory = loggerFactory; var configuration = (IGlobalConfigurationExpression)configurationExpression; if (configuration.MethodMappingEnabled != false) { configuration.IncludeSourceExtensionMethods(typeof(Enumerable)); } - _mappers = configuration.Mappers.ToArray(); + _mappers = [..configuration.Mappers]; _executionPlans = new(CompileExecutionPlan); - _validator = new(configuration); _projectionBuilder = new(CreateProjectionBuilder); Configuration = new((IProfileConfiguration)configuration); int typeMapsCount = Configuration.TypeMapsCount; @@ -75,7 +83,7 @@ public MapperConfiguration(MapperConfigurationExpression configurationExpression int index = 1; foreach (var profile in configuration.Profiles) { - var profileMap = new ProfileMap(profile, configuration); + ProfileMap profileMap = new(profile, configuration); Profiles[index++] = profileMap; typeMapsCount += profileMap.TypeMapsCount; openTypeMapsCount += profileMap.OpenTypeMapsCount; @@ -85,6 +93,7 @@ public MapperConfiguration(MapperConfigurationExpression configurationExpression _hasOpenMaps = openTypeMapsCount > 0; _resolvedMaps = new(2 * typeMapsCount); configuration.Features.Configure(this); + _licenseAccessor = new LicenseAccessor(this, _loggerFactory); Seal(); @@ -105,6 +114,10 @@ public MapperConfiguration(MapperConfigurationExpression configurationExpression _parameterReplaceVisitor = null; _typesInheritance = null; _runtimeMaps = new(GetTypeMap, openTypeMapsCount); + + var validator = new LicenseValidator(loggerFactory); + validator.Validate(_licenseAccessor.Current); + return; void Seal() { @@ -117,7 +130,7 @@ void Seal() profile.Configure(this); } IGlobalConfiguration globalConfiguration = this; - var derivedMaps = new List(); + List derivedMaps = []; foreach (var typeMap in _configuredMaps.Values) { _resolvedMaps[typeMap.Types] = typeMap; @@ -125,8 +138,7 @@ void Seal() GetDerivedTypeMaps(typeMap, derivedMaps); foreach (var derivedMap in derivedMaps) { - var includedPair = new TypePair(derivedMap.SourceType, typeMap.DestinationType); - _resolvedMaps.TryAdd(includedPair, derivedMap); + _resolvedMaps.TryAdd(new(derivedMap.SourceType, typeMap.DestinationType), derivedMap); } } foreach (var typeMap in _configuredMaps.Values) @@ -149,28 +161,27 @@ Delegate CompileExecutionPlan(MapRequest mapRequest) return executionPlan.Compile(); // breakpoint here to inspect all execution plans } } - public MapperConfiguration(Action configure) : this(Build(configure)){} + // For unit testing purposes only + internal MapperConfiguration(Action configure) : this(configure, new NullLoggerFactory()){} + public MapperConfiguration(Action configure, ILoggerFactory loggerFactory) : this(Build(configure), loggerFactory){} static MapperConfigurationExpression Build(Action configure) { MapperConfigurationExpression expr = new(); configure(expr); return expr; } - public void AssertConfigurationIsValid() => _validator.AssertConfigurationExpressionIsValid(this, _configuredMaps.Values); + public void AssertConfigurationIsValid() => Validator().AssertConfigurationExpressionIsValid([.._configuredMaps.Values]); + ConfigurationValidator Validator() => new(this); public IMapper CreateMapper() => new Mapper(this); public IMapper CreateMapper(Func serviceCtor) => new Mapper(this, serviceCtor); public void CompileMappings() { - foreach (var request in _resolvedMaps.Keys.Where(t => !t.ContainsGenericParameters).Select(types => new MapRequest(types, types, MemberMap.Instance)).ToArray()) + foreach (var request in _resolvedMaps.Keys.Where(t => !t.ContainsGenericParameters).Select(types => new MapRequest(types)).ToArray()) { GetExecutionPlan(request); } } - public LambdaExpression BuildExecutionPlan(Type sourceType, Type destinationType) - { - var typePair = new TypePair(sourceType, destinationType); - return this.Internal().BuildExecutionPlan(new(typePair, typePair, MemberMap.Instance)); - } + public LambdaExpression BuildExecutionPlan(Type sourceType, Type destinationType) => this.Internal().BuildExecutionPlan(new(new(sourceType, destinationType))); LambdaExpression IGlobalConfiguration.BuildExecutionPlan(in MapRequest mapRequest) { var typeMap = ResolveTypeMap(mapRequest.RuntimeTypes) ?? ResolveTypeMap(mapRequest.RequestedTypes); @@ -204,7 +215,7 @@ LambdaExpression GenerateObjectMapperExpression(in MapRequest mapRequest, IObjec Expression fullExpression; if (mapper == null) { - var exception = new AutoMapperMappingException("Missing type map configuration or unsupported mapping.", null, mapRequest.RuntimeTypes) + AutoMapperMappingException exception = new("Missing type map configuration or unsupported mapping.", null, mapRequest.RuntimeTypes) { MemberMap = mapRequest.MemberMap }; @@ -223,8 +234,8 @@ LambdaExpression GenerateObjectMapperExpression(in MapRequest mapRequest, IObjec return Lambda(fullExpression, source, destination, ContextParameter); } } - IGlobalConfigurationExpression ConfigurationExpression => _validator.Expression; - ProjectionBuilder CreateProjectionBuilder() => new(this, ConfigurationExpression.ProjectionMappers.ToArray()); + internal IGlobalConfigurationExpression ConfigurationExpression => _configurationExpression; + ProjectionBuilder CreateProjectionBuilder() => new(this, [..ConfigurationExpression.ProjectionMappers]); IProjectionBuilder IGlobalConfiguration.ProjectionBuilder => _projectionBuilder.Value; Func IGlobalConfiguration.ServiceCtor => ConfigurationExpression.ServiceCtor; bool IGlobalConfiguration.EnableNullPropagationForQueryMapping => ConfigurationExpression.EnableNullPropagationForQueryMapping.GetValueOrDefault(); @@ -320,7 +331,7 @@ private TypeMap GetTypeMap(TypePair initialTypes) List typesInheritance; if (_typesInheritance == null) { - typesInheritance = new(); + typesInheritance = []; } else { @@ -340,9 +351,13 @@ private TypeMap GetTypeMap(TypePair initialTypes) { continue; } - var types = new TypePair(sourceType, destinationType); + TypePair types = new(sourceType, destinationType); if (_resolvedMaps.TryGetValue(types, out typeMap)) { + if(typeMap == null) + { + continue; + } return typeMap; } typeMap = FindClosedGenericTypeMapFor(types); @@ -428,7 +443,7 @@ TypeMap[] IGlobalConfiguration.GetIncludedTypeMaps(IReadOnlyCollection { if (includedTypes.Count == 0) { - return Array.Empty(); + return []; } var typeMaps = new TypeMap[includedTypes.Count]; int index = 0; @@ -472,23 +487,24 @@ IObjectMapper FindMapper(TypePair types) return null; } void IGlobalConfiguration.RegisterTypeMap(TypeMap typeMap) => _configuredMaps[typeMap.Types] = typeMap; - void IGlobalConfiguration.AssertConfigurationIsValid(TypeMap typeMap) => _validator.AssertConfigurationIsValid(this, new[] { typeMap }); + void IGlobalConfiguration.AssertConfigurationIsValid(TypeMap typeMap) => Validator().AssertConfigurationIsValid([typeMap]); void IGlobalConfiguration.AssertConfigurationIsValid(string profileName) { if (Array.TrueForAll(Profiles, x => x.Name != profileName)) { throw new ArgumentOutOfRangeException(nameof(profileName), $"Cannot find any profiles with the name '{profileName}'."); } - _validator.AssertConfigurationIsValid(this, _configuredMaps.Values.Where(typeMap => typeMap.Profile.Name == profileName)); + Validator().AssertConfigurationIsValid(_configuredMaps.Values.Where(typeMap => typeMap.Profile.Name == profileName).ToArray()); } void IGlobalConfiguration.AssertConfigurationIsValid() => this.Internal().AssertConfigurationIsValid(typeof(TProfile).FullName); void IGlobalConfiguration.RegisterAsMap(TypeMapConfiguration typeMapConfiguration) => _resolvedMaps[typeMapConfiguration.Types] = GetIncludedTypeMap(new(typeMapConfiguration.SourceType, typeMapConfiguration.DestinationTypeOverride)); + + string IGlobalConfiguration.LicenseKey => _configurationExpression.LicenseKey; } -struct LazyValue where T : class +struct LazyValue(Func factory) where T : class { - readonly Func _factory; + readonly Func _factory = factory; T _value = null; public T Value => LazyInitializer.EnsureInitialized(ref _value, _factory); - public LazyValue(Func factory) => _factory = factory; } \ No newline at end of file diff --git a/src/AutoMapper/Configuration/MapperConfigurationExpression.cs b/src/AutoMapper/Configuration/MapperConfigurationExpression.cs index 5c36306a8d..c4d4c51461 100644 --- a/src/AutoMapper/Configuration/MapperConfigurationExpression.cs +++ b/src/AutoMapper/Configuration/MapperConfigurationExpression.cs @@ -1,6 +1,11 @@ using AutoMapper.Features; using AutoMapper.Internal.Mappers; using AutoMapper.QueryableExtensions.Impl; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + namespace AutoMapper; using Validator = Action; @@ -84,30 +89,27 @@ public interface IMapperConfigurationExpression : IProfileExpression /// Profile name, must be unique /// Profile configuration void CreateProfile(string profileName, Action config); + + /// + /// Gets or sets the license key. You can find your license key in your account. + /// + string LicenseKey { get; set; } } -public class MapperConfigurationExpression : Profile, IGlobalConfigurationExpression +public sealed class MapperConfigurationExpression : Profile, IGlobalConfigurationExpression { - private readonly List _profiles = new(); - private readonly List _validators = new(); - private readonly List _mappers; + static readonly Type[] AmTypes = [typeof(IValueResolver<,,>), typeof(IMemberValueResolver<,,,>), typeof(ITypeConverter<,>), typeof(IValueConverter<,>), typeof(IMappingAction<,>)]; + + private readonly List _profiles = []; + private readonly List _validators = []; private Func _serviceCtor = Activator.CreateInstance; private List _projectionMappers; - - public MapperConfigurationExpression() : base() => _mappers = MapperRegistry.Mappers(); - + private List _scannedAssembles = []; /// /// Add an action to be called when validating the configuration. /// /// the validation callback void IGlobalConfigurationExpression.Validator(Validator validator) => - _validators.Add(validator ?? throw new ArgumentNullException(nameof(validator))); - - /// - /// Allow the same map to exist in different profiles. - /// The default is to throw an exception, true means the maps are merged. - /// - bool IGlobalConfigurationExpression.AllowAdditiveTypeMapCreation { get; set; } - + _validators.Add(validator ?? throw new System.ArgumentNullException(nameof(validator))); /// /// How many levels deep should AutoMapper try to inline the execution plan for child classes. /// See the docs for details. @@ -123,6 +125,10 @@ void IGlobalConfigurationExpression.Validator(Validator validator) => /// Must be zero for EF6. Can be greater than zero for EF Core. /// int IGlobalConfigurationExpression.RecursiveQueriesMaxDepth { get; set; } + + public string LicenseKey { get; set; } + + public ServiceLifetime ServiceLifetime { get; set; } = ServiceLifetime.Transient; IReadOnlyCollection IGlobalConfigurationExpression.Profiles => _profiles; Func IGlobalConfigurationExpression.ServiceCtor => _serviceCtor; @@ -130,9 +136,9 @@ void IGlobalConfigurationExpression.Validator(Validator validator) => public void CreateProfile(string profileName, Action config) => AddProfile(new Profile(profileName, config)); - List IGlobalConfigurationExpression.Mappers => _mappers; + List IGlobalConfigurationExpression.Mappers { get; } = MapperRegistry.Mappers(); - Features IGlobalConfigurationExpression.Features { get; } = new Features(); + Features IGlobalConfigurationExpression.Features { get; } = new(); public void AddProfile(Profile profile) => _profiles.Add(profile); @@ -168,7 +174,11 @@ public void AddMaps(params Type[] typesFromAssembliesContainingMappingDefinition private void AddMapsCore(IEnumerable assembliesToScan) { - var autoMapAttributeProfile = new Profile(nameof(AutoMapAttribute)); + assembliesToScan = assembliesToScan.ToList(); + + _scannedAssembles.AddRange(assembliesToScan); + + Profile autoMapAttributeProfile = new(nameof(AutoMapAttribute)); foreach (var type in assembliesToScan.Where(a => !a.IsDynamic && a != typeof(Profile).Assembly).SelectMany(a => a.GetTypes())) { if (typeof(Profile).IsAssignableFrom(type) && !type.IsAbstract && !type.ContainsGenericParameters) @@ -184,7 +194,7 @@ private void AddMapsCore(IEnumerable assembliesToScan) { foreach (var memberConfigurationProvider in memberInfo.GetCustomAttributes().OfType()) { - mappingExpression.ForMember(memberInfo, cfg => memberConfigurationProvider.ApplyConfiguration(cfg)); + mappingExpression.ForMember(memberInfo, memberConfigurationProvider.ApplyConfiguration); } } @@ -195,5 +205,5 @@ private void AddMapsCore(IEnumerable assembliesToScan) AddProfile(autoMapAttributeProfile); } - public void ConstructServicesUsing(Func constructor) => _serviceCtor = constructor; + public void ConstructServicesUsing(Func constructor) => _serviceCtor = constructor ?? throw new System.ArgumentNullException(nameof(constructor)); } \ No newline at end of file diff --git a/src/AutoMapper/Configuration/MappingExpression.cs b/src/AutoMapper/Configuration/MappingExpression.cs index 6bb950e180..30676f4cf9 100644 --- a/src/AutoMapper/Configuration/MappingExpression.cs +++ b/src/AutoMapper/Configuration/MappingExpression.cs @@ -1,15 +1,11 @@ namespace AutoMapper.Configuration; -public class MappingExpression : MappingExpressionBase, IMappingExpression +public sealed class MappingExpression(MemberList memberList, TypePair types) : MappingExpressionBase(memberList, types), IMappingExpression { - public MappingExpression(TypePair types, MemberList memberList) : base(memberList, types){} - public string[] IncludedMembersNames { get; internal set; } = Array.Empty(); + public MappingExpression(TypeMap typeMap) : this(typeMap.ConfiguredMemberList, typeMap.Types) => Projection = typeMap.Projection; + public string[] IncludedMembersNames { get; internal set; } = []; public IMappingExpression ReverseMap() { - var reversedTypes = new TypePair(DestinationType, SourceType); - var reverseMap = new MappingExpression(reversedTypes, MemberList.None) - { - IsReverseMap = true - }; + MappingExpression reverseMap = new(MemberList.None, Types.Reverse()) { IsReverseMap = true }; ReverseMapCore(reverseMap); reverseMap.IncludeMembers(MapToSourceMembers().Select(m => m.DestinationMember.Name).ToArray()); foreach (var includedMemberName in IncludedMembersNames) @@ -47,7 +43,7 @@ public IMappingExpression ForMember(string name, Action ForMember(property, o=>o.Ignore()); internal MemberConfigurationExpression ForMember(MemberInfo destinationProperty, Action memberOptions) { - var expression = new MemberConfigurationExpression(destinationProperty, SourceType); + MemberConfigurationExpression expression = new(destinationProperty, SourceType); MemberConfigurations.Add(expression); memberOptions(expression); return expression; @@ -56,8 +52,8 @@ internal MemberConfigurationExpression ForMember(MemberInfo destinationProperty, public class MappingExpression : MappingExpressionBase>, IMappingExpression, IProjectionExpression { - public MappingExpression(MemberList memberList, bool projection = false) : base(memberList) => Projection = projection; - public MappingExpression(MemberList memberList, Type sourceType, Type destinationType) : base(memberList, sourceType, destinationType) { } + public MappingExpression(MemberList memberList, bool projection) : base(memberList) => Projection = projection; + public MappingExpression(MemberList memberList, TypePair types) : base(memberList, types) { } public IMappingExpression ForPath(Expression> destinationMember, Action> memberOptions) { @@ -65,7 +61,7 @@ public IMappingExpression ForPath(Expression(destinationMember, chain); + PathConfigurationExpression expression = new(destinationMember, chain); var firstMember = expression.MemberPath.First; var firstMemberConfig = GetDestinationMemberConfiguration(firstMember); if(firstMemberConfig == null) @@ -118,7 +114,7 @@ public IMappingExpression Include ForSourceMember(Expression> sourceMember, Action memberOptions) { var memberInfo = ReflectionHelper.FindProperty(sourceMember); - var srcConfig = new SourceMappingExpression(memberInfo); + SourceMappingExpression srcConfig = new(memberInfo); memberOptions(srcConfig); SourceMemberConfigurations.Add(srcConfig); return this; @@ -126,20 +122,20 @@ public IMappingExpression ForSourceMember(Expression() where T : TDestination => As(typeof(T)); public IMappingExpression AddTransform(Expression> transformer) { - var config = new ValueTransformerConfiguration(typeof(TValue), transformer); + ValueTransformerConfiguration config = new(typeof(TValue), transformer); ValueTransformers.Add(config); return this; } public IMappingExpression ReverseMap() { - var reverseMap = new MappingExpression(MemberList.None, DestinationType, SourceType){ IsReverseMap = true }; + MappingExpression reverseMap = new(MemberList.None, Types.Reverse()){ IsReverseMap = true }; ReverseMapCore(reverseMap); reverseMap.IncludeMembersCore(MapToSourceMembers().Select(m => m.GetDestinationExpression()).ToArray()); return reverseMap; } private IMappingExpression ForDestinationMember(MemberInfo destinationProperty, Action> memberOptions) { - var expression = new MemberConfigurationExpression(destinationProperty, SourceType); + MemberConfigurationExpression expression = new(destinationProperty, SourceType); MemberConfigurations.Add(expression); memberOptions(expression); return this; diff --git a/src/AutoMapper/Configuration/MemberConfigurationExpression.cs b/src/AutoMapper/Configuration/MemberConfigurationExpression.cs index d3edb3d1b0..74d7560712 100644 --- a/src/AutoMapper/Configuration/MemberConfigurationExpression.cs +++ b/src/AutoMapper/Configuration/MemberConfigurationExpression.cs @@ -1,25 +1,27 @@ namespace AutoMapper.Configuration; -public interface IPropertyMapConfiguration -{ - void Configure(TypeMap typeMap); - MemberInfo DestinationMember { get; } - LambdaExpression SourceExpression { get; } - LambdaExpression GetDestinationExpression(); - IPropertyMapConfiguration Reverse(); - bool Ignored => false; -} -public class MemberConfigurationExpression : IMemberConfigurationExpression, IPropertyMapConfiguration +public interface IPropertyMapConfiguration { - private MemberInfo[] _sourceMembers; - private readonly Type _sourceType; - protected List> PropertyMapActions { get; } = new List>(); - public MemberConfigurationExpression(MemberInfo destinationMember, Type sourceType) + void Configure(TypeMap typeMap); + MemberInfo DestinationMember { get; } + LambdaExpression SourceExpression { get; } + LambdaExpression GetDestinationExpression(); + IPropertyMapConfiguration Reverse(); + bool Ignored +#if NET8_0_OR_GREATER + => false; +#else { - DestinationMember = destinationMember; - _sourceType = sourceType; + get; } - public MemberInfo DestinationMember { get; } - public void MapAtRuntime() => PropertyMapActions.Add(pm => pm.Inline = false); +#endif +} +public class MemberConfigurationExpression(MemberInfo destinationMember, Type sourceType) : IMemberConfigurationExpression, IPropertyMapConfiguration +{ + private MemberInfo[] _sourceMembers; + private readonly Type _sourceType = sourceType; + protected List> PropertyMapActions { get; } = []; + public MemberInfo DestinationMember { get; } = destinationMember; + public void MapAtRuntime() => PropertyMapActions.Add(pm => pm.Inline = false); public void NullSubstitute(object nullSubstitute) => PropertyMapActions.Add(pm => pm.NullSubstitute = nullSubstitute); public void MapFrom() where TValueResolver : IValueResolver => MapFromCore(new(typeof(TValueResolver), typeof(IValueResolver))); @@ -31,9 +33,8 @@ public void MapFrom(Expression(string sourceMemberName) where TValueResolver : IMemberValueResolver => MapFromCore(null, sourceMemberName); private void MapFromCore(Expression> sourceMember, string sourceMemberName = null) where TValueResolver : IMemberValueResolver => - MapFromCore(new(typeof(TValueResolver), typeof(IMemberValueResolver)) + MapFromCore(new(typeof(TValueResolver), typeof(IMemberValueResolver), sourceMemberName) { - SourceMemberName = sourceMemberName, SourceMemberLambda = sourceMember }); public void MapFrom(IValueResolver valueResolver) => @@ -44,15 +45,15 @@ public void MapFrom(IMemberValueResolver(Func mappingFunction) => - MapFromResult((src, dest, destMember, ctxt) => mappingFunction(src, dest)); + MapFromFunc((src, dest, destMember, ctxt) => mappingFunction(src, dest)); public void MapFrom(Func mappingFunction) => - MapFromResult((src, dest, destMember, ctxt) => mappingFunction(src, dest, destMember)); + MapFromFunc((src, dest, destMember, ctxt) => mappingFunction(src, dest, destMember)); public void MapFrom(Func mappingFunction) => - MapFromResult((src, dest, destMember, ctxt) => mappingFunction(src, dest, destMember, ctxt)); - private void MapFromResult(Expression> expr) => + MapFromFunc((src, dest, destMember, ctxt) => mappingFunction(src, dest, destMember, ctxt)); + private void MapFromFunc(Expression> expr) => SetResolver(new FuncResolver(expr)); - public void MapFrom(Expression> mapExpression) => MapFromUntyped(mapExpression); - internal void MapFromUntyped(LambdaExpression sourceExpression) + public void MapFrom(Expression> mapExpression) => MapFromExpression(mapExpression); + internal void MapFromExpression(LambdaExpression sourceExpression) { SourceExpression = sourceExpression; PropertyMapActions.Add(pm => pm.MapFrom(sourceExpression)); @@ -72,26 +73,26 @@ public void Condition(Func condition) => public void Condition(Func condition) => ConditionCore((src, dest, srcMember, destMember, ctxt) => condition(src)); private void ConditionCore(Expression> expr) => PropertyMapActions.Add(pm => pm.Condition = expr); - public void PreCondition(Func condition) => PreConditionCore((src, dest, ctxt) => condition(src)); - public void PreCondition(Func condition) => PreConditionCore((src, dest, ctxt) => condition(ctxt)); - public void PreCondition(Func condition) => PreConditionCore((src, dest, ctxt) => condition(src, ctxt)); - public void PreCondition(Func condition) => PreConditionCore((src, dest, ctxt) => condition(src, dest, ctxt)); + public void PreCondition(Func condition) => PreConditionCore((src, dest, ctxt) => condition(src)); + public void PreCondition(Func condition) => PreConditionCore((src, dest, ctxt) => condition(ctxt)); + public void PreCondition(Func condition) => PreConditionCore((src, dest, ctxt) => condition(src, ctxt)); + public void PreCondition(Func condition) => PreConditionCore((src, dest, ctxt) => condition(src, dest, ctxt)); private void PreConditionCore(Expression> expr) => - PropertyMapActions.Add(pm => pm.PreCondition = expr); + PropertyMapActions.Add(pm => pm.PreCondition = expr); public void AddTransform(Expression> transformer) => PropertyMapActions.Add(pm => pm.AddValueTransformation(new ValueTransformerConfiguration(pm.DestinationType, transformer))); - public void ExplicitExpansion() => PropertyMapActions.Add(pm => pm.ExplicitExpansion = true); - public void Ignore() => Ignore(ignorePaths: true); + public void ExplicitExpansion(bool value) => PropertyMapActions.Add(pm => pm.ExplicitExpansion = value); + public void Ignore() => Ignore(ignorePaths: true); public void Ignore(bool ignorePaths) { Ignored = true; - PropertyMapActions.Add(pm => - { + PropertyMapActions.Add(pm => + { pm.Ignored = true; - if (ignorePaths && pm.TypeMap.PathMaps.Count > 0) - { - pm.TypeMap.IgnorePaths(DestinationMember); - } + if (ignorePaths && pm.TypeMap.PathMaps.Count > 0) + { + pm.TypeMap.IgnorePaths(DestinationMember); + } }); } public void AllowNull() => SetAllowNull(true); @@ -112,18 +113,16 @@ public void ConvertUsing(IValueConverter public void ConvertUsing(IValueConverter valueConverter, string sourceMemberName) => ConvertUsingCore(valueConverter, null, sourceMemberName); private void ConvertUsingCore(Expression> sourceMember = null, string sourceMemberName = null) => - ConvertUsingCore(new(typeof(TValueConverter), typeof(IValueConverter)) + ConvertUsingCore(new(typeof(TValueConverter), typeof(IValueConverter), sourceMemberName) { SourceMemberLambda = sourceMember, - SourceMemberName = sourceMemberName }); protected void ConvertUsingCore(ValueConverter converter) => SetResolver(converter); private void ConvertUsingCore(IValueConverter valueConverter, Expression> sourceMember = null, string sourceMemberName = null) => - ConvertUsingCore(new(valueConverter, typeof(IValueConverter)) + ConvertUsingCore(new(valueConverter, typeof(IValueConverter), sourceMemberName) { SourceMemberLambda = sourceMember, - SourceMemberName = sourceMemberName }); public void Configure(TypeMap typeMap) { @@ -132,16 +131,16 @@ public void Configure(TypeMap typeMap) { destMember = Array.Find(typeMap.DestinationTypeDetails.ReadAccessors, m => m.MetadataToken == destMember.MetadataToken); } - var propertyMap = typeMap.FindOrCreatePropertyMapFor(destMember, typeof(TMember) == typeof(object) ? destMember.GetMemberType() : typeof(TMember)); + var propertyMap = typeMap.FindOrCreatePropertyMapFor(destMember, typeof(TMember) == typeof(object) ? destMember.GetMemberType() : typeof(TMember)); Apply(propertyMap); - } - private void Apply(PropertyMap propertyMap) - { + } + private void Apply(PropertyMap propertyMap) + { foreach(var action in PropertyMapActions) { action(propertyMap); } - } + } public LambdaExpression SourceExpression { get; private set; } public bool Ignored { get; private set; } public LambdaExpression GetDestinationExpression() => DestinationMember.Lambda(); @@ -154,7 +153,7 @@ public IPropertyMapConfiguration Reverse() { return null; } - var reversedMemberConfiguration = new MemberConfigurationExpression(_sourceMembers[0], destinationType); + MemberConfigurationExpression reversedMemberConfiguration = new(_sourceMembers[0], destinationType); reversedMemberConfiguration.MapFrom(DestinationMember.Name); return reversedMemberConfiguration; } @@ -166,30 +165,17 @@ public IPropertyMapConfiguration Reverse() } public void DoNotUseDestinationValue() => SetUseDestinationValue(false); } -public class MemberConfigurationExpression : MemberConfigurationExpression, IMemberConfigurationExpression -{ - public MemberConfigurationExpression(MemberInfo destinationMember, Type sourceType) : base(destinationMember, sourceType){} - public void MapFrom(Type valueResolverType) => MapFromCore(new(valueResolverType, valueResolverType.GetGenericInterface(typeof(IValueResolver<,,>)))); +public sealed class MemberConfigurationExpression(MemberInfo destinationMember, Type sourceType) : MemberConfigurationExpression(destinationMember, sourceType), IMemberConfigurationExpression +{ + public void MapFrom(Type valueResolverType) => MapFromCore(new(valueResolverType, valueResolverType.GetGenericInterface(typeof(IValueResolver<,,>)))); public void MapFrom(Type valueResolverType, string sourceMemberName) => - MapFromCore(new(valueResolverType, valueResolverType.GetGenericInterface(typeof(IMemberValueResolver<,,,>))) - { - SourceMemberName = sourceMemberName - }); - public void MapFrom(IMemberValueResolver resolver, string sourceMemberName) => - MapFromCore(new(resolver, typeof(IMemberValueResolver)) - { - SourceMemberName = sourceMemberName - }); - public void ConvertUsing(Type valueConverterType) => ConvertUsingCore(valueConverterType); - public void ConvertUsing(Type valueConverterType, string sourceMemberName) => ConvertUsingCore(valueConverterType, sourceMemberName); + MapFromCore(new(valueResolverType, valueResolverType.GetGenericInterface(typeof(IMemberValueResolver<,,,>)), sourceMemberName)); + public void MapFrom(IMemberValueResolver resolver, string sourceMemberName) => + MapFromCore(new(resolver, typeof(IMemberValueResolver), sourceMemberName)); + public void ConvertUsing(Type valueConverterType) => ConvertUsingCore(valueConverterType); + public void ConvertUsing(Type valueConverterType, string sourceMemberName) => ConvertUsingCore(valueConverterType, sourceMemberName); public void ConvertUsing(IValueConverter valueConverter, string sourceMemberName) => - base.ConvertUsingCore(new(valueConverter, typeof(IValueConverter)) - { - SourceMemberName = sourceMemberName - }); + base.ConvertUsingCore(new(valueConverter, typeof(IValueConverter), sourceMemberName)); private void ConvertUsingCore(Type valueConverterType, string sourceMemberName = null) => - base.ConvertUsingCore(new(valueConverterType, valueConverterType.GetGenericInterface(typeof(IValueConverter<,>))) - { - SourceMemberName = sourceMemberName - }); + base.ConvertUsingCore(new(valueConverterType, valueConverterType.GetGenericInterface(typeof(IValueConverter<,>)), sourceMemberName)); } \ No newline at end of file diff --git a/src/AutoMapper/Configuration/PathConfigurationExpression.cs b/src/AutoMapper/Configuration/PathConfigurationExpression.cs index ecd2e46783..efdd49a9db 100644 --- a/src/AutoMapper/Configuration/PathConfigurationExpression.cs +++ b/src/AutoMapper/Configuration/PathConfigurationExpression.cs @@ -21,23 +21,18 @@ public interface IPathConfigurationExpression void Condition(Func, bool> condition); } public readonly record struct ConditionParameters(TSource Source, TDestination Destination, TMember SourceMember, TMember DestinationMember, ResolutionContext Context); -public class PathConfigurationExpression : IPathConfigurationExpression, IPropertyMapConfiguration +public sealed class PathConfigurationExpression(LambdaExpression destinationExpression, Stack chain) : IPathConfigurationExpression, IPropertyMapConfiguration { - private readonly LambdaExpression _destinationExpression; + private readonly LambdaExpression _destinationExpression = destinationExpression; private LambdaExpression _sourceExpression; - protected List> PathMapActions { get; } = new List>(); - public PathConfigurationExpression(LambdaExpression destinationExpression, Stack chain) - { - _destinationExpression = destinationExpression; - MemberPath = new MemberPath(chain); - } - public MemberPath MemberPath { get; } + List> PathMapActions { get; } = []; + public MemberPath MemberPath { get; } = new(chain); public MemberInfo DestinationMember => MemberPath.Last; public void MapFrom(Expression> sourceExpression) => MapFromUntyped(sourceExpression); public void Ignore() => PathMapActions.Add(pm => pm.Ignored = true); public void MapFromUntyped(LambdaExpression sourceExpression) { - _sourceExpression = sourceExpression ?? throw new ArgumentNullException(nameof(sourceExpression), $"{nameof(sourceExpression)} may not be null when mapping {DestinationMember.Name} from {typeof(TSource)} to {typeof(TDestination)}."); + _sourceExpression = sourceExpression ?? throw new System.ArgumentNullException(nameof(sourceExpression), $"{nameof(sourceExpression)} may not be null when mapping {DestinationMember.Name} from {typeof(TSource)} to {typeof(TDestination)}."); PathMapActions.Add(pm => pm.MapFrom(sourceExpression)); } public void Configure(TypeMap typeMap) @@ -58,11 +53,11 @@ internal static IPropertyMapConfiguration Create(LambdaExpression destination, L { return null; } - var reversed = new PathConfigurationExpression(destination, chain); + PathConfigurationExpression reversed = new(destination, chain); if (reversed.MemberPath.Length == 1) { - var reversedMemberExpression = new MemberConfigurationExpression(reversed.DestinationMember, typeof(TSource)); - reversedMemberExpression.MapFromUntyped(source); + MemberConfigurationExpression reversedMemberExpression = new(reversed.DestinationMember, typeof(TSource)); + reversedMemberExpression.MapFromExpression(source); return reversedMemberExpression; } reversed.MapFromUntyped(source); @@ -71,12 +66,15 @@ internal static IPropertyMapConfiguration Create(LambdaExpression destination, L public LambdaExpression SourceExpression => _sourceExpression; public LambdaExpression GetDestinationExpression() => _destinationExpression; public IPropertyMapConfiguration Reverse() => Create(_sourceExpression, _destinationExpression); +#if FULL_OR_STANDARD + public bool Ignored => false; + #endif + public void Condition(Func, bool> condition) => PathMapActions.Add(pm => { Expression> expr = - (src, dest, srcMember, destMember, ctxt) => - condition(new ConditionParameters(src, dest, srcMember, destMember, ctxt)); + (src, dest, srcMember, destMember, ctxt) => condition(new(src, dest, srcMember, destMember, ctxt)); pm.Condition = expr; }); } \ No newline at end of file diff --git a/src/AutoMapper/Configuration/Profile.cs b/src/AutoMapper/Configuration/Profile.cs index 499b7fb33e..ce79b32b0b 100644 --- a/src/AutoMapper/Configuration/Profile.cs +++ b/src/AutoMapper/Configuration/Profile.cs @@ -10,7 +10,7 @@ public interface IProfileConfiguration bool? AllowNullCollections { get; } bool? EnableNullPropagationForQueryMapping { get; } IReadOnlyCollection> AllTypeMapActions { get; } - IReadOnlyCollection> AllPropertyMapActions { get; } + IReadOnlyCollection AllPropertyMapActions { get; } /// /// Source extension methods included for search @@ -54,13 +54,13 @@ public interface IProfileConfiguration /// public class Profile : IProfileExpressionInternal, IProfileConfiguration { - private readonly List _prefixes = new() { "Get" }; - private readonly List _postfixes = new(); - private readonly List _typeMapConfigs = new(); + private readonly List _prefixes = ["Get"]; + private readonly List _postfixes = []; + private readonly List _typeMapConfigs = []; private readonly PrePostfixName _prePostfixName = new(); private ReplaceName _replaceName; private readonly MemberConfiguration _memberConfiguration; - private List> _allPropertyMapActions; + private List _allPropertyMapActions; private List> _allTypeMapActions; private List _globalIgnores; private List _openTypeMapConfigs; @@ -81,7 +81,7 @@ protected Profile() bool? IProfileExpressionInternal.FieldMappingEnabled { get; set; } bool? IProfileConfiguration.FieldMappingEnabled => this.Internal().FieldMappingEnabled; bool? IProfileConfiguration.EnableNullPropagationForQueryMapping => this.Internal().EnableNullPropagationForQueryMapping; - IReadOnlyCollection> IProfileConfiguration.AllPropertyMapActions + IReadOnlyCollection IProfileConfiguration.AllPropertyMapActions => _allPropertyMapActions.NullCheck(); IReadOnlyCollection> IProfileConfiguration.AllTypeMapActions => _allTypeMapActions.NullCheck(); IReadOnlyCollection IProfileConfiguration.GlobalIgnores => _globalIgnores.NullCheck(); @@ -108,24 +108,21 @@ public INamingConvention DestinationMemberNamingConvention get => _memberConfiguration.DestinationNamingConvention; set => _memberConfiguration.DestinationNamingConvention = value; } - public List ValueTransformers => _valueTransformerConfigs ??= new(); + public List ValueTransformers => _valueTransformerConfigs ??= []; List IProfileExpressionInternal.Prefixes => _prefixes; List IProfileExpressionInternal.Postfixes => _postfixes; public void DisableConstructorMapping() => _constructorMappingEnabled = false; void IProfileExpressionInternal.ForAllMaps(Action configuration) { - _allTypeMapActions ??= new(); + _allTypeMapActions ??= []; _allTypeMapActions.Add(configuration); } void IProfileExpressionInternal.ForAllPropertyMaps(Func condition, Action configuration) { - _allPropertyMapActions ??= new(); - _allPropertyMapActions.Add((pm, cfg) => - { - if (condition(pm)) configuration(pm, cfg); - }); + _allPropertyMapActions ??= []; + _allPropertyMapActions.Add(new(condition, configuration)); } public IProjectionExpression CreateProjection() => CreateProjection(MemberList.Destination); @@ -137,7 +134,7 @@ public IMappingExpression CreateMap(memberList); private IMappingExpression CreateMapCore(MemberList memberList, bool projection = false) { - var mappingExp = new MappingExpression(memberList, projection); + MappingExpression mappingExp = new(memberList, projection); _typeMapConfigs.Add(mappingExp); return mappingExp; } @@ -147,12 +144,12 @@ public IMappingExpression CreateMap(Type sourceType, Type destinationType) => public IMappingExpression CreateMap(Type sourceType, Type destinationType, MemberList memberList) { - var types = new TypePair(sourceType, destinationType); - var map = new MappingExpression(types, memberList); + TypePair types = new(sourceType, destinationType); + MappingExpression map = new(memberList, types); _typeMapConfigs.Add(map); if (types.ContainsGenericParameters) { - _openTypeMapConfigs ??= new(); + _openTypeMapConfigs ??= []; _openTypeMapConfigs.Add(map); } return map; @@ -173,13 +170,14 @@ public void ReplaceMemberName(string original, string newValue) public void RecognizeDestinationPostfixes(params string[] postfixes) => _prePostfixName.DestinationPostfixes.TryAdd(postfixes); public void AddGlobalIgnore(string propertyNameStartingWith) { - _globalIgnores ??= new(); + _globalIgnores ??= []; _globalIgnores.Add(propertyNameStartingWith); } public void IncludeSourceExtensionMethods(Type type) { - _sourceExtensionMethods ??= new(); + _sourceExtensionMethods ??= []; _sourceExtensionMethods.AddRange( - type.GetMethods(TypeExtensions.StaticFlags).Where(m => m.Has() && m.GetParameters().Length == 1)); + type.GetMethods(Internal.TypeExtensions.StaticFlags).Where(m => m.Has() && m.GetParameters().Length == 1)); } -} \ No newline at end of file +} +public readonly record struct PropertyMapAction(Func Condition, Action Action); \ No newline at end of file diff --git a/src/AutoMapper/Configuration/SourceMappingExpression.cs b/src/AutoMapper/Configuration/SourceMappingExpression.cs index 80d63ed664..dc42c755bf 100644 --- a/src/AutoMapper/Configuration/SourceMappingExpression.cs +++ b/src/AutoMapper/Configuration/SourceMappingExpression.cs @@ -1,5 +1,4 @@ namespace AutoMapper.Configuration; - public interface ISourceMemberConfiguration { void Configure(TypeMap typeMap); @@ -15,19 +14,14 @@ public interface ISourceMemberConfigurationExpression /// void DoNotValidate(); } -public class SourceMappingExpression : ISourceMemberConfigurationExpression, ISourceMemberConfiguration +public sealed class SourceMappingExpression(MemberInfo sourceMember) : ISourceMemberConfigurationExpression, ISourceMemberConfiguration { - private readonly MemberInfo _sourceMember; - private readonly List> _sourceMemberActions = new List>(); - - public SourceMappingExpression(MemberInfo sourceMember) => _sourceMember = sourceMember; - - public void DoNotValidate() => _sourceMemberActions.Add(smc => smc.Ignore()); - + private readonly MemberInfo _sourceMember = sourceMember; + private readonly List> _sourceMemberActions = []; + public void DoNotValidate() => _sourceMemberActions.Add(smc => smc.Ignored = true); public void Configure(TypeMap typeMap) { var sourcePropertyConfig = typeMap.FindOrCreateSourceMemberConfigFor(_sourceMember); - foreach (var action in _sourceMemberActions) { action(sourcePropertyConfig); @@ -37,15 +31,9 @@ public void Configure(TypeMap typeMap) /// /// Contains member configuration relating to source members /// -public class SourceMemberConfig +[EditorBrowsable(EditorBrowsableState.Never)] +public sealed class SourceMemberConfig(MemberInfo sourceMember) { - private bool _ignored; - - public SourceMemberConfig(MemberInfo sourceMember) => SourceMember = sourceMember; - - public MemberInfo SourceMember { get; } - - public void Ignore() => _ignored = true; - - public bool IsIgnored() => _ignored; + public MemberInfo SourceMember { get; } = sourceMember; + public bool Ignored { get; set; } } \ No newline at end of file diff --git a/src/AutoMapper/Configuration/TypeMapConfiguration.cs b/src/AutoMapper/Configuration/TypeMapConfiguration.cs index 173eecd099..c86f539bfa 100644 --- a/src/AutoMapper/Configuration/TypeMapConfiguration.cs +++ b/src/AutoMapper/Configuration/TypeMapConfiguration.cs @@ -1,23 +1,15 @@ using AutoMapper.Features; namespace AutoMapper.Configuration; [EditorBrowsable(EditorBrowsableState.Never)] -public abstract class TypeMapConfiguration +public abstract class TypeMapConfiguration(MemberList memberList, TypePair types) { private List _valueTransformers; private Features _features; private List _sourceMemberConfigurations; private List _ctorParamConfigurations; private List _memberConfigurations; - private readonly MemberList _memberList; - private readonly TypePair _types; - protected TypeMapConfiguration(MemberList memberList, Type sourceType, Type destinationType) : this(memberList, new TypePair(sourceType, destinationType)) - { - } - protected TypeMapConfiguration(MemberList memberList, TypePair types) - { - _memberList = memberList; - _types = types; - } + private readonly MemberList _memberList = memberList; + private readonly TypePair _types = types; public Type DestinationTypeOverride { get; protected set; } protected bool Projection { get; set; } public TypePair Types => _types; @@ -28,12 +20,12 @@ protected TypeMapConfiguration(MemberList memberList, TypePair types) public Type DestinationType => _types.DestinationType; public Features Features => _features ??= new(); public TypeMapConfiguration ReverseTypeMap => ReverseMapExpression; - public List ValueTransformers => _valueTransformers ??= new(); + public List ValueTransformers => _valueTransformers ??= []; protected TypeMapConfiguration ReverseMapExpression { get; set; } - protected List> TypeMapActions { get; } = new List>(); - protected List MemberConfigurations => _memberConfigurations ??= new(); - protected List SourceMemberConfigurations => _sourceMemberConfigurations ??= new(); - protected List CtorParamConfigurations => _ctorParamConfigurations ??= new(); + protected List> TypeMapActions { get; } = []; + protected List MemberConfigurations => _memberConfigurations ??= []; + protected List SourceMemberConfigurations => _sourceMemberConfigurations ??= []; + protected List CtorParamConfigurations => _ctorParamConfigurations ??= []; public void Configure(TypeMap typeMap, List sourceMembers) { TypeMap = typeMap; @@ -124,7 +116,7 @@ private void ConfigureReverseMap(TypeMap typeMap) } private void MapDestinationCtorToSource(TypeMap typeMap, List sourceMembers) { - sourceMembers ??= new(); + sourceMembers ??= []; ConstructorMap ctorMap = new(); typeMap.ConstructorMap = ctorMap; foreach (var destCtor in typeMap.DestinationConstructors) @@ -158,25 +150,28 @@ private void MapDestinationCtorToSource(TypeMap typeMap, List source bool IsConfigured(ParameterInfo parameter) => _ctorParamConfigurations?.Any(c => c.CtorParamName == parameter.Name) is true; } protected IEnumerable MapToSourceMembers() => - _memberConfigurations?.Where(m => m.SourceExpression != null && m.SourceExpression.Body == m.SourceExpression.Parameters[0]) ?? Array.Empty(); + _memberConfigurations?.Where(m => m.SourceExpression != null && m.SourceExpression.Body == m.SourceExpression.Parameters[0]) ?? []; private void ReverseIncludedMembers(TypeMap typeMap) { Stack chain = null; foreach (var includedMember in typeMap.IncludedMembers.Where(i => i.IsMemberPath(out chain))) { - var memberPath = new MemberPath(chain); var newSource = Parameter(typeMap.DestinationType, "source"); var customExpression = Lambda(newSource, newSource); - ReverseSourceMembers(memberPath, customExpression); + ReverseSourceMembers(new(chain), customExpression); } } private void ReverseSourceMembers(TypeMap typeMap) { - foreach (var propertyMap in typeMap.PropertyMaps.Where(p => p.SourceMembers.Length > 1 && !p.SourceMembers.Any(s => s is MethodInfo))) + foreach (var propertyMap in typeMap.PropertyMaps) { - var memberPath = new MemberPath(propertyMap.SourceMembers); + var sourceMembers = propertyMap.SourceMembers; + if(sourceMembers.Length <= 1 || Array.Exists(sourceMembers, m => m is MethodInfo)) + { + continue; + } var customExpression = propertyMap.DestinationMember.Lambda(); - ReverseSourceMembers(memberPath, customExpression); + ReverseSourceMembers(new(sourceMembers), customExpression); } } private void ReverseSourceMembers(MemberPath memberPath, LambdaExpression customExpression) @@ -186,9 +181,7 @@ private void ReverseSourceMembers(MemberPath memberPath, LambdaExpression custom var newDestination = Parameter(reverseTypeMap.DestinationType, "destination"); var path = memberPath.Members.Chain(newDestination); var forPathLambda = Lambda(path, newDestination); - var pathMap = reverseTypeMap.FindOrCreatePathMapFor(forPathLambda, memberPath, reverseTypeMap); - pathMap.MapFrom(customExpression); }); } @@ -199,19 +192,19 @@ protected void ForSourceMemberCore(string sourceMemberName, Action memberOptions) { - var srcConfig = new SourceMappingExpression(memberInfo); + SourceMappingExpression srcConfig = new(memberInfo); memberOptions(srcConfig); SourceMemberConfigurations.Add(srcConfig); } protected void IncludeCore(Type derivedSourceType, Type derivedDestinationType) { - var derivedTypes = new TypePair(derivedSourceType, derivedDestinationType); + TypePair derivedTypes = new(derivedSourceType, derivedDestinationType); derivedTypes.CheckIsDerivedFrom(_types); TypeMapActions.Add(tm => tm.IncludeDerivedTypes(derivedTypes)); } protected void IncludeBaseCore(Type sourceBase, Type destinationBase) { - var baseTypes = new TypePair(sourceBase, destinationBase); + TypePair baseTypes = new(sourceBase, destinationBase); _types.CheckIsDerivedFrom(baseTypes); TypeMapActions.Add(tm => tm.IncludeBaseTypes(baseTypes)); } @@ -232,12 +225,10 @@ public IPropertyMapConfiguration GetDestinationMemberConfiguration(MemberInfo de } protected abstract void IgnoreDestinationMember(MemberInfo property, bool ignorePaths = true); } -public abstract class MappingExpressionBase : TypeMapConfiguration, IMappingExpressionBase +public abstract class MappingExpressionBase(MemberList memberList, TypePair types) : TypeMapConfiguration(memberList, types), IMappingExpressionBase where TMappingExpression : class, IMappingExpressionBase { - protected MappingExpressionBase(MemberList memberList) : base(memberList, typeof(TSource), typeof(TDestination)){ } - protected MappingExpressionBase(MemberList memberList, Type sourceType, Type destinationType) : base(memberList, sourceType, destinationType){} - protected MappingExpressionBase(MemberList memberList, TypePair types) : base(memberList, types){} + protected MappingExpressionBase(MemberList memberList) : this(memberList, new(typeof(TSource), typeof(TDestination))){ } public void As(Type typeOverride) { if (typeOverride == DestinationType) @@ -351,7 +342,7 @@ public void ConvertUsing() where TTypeConverter : ITypeConverter SetTypeConverter(new ClassTypeConverter(typeof(TTypeConverter), typeof(ITypeConverter))); public TMappingExpression ForCtorParam(string ctorParamName, Action> paramOptions) { - var ctorParamExpression = new CtorParamConfigurationExpression(ctorParamName, SourceType); + CtorParamConfigurationExpression ctorParamExpression = new(ctorParamName, SourceType); paramOptions(ctorParamExpression); CtorParamConfigurations.Add(ctorParamExpression); return this as TMappingExpression; diff --git a/src/AutoMapper/ConstructorMap.cs b/src/AutoMapper/ConstructorMap.cs index c79e445b25..c3bffeb25f 100644 --- a/src/AutoMapper/ConstructorMap.cs +++ b/src/AutoMapper/ConstructorMap.cs @@ -1,9 +1,9 @@ namespace AutoMapper; [EditorBrowsable(EditorBrowsableState.Never)] -public class ConstructorMap +public sealed class ConstructorMap { private bool? _canResolve; - private readonly List _ctorParams = new(); + private readonly List _ctorParams = []; public ConstructorInfo Ctor { get; private set; } public IReadOnlyCollection CtorParams => _ctorParams; public void Reset(ConstructorInfo ctor) @@ -17,11 +17,11 @@ public bool CanResolve get => _canResolve ??= ParametersCanResolve(); set => _canResolve = value; } - private bool ParametersCanResolve() + bool ParametersCanResolve() { foreach (var param in _ctorParams) { - if (!param.CanResolveValue) + if (!param.IsMapped) { return false; } @@ -42,58 +42,66 @@ public ConstructorParameterMap this[string name] return null; } } - public void AddParameter(ParameterInfo parameter, IEnumerable sourceMembers, TypeMap typeMap) => - _ctorParams.Add(new(typeMap, parameter, sourceMembers.ToArray())); - public bool ApplyIncludedMember(IncludedMember includedMember) + public void AddParameter(ParameterInfo parameter, IEnumerable sourceMembers, TypeMap typeMap) => _ctorParams.Add(new(typeMap, parameter, sourceMembers.ToArray())); + public bool ApplyMap(TypeMap typeMap, IncludedMember includedMember = null) { - var includedMap = includedMember.TypeMap.ConstructorMap; - if (CanResolve || includedMap?.Ctor != Ctor) + var constructorMap = typeMap.ConstructorMap; + if(constructorMap == null) { return false; } - bool canResolve = false; - var includedParams = includedMap._ctorParams; - for(int index = 0; index < includedParams.Count; index++) + bool applied = false; + foreach(var parameterMap in _ctorParams) { - var includedParam = includedParams[index]; - if (!includedParam.CanResolveValue || _ctorParams[index].CanResolveValue) + var inheritedParameterMap = constructorMap[parameterMap.DestinationName]; + if(inheritedParameterMap is not { IsMapped: true, DestinationType: var type } || type != parameterMap.DestinationType || !parameterMap.ApplyMap(inheritedParameterMap, includedMember)) { continue; } - canResolve = true; + applied = true; _canResolve = null; - _ctorParams[index] = new(includedParam, includedMember); } - return canResolve; + return applied; } } [EditorBrowsable(EditorBrowsableState.Never)] public class ConstructorParameterMap : MemberMap { - private Type _sourceType; - public ConstructorParameterMap(TypeMap typeMap, ParameterInfo parameter, MemberInfo[] sourceMembers) : base(typeMap) + public ConstructorParameterMap(TypeMap typeMap, ParameterInfo parameter, MemberInfo[] sourceMembers) : base(typeMap, parameter.ParameterType) { Parameter = parameter; + if(DestinationType.IsByRef) + { + DestinationType = DestinationType.GetElementType(); + } if (sourceMembers.Length > 0) { MapByConvention(sourceMembers); } else { - SourceMembers = Array.Empty(); + SourceMembers = []; } } - public ConstructorParameterMap(ConstructorParameterMap parameterMap, IncludedMember includedMember) : - this(includedMember.TypeMap, parameterMap.Parameter, parameterMap.SourceMembers) => - IncludedMember = includedMember.Chain(parameterMap.IncludedMember); public ParameterInfo Parameter { get; } - public override Type SourceType => _sourceType ??= GetSourceType(); - public override Type DestinationType => Parameter.ParameterType; - public override IncludedMember IncludedMember { get; } + public override IncludedMember IncludedMember { get; protected set; } public override MemberInfo[] SourceMembers { get; set; } public override string DestinationName => Parameter.Name; public Expression DefaultValue(IGlobalConfiguration configuration) => Parameter.IsOptional ? Parameter.GetDefaultValue(configuration) : configuration.Default(DestinationType); - public override string ToString() => $"{Constructor}, parameter {DestinationName}"; - private MemberInfo Constructor => Parameter.Member; + public override string ToString() => $"{Parameter.Member}, parameter {DestinationName}"; + public bool ApplyMap(ConstructorParameterMap inheritedParameterMap, IncludedMember includedMember) + { + if(includedMember != null && IsMapped) + { + return false; + } + ExplicitExpansion ??= inheritedParameterMap.ExplicitExpansion; + if(ApplyInheritedMap(inheritedParameterMap)) + { + IncludedMember = includedMember?.Chain(inheritedParameterMap.IncludedMember); + return true; + } + return false; + } public override bool? ExplicitExpansion { get; set; } } \ No newline at end of file diff --git a/src/AutoMapper/Execution/ExpressionBuilder.cs b/src/AutoMapper/Execution/ExpressionBuilder.cs index 7ffd081c30..3b30971569 100644 --- a/src/AutoMapper/Execution/ExpressionBuilder.cs +++ b/src/AutoMapper/Execution/ExpressionBuilder.cs @@ -6,7 +6,7 @@ namespace AutoMapper.Execution; [EditorBrowsable(EditorBrowsableState.Never)] public static class ExpressionBuilder { - public static readonly MethodInfo ObjectToString = typeof(object).GetMethod(nameof(object.ToString)); + public static readonly MethodInfo ObjectToString = typeof(object).GetMethod(nameof(ToString)); public static readonly Expression True = Constant(true, typeof(bool)); public static readonly Expression Null = Expression.Default(typeof(object)); public static readonly Expression Empty = Empty(); @@ -42,7 +42,7 @@ public static (List Variables, List Expressions var variables = configuration?.Variables; if (variables == null) { - variables = new(); + variables = []; } else { @@ -51,7 +51,7 @@ public static (List Variables, List Expressions var expressions = configuration?.Expressions; if (expressions == null) { - expressions = new(); + expressions = []; } else { @@ -68,6 +68,7 @@ public static Expression MapExpression(this IGlobalConfiguration configuration, bool nullCheck; if (typeMap != null) { + typeMap.CheckProjection(); var allowNull = memberMap?.AllowNull; nullCheck = !typeMap.HasTypeConverter && (destination.NodeType != ExpressionType.Default || (allowNull.HasValue && allowNull != profileMap.AllowNullDestinationValues)); @@ -87,14 +88,13 @@ public static Expression MapExpression(this IGlobalConfiguration configuration, { mapExpression = mapper.MapExpression(configuration, profileMap, memberMap, source, destination); nullCheck = mapExpression != source; - mapExpression = ToType(mapExpression, typePair.DestinationType); } else { nullCheck = true; } } - mapExpression ??= ContextMap(typePair, source, destination, memberMap); + mapExpression = mapExpression == null ? ContextMap(typePair, source, destination, memberMap) : ToType(mapExpression, typePair.DestinationType); return nullCheck ? configuration.NullCheckSource(profileMap, source, destination, mapExpression, memberMap) : mapExpression; } public static Expression NullCheckSource(this IGlobalConfiguration configuration, ProfileMap profileMap, Expression source, Expression destination, @@ -180,12 +180,8 @@ public static Expression ApplyTransformers(this MemberMap memberMap, Expression var perMember = memberMap.ValueTransformers; var perMap = memberMap.TypeMap.ValueTransformers; var perProfile = memberMap.Profile.ValueTransformers; - var result = source; - if (perMember.Count > 0 || perMap.Count > 0 || perProfile.Count > 0) - { - result = memberMap.ApplyTransformers(source, configuration, perMember.Concat(perMap).Concat(perProfile)); - } - return result; + return perMember.Count > 0 || perMap.Count > 0 || perProfile.Count > 0 ? + memberMap.ApplyTransformers(source, configuration, perMember.Concat(perMap).Concat(perProfile)) : source; } static Expression ApplyTransformers(this MemberMap memberMap, Expression result, IGlobalConfiguration configuration, IEnumerable transformers) { @@ -234,7 +230,7 @@ public static MemberInfo[] ToMemberInfos(this Stack chain) } public static Stack GetChain(this Expression expression) { - var stack = new Stack(); + Stack stack = []; while (expression != null) { var member = expression switch @@ -256,14 +252,8 @@ public static Stack GetChain(this Expression expression) } return stack; } - public static IEnumerable GetMemberExpressions(this Expression expression) - { - if (expression is not MemberExpression memberExpression) - { - return Array.Empty(); - } - return expression.GetChain().Select(m => m.Expression as MemberExpression).TakeWhile(m => m != null); - } + public static IEnumerable GetMemberExpressions(this Expression expression) => expression is MemberExpression ? + expression.GetChain().Select(m => m.Expression as MemberExpression).TakeWhile(m => m != null) : []; public static bool IsMemberPath(this LambdaExpression lambda, out Stack members) { Expression currentExpression = null; @@ -362,10 +352,10 @@ private static Expression Replace(this ParameterReplaceVisitor visitor, LambdaEx return newLambda; } public static Expression Replace(this Expression exp, Expression old, Expression replace) => new ReplaceVisitor().Replace(exp, old, replace); - public static Expression NullCheck(this Expression expression, IGlobalConfiguration configuration, MemberMap memberMap = null, Expression defaultValue = null) + public static Expression NullCheck(this Expression expression, IGlobalConfiguration configuration, MemberMap memberMap = null, Expression defaultValue = null, IncludedMember includedMember = null) { var chain = expression.GetChain(); - var min = memberMap?.IncludedMember == null ? 2 : 1; + var min = (includedMember ?? memberMap?.IncludedMember) == null ? 2 : 1; if (chain.Count < min || chain.Peek().Target is not ParameterExpression parameter) { return expression; diff --git a/src/AutoMapper/Execution/ObjectFactory.cs b/src/AutoMapper/Execution/ObjectFactory.cs index 1f8ce014d2..84573924b2 100644 --- a/src/AutoMapper/Execution/ObjectFactory.cs +++ b/src/AutoMapper/Execution/ObjectFactory.cs @@ -19,7 +19,7 @@ private static Func GenerateConstructor(Type type) => }; private static Expression CallConstructor(Type type, IGlobalConfiguration configuration) { - var defaultCtor = type.GetConstructor(TypeExtensions.InstanceFlags, null, Type.EmptyTypes, null); + var defaultCtor = type.GetConstructor(Internal.TypeExtensions.InstanceFlags, null, [], null); if (defaultCtor != null) { return New(defaultCtor); @@ -39,7 +39,7 @@ private static Expression CreateInterfaceExpression(Type type) => type.IsGenericType(typeof(ISet<>)) ? CreateCollection(type, typeof(HashSet<>)) : type.IsCollection() ? CreateCollection(type, typeof(List<>), GetIEnumerableArguments(type)) : InvalidType(type, $"Cannot create an instance of interface type {type}."); - private static Type[] GetIEnumerableArguments(Type type) => type.GetIEnumerableType()?.GenericTypeArguments ?? new[] { typeof(object) }; + private static Type[] GetIEnumerableArguments(Type type) => type.GetIEnumerableType()?.GenericTypeArguments ?? [typeof(object)]; private static Expression CreateCollection(Type type, Type collectionType, Type[] genericArguments = null) => ToType(New(collectionType.MakeGenericType(genericArguments ?? type.GenericTypeArguments)), type); private static Expression CreateReadOnlyDictionary(Type[] typeArguments) diff --git a/src/AutoMapper/Execution/ProxyGenerator.cs b/src/AutoMapper/Execution/ProxyGenerator.cs index 9b793db086..64956af715 100644 --- a/src/AutoMapper/Execution/ProxyGenerator.cs +++ b/src/AutoMapper/Execution/ProxyGenerator.cs @@ -4,10 +4,10 @@ namespace AutoMapper.Execution; public static class ProxyGenerator { - private static readonly MethodInfo DelegateCombine = typeof(Delegate).GetMethod(nameof(Delegate.Combine), new[] { typeof(Delegate), typeof(Delegate) }); + private static readonly MethodInfo DelegateCombine = typeof(Delegate).GetMethod(nameof(Delegate.Combine), [typeof(Delegate), typeof(Delegate)]); private static readonly MethodInfo DelegateRemove = typeof(Delegate).GetMethod(nameof(Delegate.Remove)); private static readonly EventInfo PropertyChanged = typeof(INotifyPropertyChanged).GetEvent(nameof(INotifyPropertyChanged.PropertyChanged)); - private static readonly ConstructorInfo ProxyBaseCtor = typeof(ProxyBase).GetConstructor(Type.EmptyTypes); + private static readonly ConstructorInfo ProxyBaseCtor = typeof(ProxyBase).GetConstructor([]); private static readonly ModuleBuilder ProxyModule = CreateProxyModule(); private static readonly LockingConcurrentDictionary ProxyTypes = new(EmitProxy); private static ModuleBuilder CreateProxyModule() @@ -34,11 +34,11 @@ TypeBuilder GenerateType() var propertyNames = string.Join("_", typeDescription.AdditionalProperties.Select(p => p.Name)); var typeName = $"Proxy_{interfaceType.FullName}_{typeDescription.GetHashCode()}_{propertyNames}"; const int MaxTypeNameLength = 1023; - typeName = typeName.Substring(0, Math.Min(MaxTypeNameLength, typeName.Length)); + typeName = typeName[..Math.Min(MaxTypeNameLength, typeName.Length)]; Debug.WriteLine(typeName, "Emitting proxy type"); return ProxyModule.DefineType(typeName, TypeAttributes.Class | TypeAttributes.Sealed | TypeAttributes.Public, typeof(ProxyBase), - interfaceType.IsInterface ? new[] { interfaceType } : Type.EmptyTypes); + interfaceType.IsInterface ? [interfaceType] : []); } void GeneratePropertyChanged() { @@ -65,7 +65,7 @@ void EventAccessor(MethodInfo method, MethodInfo delegateMethod) } void GenerateFields() { - var fieldBuilders = new Dictionary(); + Dictionary fieldBuilders = []; foreach (var property in PropertiesToImplement()) { if (fieldBuilders.TryGetValue(property.Name, out var propertyEmitter)) @@ -83,8 +83,8 @@ void GenerateFields() } List PropertiesToImplement() { - var propertiesToImplement = new List(); - var allInterfaces = new List(interfaceType.GetInterfaces()) { interfaceType }; + List propertiesToImplement = []; + List allInterfaces = [..interfaceType.GetInterfaces(), interfaceType]; // first we collect all properties, those with setters before getters in order to enable less specific redundant getters foreach (var property in allInterfaces.Where(intf => intf != typeof(INotifyPropertyChanged)) @@ -105,16 +105,16 @@ List PropertiesToImplement() } void GenerateConstructor() { - var constructorBuilder = typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, Type.EmptyTypes); + var constructorBuilder = typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, []); var ctorIl = constructorBuilder.GetILGenerator(); ctorIl.Emit(OpCodes.Ldarg_0); ctorIl.Emit(OpCodes.Call, ProxyBaseCtor); ctorIl.Emit(OpCodes.Ret); } } - public static Type GetProxyType(Type interfaceType) => ProxyTypes.GetOrAdd(new(interfaceType, Array.Empty())); + public static Type GetProxyType(Type interfaceType) => ProxyTypes.GetOrAdd(new(interfaceType, [])); public static Type GetSimilarType(Type sourceType, IEnumerable additionalProperties) => - ProxyTypes.GetOrAdd(new(sourceType, additionalProperties.OrderBy(p=>p.Name).ToArray())); + ProxyTypes.GetOrAdd(new(sourceType, [..additionalProperties.OrderBy(p => p.Name)])); class PropertyEmitter { private static readonly MethodInfo ProxyBaseNotifyPropertyChanged = typeof(ProxyBase).GetInstanceMethod("NotifyPropertyChanged"); @@ -142,7 +142,7 @@ public PropertyEmitter(TypeBuilder owner, PropertyDescription property, FieldBui } _setterBuilder = owner.DefineMethod($"set_{name}", MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig | - MethodAttributes.SpecialName, typeof(void), new[] { propertyType }); + MethodAttributes.SpecialName, typeof(void), [propertyType]); ILGenerator setterIl = _setterBuilder.GetILGenerator(); setterIl.Emit(OpCodes.Ldarg_0); setterIl.Emit(OpCodes.Ldarg_1); @@ -164,8 +164,7 @@ public PropertyEmitter(TypeBuilder owner, PropertyDescription property, FieldBui public abstract class ProxyBase { public ProxyBase() { } - protected void NotifyPropertyChanged(PropertyChangedEventHandler handler, string method) => - handler?.Invoke(this, new PropertyChangedEventArgs(method)); + protected void NotifyPropertyChanged(PropertyChangedEventHandler handler, string method) => handler?.Invoke(this, new(method)); } public readonly record struct TypeDescription(Type Type, PropertyDescription[] AdditionalProperties) { diff --git a/src/AutoMapper/Execution/TypeMapPlanBuilder.cs b/src/AutoMapper/Execution/TypeMapPlanBuilder.cs index f3334bc478..06cf1de571 100644 --- a/src/AutoMapper/Execution/TypeMapPlanBuilder.cs +++ b/src/AutoMapper/Execution/TypeMapPlanBuilder.cs @@ -1,40 +1,42 @@ namespace AutoMapper.Execution; -public ref struct TypeMapPlanBuilder + +public ref struct TypeMapPlanBuilder(IGlobalConfiguration configuration, TypeMap typeMap) { static readonly MethodInfo MappingError = typeof(TypeMapPlanBuilder).GetStaticMethod(nameof(MemberMappingError)); - static readonly MethodInfo IncTypeDepthInfo = typeof(ResolutionContext).GetInstanceMethod(nameof(ResolutionContext.IncrementTypeDepth)); - static readonly MethodInfo DecTypeDepthInfo = typeof(ResolutionContext).GetInstanceMethod(nameof(ResolutionContext.DecrementTypeDepth)); - static readonly MethodInfo CacheDestinationMethod = typeof(ResolutionContext).GetInstanceMethod(nameof(ResolutionContext.CacheDestination)); - static readonly MethodInfo GetDestinationMethod = typeof(ResolutionContext).GetInstanceMethod(nameof(ResolutionContext.GetDestination)); - readonly IGlobalConfiguration _configuration; - readonly ParameterExpression _destination; - readonly ParameterExpression _initialDestination; - readonly ParameterExpression[] _parameters; - readonly TypeMap _typeMap; - readonly ParameterExpression _source; - List _variables; - List _expressions; - CatchBlock[] _catches; - public TypeMapPlanBuilder(IGlobalConfiguration configuration, TypeMap typeMap) - { - _configuration = configuration; - _typeMap = typeMap; - _source = Parameter(typeMap.SourceType, "source"); - _initialDestination = Parameter(typeMap.DestinationType, "destination"); - _destination = Variable(typeMap.DestinationType, "typeMapDestination"); - _variables = configuration.Variables; - _expressions = configuration.Expressions; - _catches = configuration.Catches; - _parameters = _configuration.Parameters ?? new ParameterExpression[] { null, null, ContextParameter }; - } + + static readonly MethodInfo IncTypeDepthInfo = + typeof(ResolutionContext).GetInstanceMethod(nameof(ResolutionContext.IncrementTypeDepth)); + + static readonly MethodInfo DecTypeDepthInfo = + typeof(ResolutionContext).GetInstanceMethod(nameof(ResolutionContext.DecrementTypeDepth)); + + static readonly MethodInfo CacheDestinationMethod = + typeof(ResolutionContext).GetInstanceMethod(nameof(ResolutionContext.CacheDestination)); + + static readonly MethodInfo GetDestinationMethod = + typeof(ResolutionContext).GetInstanceMethod(nameof(ResolutionContext.GetDestination)); + + readonly IGlobalConfiguration _configuration = configuration; + readonly ParameterExpression _destination = Variable(typeMap.DestinationType, "typeMapDestination"); + readonly ParameterExpression _initialDestination = Parameter(typeMap.DestinationType, "destination"); + readonly ParameterExpression[] _parameters = configuration.Parameters ?? [null, null, ContextParameter]; + readonly TypeMap _typeMap = typeMap; + readonly ParameterExpression _source = Parameter(typeMap.SourceType, "source"); + List _variables = configuration.Variables; + List _expressions = configuration.Expressions; + CatchBlock[] _catches = configuration.Catches; public Type DestinationType => _destination.Type; - private static AutoMapperMappingException MemberMappingError(Exception innerException, MemberMap memberMap) => new("Error mapping types.", innerException, memberMap); + + private static AutoMapperMappingException MemberMappingError(Exception innerException, MemberMap memberMap) => + new("Error mapping types.", innerException, memberMap); + ParameterExpression[] GetParameters(ParameterExpression first = null, ParameterExpression second = null) { _parameters[0] = first ?? _source; _parameters[1] = second ?? _destination; return _parameters; } + public LambdaExpression CreateMapperLambda() { var parameters = GetParameters(second: _initialDestination); @@ -43,9 +45,10 @@ public LambdaExpression CreateMapperLambda() { return Lambda(customExpression, parameters); } - _variables ??= new(); - _expressions ??= new(); - _catches ??= new CatchBlock[1]; + + _variables ??= []; + _expressions ??= []; + _catches ??= [null]; var typeMapsPath = _configuration.TypeMapsPath; Clear(ref typeMapsPath); CheckForCycles(_configuration, _typeMap, typeMapsPath); @@ -58,20 +61,24 @@ public LambdaExpression CreateMapperLambda() { IncludeMembers(); } + var checkContext = CheckContext(_typeMap); if (checkContext != null) { _expressions.Add(checkContext); } + _expressions.Add(mapperFunc); _variables.Add(_destination); mapperFunc = Block(_variables, _expressions); - return Lambda(_configuration.NullCheckSource(_typeMap.Profile, _source, _initialDestination, mapperFunc, null), GetParameters(second: _initialDestination)); + return Lambda(_configuration.NullCheckSource(_typeMap.Profile, _source, _initialDestination, mapperFunc, null), + GetParameters(second: _initialDestination)); + static void Clear(ref HashSet typeMapsPath) { if (typeMapsPath == null) { - typeMapsPath = new HashSet(); + typeMapsPath = []; } else { @@ -79,16 +86,20 @@ static void Clear(ref HashSet typeMapsPath) } } } + void IncludeMembers() { foreach (var includedMap in _typeMap.IncludedMembersTypeMaps) { var variable = includedMap.Variable; _variables.Add(variable); - _expressions.Add(Assign(variable, _configuration.ReplaceParameters(includedMap.MemberExpression, _source).NullCheck(null))); + _expressions.Add(Assign(variable, + _configuration.ReplaceParameters(includedMap.MemberExpression, _source).NullCheck(null))); } } - private static void CheckForCycles(IGlobalConfiguration configuration, TypeMap typeMap, HashSet typeMapsPath) + + private static void CheckForCycles(IGlobalConfiguration configuration, TypeMap typeMap, + HashSet typeMapsPath) { typeMapsPath.Add(typeMap); foreach (var memberMap in MemberMaps()) @@ -98,25 +109,31 @@ private static void CheckForCycles(IGlobalConfiguration configuration, TypeMap t { continue; } - if (memberMap.Inline && (memberTypeMap.PreserveReferences || typeMapsPath.Count == configuration.MaxExecutionPlanDepth)) + + if (memberMap.Inline && (memberTypeMap.PreserveReferences || + typeMapsPath.Count == configuration.MaxExecutionPlanDepth)) { memberMap.Inline = false; TraceInline(typeMap, memberMap); } + if (memberTypeMap.PreserveReferences || memberTypeMap.MapExpression != null) { continue; } + if (typeMapsPath.Contains(memberTypeMap)) { - if (memberTypeMap.SourceType.IsValueType) + if (memberTypeMap.SourceType.IsValueType || memberTypeMap.DestinationType.IsValueType) { if (memberTypeMap.MaxDepth == 0) { memberTypeMap.MaxDepth = 10; } + continue; } + memberTypeMap.PreserveReferences = true; Trace(typeMap, memberTypeMap, memberMap); if (memberMap.Inline) @@ -124,52 +141,69 @@ private static void CheckForCycles(IGlobalConfiguration configuration, TypeMap t memberMap.Inline = false; TraceInline(typeMap, memberMap); } + foreach (var derivedTypeMap in configuration.GetIncludedTypeMaps(memberTypeMap)) { derivedTypeMap.PreserveReferences = true; Trace(typeMap, derivedTypeMap, memberMap); } } + CheckForCycles(configuration, memberTypeMap, typeMapsPath); } + typeMapsPath.Remove(typeMap); return; + IEnumerable MemberMaps() { var memberMaps = typeMap.MemberMaps; - return typeMap.HasDerivedTypesToInclude ? - memberMaps.Concat(configuration.GetIncludedTypeMaps(typeMap).SelectMany(tm => tm.MemberMaps)) : - memberMaps; + return typeMap.HasDerivedTypesToInclude + ? memberMaps.Concat(configuration.GetIncludedTypeMaps(typeMap).SelectMany(tm => tm.MemberMaps)) + : memberMaps; } + TypeMap ResolveMemberTypeMap(MemberMap memberMap) { if (!memberMap.CanResolveValue) { return null; } + var types = memberMap.Types(); return types.ContainsGenericParameters ? null : configuration.ResolveAssociatedTypeMap(types); } + [Conditional("DEBUG")] static void Trace(TypeMap typeMap, TypeMap memberTypeMap, MemberMap memberMap) => - Debug.WriteLine($"Setting PreserveReferences: {memberMap.DestinationName} {typeMap.SourceType} - {typeMap.DestinationType} => {memberTypeMap.SourceType} - {memberTypeMap.DestinationType}"); + Debug.WriteLine( + $"Setting PreserveReferences: {memberMap.DestinationName} {typeMap.SourceType} - {typeMap.DestinationType} => {memberTypeMap.SourceType} - {memberTypeMap.DestinationType}"); + [Conditional("DEBUG")] static void TraceInline(TypeMap typeMap, MemberMap memberMap) => - Debug.WriteLine($"Resetting Inline: {memberMap.DestinationName} in {typeMap.SourceType} - {typeMap.DestinationType}"); + Debug.WriteLine( + $"Resetting Inline: {memberMap.DestinationName} in {typeMap.SourceType} - {typeMap.DestinationType}"); } + private Expression CreateDestinationFunc() { var newDestFunc = CreateNewDestinationFunc(); - var getDest = DestinationType.IsValueType ? newDestFunc : Coalesce(_initialDestination, ToType(newDestFunc, DestinationType)); + var getDest = DestinationType.IsValueType + ? newDestFunc + : Coalesce(_initialDestination, ToType(newDestFunc, DestinationType)); var destinationFunc = Assign(_destination, getDest); - return _typeMap.PreserveReferences ? - Block(destinationFunc, Call(ContextParameter, CacheDestinationMethod, _source, Constant(DestinationType), _destination), _destination) : - destinationFunc; + return _typeMap.PreserveReferences + ? Block(destinationFunc, + Call(ContextParameter, CacheDestinationMethod, _source, Constant(DestinationType), _destination), + _destination) + : destinationFunc; } + Expression ReplaceParameters(LambdaExpression lambda) => _configuration.ReplaceParameters(lambda, GetParameters()); + private Expression CreateAssignmentFunc(Expression createDestination) { - List actions = new() { createDestination }; + List actions = [createDestination]; Expression typeMapExpression = null; var hasMaxDepth = _typeMap.MaxDepth > 0; if (hasMaxDepth) @@ -177,6 +211,7 @@ private Expression CreateAssignmentFunc(Expression createDestination) typeMapExpression = Constant(_typeMap); actions.Add(Call(ContextParameter, IncTypeDepthInfo, typeMapExpression)); } + AddBeforeMap(actions); AddPropertyMaps(actions); AddPathMaps(actions); @@ -185,9 +220,11 @@ private Expression CreateAssignmentFunc(Expression createDestination) { actions.Add(Call(ContextParameter, DecTypeDepthInfo, typeMapExpression)); } + actions.Add(_destination); return Block(actions); } + private void AddAfterMap(List actions) { var afterMap = _typeMap.AfterMapActions; @@ -195,11 +232,13 @@ private void AddAfterMap(List actions) { return; } + foreach (var afterMapAction in afterMap) { actions.Add(ReplaceParameters(afterMapAction)); } } + private void AddPathMaps(List actions) { var pathMaps = _typeMap.PathMaps; @@ -207,15 +246,25 @@ private void AddPathMaps(List actions) { return; } + foreach (var pathMap in pathMaps) { if (pathMap.Ignored) { continue; } - actions.Add(TryPathMap(pathMap)); + + try + { + actions.Add(TryPathMap(pathMap)); + } + catch (Exception e) when (e is not AutoMapperConfigurationException) + { + throw new AutoMapperMappingException("Error building path mapping strategy.", e, pathMap); + } } } + private void AddBeforeMap(List actions) { var beforeMap = _typeMap.BeforeMapActions; @@ -223,11 +272,13 @@ private void AddBeforeMap(List actions) { return; } + foreach (var beforeMapAction in beforeMap) { actions.Add(ReplaceParameters(beforeMapAction)); } } + private void AddPropertyMaps(List actions) { var propertyMaps = _typeMap.OrderedPropertyMaps(); @@ -235,35 +286,52 @@ private void AddPropertyMaps(List actions) { return; } + foreach (var propertyMap in propertyMaps) { if (!propertyMap.CanResolveValue) { continue; } - var property = TryMemberMap(propertyMap, CreatePropertyMapFunc(propertyMap, _destination, propertyMap.DestinationMember)); - if (_typeMap.ConstructorParameterMatches(propertyMap.DestinationName)) + + try + { + var property = TryMemberMap(propertyMap, + CreatePropertyMapFunc(propertyMap, _destination, propertyMap.DestinationMember)); + if (_typeMap.ConstructorParameterMatches(propertyMap.DestinationName)) + { + property = _initialDestination.IfNullElse(_configuration.Default(property.Type), property); + } + + actions.Add(property); + } + catch (Exception e) when (e is not AutoMapperConfigurationException) { - property = _initialDestination.IfNullElse(_configuration.Default(property.Type), property); + throw new AutoMapperMappingException("Error building member mapping strategy.", e, propertyMap); } - actions.Add(property); } } + private Expression TryPathMap(PathMap pathMap) { - var destination = ((MemberExpression)_configuration.ConvertReplaceParameters(pathMap.DestinationExpression, _destination)).Expression; + var destination = + ((MemberExpression)_configuration.ConvertReplaceParameters(pathMap.DestinationExpression, _destination)) + .Expression; var pathMapFunc = CreatePropertyMapFunc(pathMap, destination, pathMap.MemberPath.Last); _expressions.Clear(); foreach (var member in destination.GetMemberExpressions()) { var setter = GetSetter(member); var ifNull = setter == null - ? Throw(Constant(new NullReferenceException($"{member} cannot be null because it's used by ForPath.")), member.Type) + ? Throw(Constant(new NullReferenceException($"{member} cannot be null because it's used by ForPath.")), + member.Type) : (Expression)Assign(setter, ObjectFactory.GenerateConstructorExpression(member.Type, _configuration)); _expressions.Add(member.IfNullElse(ifNull, _configuration.Default(member.Type))); } + _expressions.Add(pathMapFunc); return TryMemberMap(pathMap, Block(_expressions)); + static Expression GetSetter(MemberExpression memberExpression) => memberExpression.Member switch { PropertyInfo { CanWrite: true } property => Property(memberExpression.Expression, property), @@ -271,6 +339,7 @@ private Expression TryPathMap(PathMap pathMap) _ => null, }; } + private Expression CreateMapperFunc(Expression assignmentFunc) { var mapperFunc = assignmentFunc; @@ -279,37 +348,54 @@ private Expression CreateMapperFunc(Expression assignmentFunc) { mapperFunc = Condition(overMaxDepth, _configuration.Default(DestinationType), mapperFunc); } + return CheckReferencesCache(mapperFunc); } + private Expression CheckReferencesCache(Expression valueBuilder) { - if(!_typeMap.PreserveReferences) + if (!_typeMap.PreserveReferences) { return valueBuilder; } + var getCachedDestination = Call(ContextParameter, GetDestinationMethod, _source, Constant(DestinationType)); return Coalesce(ToType(getCachedDestination, DestinationType), valueBuilder); } + private Expression CreateNewDestinationFunc() => _typeMap switch { - { CustomCtorFunction: LambdaExpression constructUsingFunc } => _configuration.ReplaceParameters(constructUsingFunc, GetParameters(second: ContextParameter)), + { CustomCtorFunction: LambdaExpression constructUsingFunc } => _configuration.ReplaceParameters( + constructUsingFunc, GetParameters(second: ContextParameter)), { ConstructorMap: { CanResolve: true } constructorMap } => ConstructorMapping(constructorMap), - { DestinationType: { IsInterface: true } interfaceType } => Throw(Constant(new AutoMapperMappingException("Cannot create interface "+interfaceType, null, _typeMap)), interfaceType), + { DestinationType: { IsInterface: true } interfaceType } => Throw( + Constant(new AutoMapperMappingException("Cannot create interface " + interfaceType, null, _typeMap)), + interfaceType), _ => ObjectFactory.GenerateConstructorExpression(DestinationType, _configuration) }; + private Expression ConstructorMapping(ConstructorMap constructorMap) { - List variables = new(); - List body = new(); + List variables = []; + List body = []; foreach (var parameter in constructorMap.CtorParams) { - var variable = Variable(parameter.DestinationType, parameter.DestinationName); - variables.Add(variable); - body.Add(Assign(variable, CreateConstructorParameterExpression(parameter))); + try + { + var variable = Variable(parameter.DestinationType, parameter.DestinationName); + variables.Add(variable); + body.Add(Assign(variable, CreateConstructorParameterExpression(parameter))); + } + catch (Exception e) when (e is not AutoMapperConfigurationException) + { + throw new AutoMapperMappingException("Error building constructor parameter mapping strategy.", e, parameter); + } } + body.Add(CheckReferencesCache(New(constructorMap.Ctor, variables))); return Block(variables, body); } + private Expression CreateConstructorParameterExpression(ConstructorParameterMap ctorParamMap) { var defaultValue = ctorParamMap.DefaultValue(_configuration); @@ -324,12 +410,14 @@ private Expression CreateConstructorParameterExpression(ConstructorParameterMap _expressions.Add(mapMember); return TryMemberMap(ctorParamMap, Block(_variables, _expressions)); } + private Expression TryMemberMap(MemberMap memberMap, Expression memberMapExpression) { var newException = Call(MappingError, ExceptionParameter, Constant(memberMap)); _catches[0] = Catch(ExceptionParameter, Throw(newException, memberMapExpression.Type)); return TryCatch(memberMapExpression, _catches); } + private Expression CreatePropertyMapFunc(MemberMap memberMap, Expression destination, MemberInfo destinationMember) { Expression destinationMemberAccess, destinationMemberGetter; @@ -338,7 +426,9 @@ private Expression CreatePropertyMapFunc(MemberMap memberMap, Expression destina { destinationMemberAccess = Property(destination, destinationProperty); destinationMemberReadOnly = !destinationProperty.CanWrite; - destinationMemberGetter = destinationProperty.CanRead ? destinationMemberAccess : _configuration.Default(memberMap.DestinationType); + destinationMemberGetter = destinationProperty.CanRead + ? destinationMemberAccess + : _configuration.Default(memberMap.DestinationType); } else { @@ -347,46 +437,62 @@ private Expression CreatePropertyMapFunc(MemberMap memberMap, Expression destina destinationMemberReadOnly = destinationField.IsInitOnly; destinationMemberGetter = destinationMemberAccess; } + var customSource = GetCustomSource(memberMap); var valueResolver = BuildValueResolverFunc(memberMap, customSource, destinationMemberGetter); var resolvedValueVariable = Variable(valueResolver.Type, "resolvedValue"); - var destinationMemberValue = DestinationMemberValue(memberMap, destinationMemberGetter, destinationMemberReadOnly); + var destinationMemberValue = + DestinationMemberValue(memberMap, destinationMemberGetter, destinationMemberReadOnly); var mappedMember = MapMember(memberMap, resolvedValueVariable, destinationMemberValue); var mappedMemberVariable = SetVariables(valueResolver, resolvedValueVariable, mappedMember); - var mapperExpr = destinationMemberReadOnly ? (Expression)mappedMemberVariable : Assign(destinationMemberAccess, mappedMemberVariable); + var mapperExpr = destinationMemberReadOnly + ? (Expression)mappedMemberVariable + : Assign(destinationMemberAccess, mappedMemberVariable); if (memberMap.Condition != null) { _expressions.Add(IfThen( - _configuration.ConvertReplaceParameters(memberMap.Condition, new[] { customSource, _destination, mappedMemberVariable, destinationMemberGetter, ContextParameter }), + _configuration.ConvertReplaceParameters(memberMap.Condition, + [customSource, _destination, mappedMemberVariable, destinationMemberGetter, ContextParameter]), mapperExpr)); } else if (!destinationMemberReadOnly) { _expressions.Add(mapperExpr); } + if (memberMap.PreCondition != null) { Precondition(memberMap, customSource); } + return Block(_variables, _expressions); } - Expression DestinationMemberValue(MemberMap memberMap, Expression destinationMemberGetter, bool destinationMemberReadOnly) + + Expression DestinationMemberValue(MemberMap memberMap, Expression destinationMemberGetter, + bool destinationMemberReadOnly) { if (destinationMemberReadOnly || memberMap.UseDestinationValue is true) { return destinationMemberGetter; } + var defaultValue = _configuration.Default(memberMap.DestinationType); - return DestinationType.IsValueType ? defaultValue : Condition(ReferenceEqual(_initialDestination, Null), defaultValue, destinationMemberGetter); + return DestinationType.IsValueType + ? defaultValue + : Condition(ReferenceEqual(_initialDestination, Null), defaultValue, destinationMemberGetter); } + void Precondition(MemberMap memberMap, ParameterExpression customSource) { - var preCondition = _configuration.ConvertReplaceParameters(memberMap.PreCondition, GetParameters(first: customSource)); + var preCondition = + _configuration.ConvertReplaceParameters(memberMap.PreCondition, GetParameters(first: customSource)); var ifThen = IfThen(preCondition, Block(_expressions)); _expressions.Clear(); _expressions.Add(ifThen); } - ParameterExpression SetVariables(Expression valueResolver, ParameterExpression resolvedValueVariable, Expression mappedMember) + + ParameterExpression SetVariables(Expression valueResolver, ParameterExpression resolvedValueVariable, + Expression mappedMember) { _expressions.Clear(); _variables.Clear(); @@ -403,20 +509,26 @@ ParameterExpression SetVariables(Expression valueResolver, ParameterExpression r _variables.Add(mappedMemberVariable); _expressions.Add(Assign(mappedMemberVariable, mappedMember)); } + return mappedMemberVariable; } + Expression MapMember(MemberMap memberMap, ParameterExpression resolvedValue, Expression destinationMemberValue) { var typePair = memberMap.Types(); var profile = _typeMap.Profile; - var mapMember = memberMap.Inline ? - _configuration.MapExpression(profile, typePair, resolvedValue, memberMap, destinationMemberValue) : - _configuration.NullCheckSource(profile, resolvedValue, destinationMemberValue, ContextMap(typePair, resolvedValue, destinationMemberValue, memberMap), memberMap); + var mapMember = memberMap.Inline + ? _configuration.MapExpression(profile, typePair, resolvedValue, memberMap, destinationMemberValue) + : _configuration.NullCheckSource(profile, resolvedValue, destinationMemberValue, + ContextMap(typePair, resolvedValue, destinationMemberValue, memberMap), memberMap); return memberMap.ApplyTransformers(mapMember, _configuration); } + private Expression BuildValueResolverFunc(MemberMap memberMap, Expression customSource, Expression destValueExpr) { - var valueResolverFunc = memberMap.Resolver?.GetExpression(_configuration, memberMap, customSource, _destination, destValueExpr) ?? destValueExpr; + var valueResolverFunc = + memberMap.Resolver?.GetExpression(_configuration, memberMap, customSource, _destination, destValueExpr) ?? + destValueExpr; if (memberMap.NullSubstitute != null) { valueResolverFunc = memberMap.NullSubstitute(valueResolverFunc); @@ -430,51 +542,82 @@ private Expression BuildValueResolverFunc(MemberMap memberMap, Expression custom valueResolverFunc = Coalesce(valueResolverFunc, ToType(ctor, valueResolverFunc.Type)); } } + return valueResolverFunc; } + private ParameterExpression GetCustomSource(MemberMap memberMap) => memberMap.IncludedMember?.Variable ?? _source; } + public interface IValueResolver { - Expression GetExpression(IGlobalConfiguration configuration, MemberMap memberMap, Expression source, Expression destination, Expression destinationMember); + Expression GetExpression(IGlobalConfiguration configuration, MemberMap memberMap, Expression source, + Expression destination, Expression destinationMember); + MemberInfo GetSourceMember(MemberMap memberMap); Type ResolvedType { get; } +#if NET8_0_OR_GREATER string SourceMemberName => null; LambdaExpression ProjectToExpression => null; IValueResolver CloseGenerics(TypeMap typeMap) => this; +#else + string SourceMemberName { get; } + LambdaExpression ProjectToExpression { get; } + IValueResolver CloseGenerics(TypeMap typeMap); +#endif } -public class MemberPathResolver : IValueResolver + +public class MemberPathResolver(MemberInfo[] members) : IValueResolver { - private readonly MemberInfo[] _members; - public MemberPathResolver(MemberInfo[] members) => _members = members; + private readonly MemberInfo[] _members = members; public Type ResolvedType => _members?[^1].GetMemberType(); - public Expression GetExpression(IGlobalConfiguration configuration, MemberMap memberMap, Expression source, Expression destination, Expression destinationMember) + + public Expression GetExpression(IGlobalConfiguration configuration, MemberMap memberMap, Expression source, + Expression destination, Expression destinationMember) { var expression = _members.Chain(source); - return memberMap.IncludedMember == null && _members.Length < 2 ? expression : expression.NullCheck(configuration, memberMap, destinationMember); + return memberMap.IncludedMember == null && _members.Length < 2 + ? expression + : expression.NullCheck(configuration, memberMap, destinationMember); } + public MemberInfo GetSourceMember(MemberMap memberMap) => _members.Length == 1 ? _members[0] : null; public LambdaExpression ProjectToExpression => _members.Lambda(); - public IValueResolver CloseGenerics(TypeMap typeMap) => _members[0].DeclaringType.ContainsGenericParameters ? - new MemberPathResolver(ReflectionHelper.GetMemberPath(typeMap.SourceType, Array.ConvertAll(_members, m => m.Name), typeMap)) : this; + + public IValueResolver CloseGenerics(TypeMap typeMap) => _members[0].DeclaringType.ContainsGenericParameters + ? new MemberPathResolver(ReflectionHelper.GetMemberPath(typeMap.SourceType, + Array.ConvertAll(_members, m => m.Name), typeMap)) + : this; + +#if FULL_OR_STANDARD + public string SourceMemberName => null; +#endif } -public abstract class LambdaValueResolver + +public abstract class LambdaValueResolver(LambdaExpression lambda) { - public LambdaExpression Lambda { get; } + public LambdaExpression Lambda { get; } = lambda; public Type ResolvedType => Lambda.ReturnType; - protected LambdaValueResolver(LambdaExpression lambda) => Lambda = lambda; } -public class FuncResolver : LambdaValueResolver, IValueResolver + +public class FuncResolver(LambdaExpression lambda) : LambdaValueResolver(lambda), IValueResolver { - public FuncResolver(LambdaExpression lambda) : base(lambda) { } - public Expression GetExpression(IGlobalConfiguration configuration, MemberMap memberMap, Expression source, Expression destination, Expression destinationMember) => - configuration.ConvertReplaceParameters(Lambda, new[] { source, destination, destinationMember, ContextParameter }); + public Expression GetExpression(IGlobalConfiguration configuration, MemberMap memberMap, Expression source, + Expression destination, Expression destinationMember) => + configuration.ConvertReplaceParameters(Lambda, [source, destination, destinationMember, ContextParameter]); + public MemberInfo GetSourceMember(MemberMap _) => null; +#if FULL_OR_STANDARD + public string SourceMemberName => null; + public LambdaExpression ProjectToExpression => null; + public IValueResolver CloseGenerics(TypeMap typeMap) => this; +#endif } -public class ExpressionResolver : LambdaValueResolver, IValueResolver + +public class ExpressionResolver(LambdaExpression lambda) : LambdaValueResolver(lambda), IValueResolver { - public ExpressionResolver(LambdaExpression lambda) : base(lambda) { } - public Expression GetExpression(IGlobalConfiguration configuration, MemberMap memberMap, Expression source, Expression _, Expression destinationMember) + public Expression GetExpression(IGlobalConfiguration configuration, MemberMap memberMap, Expression source, + Expression _, Expression destinationMember) { var mapFrom = configuration.ReplaceParameters(Lambda, source); var nullCheckedExpression = mapFrom.NullCheck(configuration, memberMap, destinationMember); @@ -482,62 +625,103 @@ public Expression GetExpression(IGlobalConfiguration configuration, MemberMap me { return nullCheckedExpression; } + var defaultExpression = configuration.Default(mapFrom.Type); - return TryCatch(mapFrom, Catch(typeof(NullReferenceException), defaultExpression), Catch(typeof(ArgumentNullException), defaultExpression)); + return TryCatch(mapFrom, Catch(typeof(NullReferenceException), defaultExpression), + Catch(typeof(ArgumentNullException), defaultExpression)); } + public MemberInfo GetSourceMember(MemberMap _) => Lambda.GetMember(); public LambdaExpression ProjectToExpression => Lambda; + +#if FULL_OR_STANDARD + public string SourceMemberName => null; + public IValueResolver CloseGenerics(TypeMap typeMap) => this; +#endif } -public abstract class ValueResolverConfig + +[EditorBrowsable(EditorBrowsableState.Never)] +public abstract class ValueResolverConfig( + Type concreteType, + Type interfaceType, + Expression instance, + string sourceMemberName) { - private protected readonly Expression _instance; - public Type ConcreteType { get; } - public Type InterfaceType { get; } + private protected readonly Expression _instance = instance; + public Type ConcreteType { get; } = concreteType; + public Type InterfaceType { get; } = interfaceType; public LambdaExpression SourceMemberLambda { get; init; } - protected ValueResolverConfig(Type concreteType, Type interfaceType, Expression instance = null) - { - ConcreteType = concreteType; - InterfaceType = interfaceType; - _instance = instance; - } - protected ValueResolverConfig(object instance, Type interfaceType) + + protected ValueResolverConfig(object instance, Type interfaceType, string sourceMemberName) : this(null, + interfaceType, Constant(instance), sourceMemberName) { - _instance = Constant(instance); - InterfaceType = interfaceType; } - public string SourceMemberName { get; init; } + + public string SourceMemberName { get; } = sourceMemberName; public Type ResolvedType => InterfaceType.GenericTypeArguments[^1]; } + +[EditorBrowsable(EditorBrowsableState.Never)] public class ValueConverter : ValueResolverConfig, IValueResolver { - public ValueConverter(Type concreteType, Type interfaceType) : base(concreteType, interfaceType, ServiceLocator(concreteType)) { } - public ValueConverter(object instance, Type interfaceType) : base(instance, interfaceType) { } - public Expression GetExpression(IGlobalConfiguration configuration, MemberMap memberMap, Expression source, Expression _, Expression destinationMember) + public ValueConverter(Type concreteType, Type interfaceType, string sourceMemberName) : base(concreteType, + interfaceType, ServiceLocator(concreteType), sourceMemberName) + { + } + + public ValueConverter(object instance, Type interfaceType, string sourceMemberName) : base(instance, interfaceType, + sourceMemberName) + { + } + + public Expression GetExpression(IGlobalConfiguration configuration, MemberMap memberMap, Expression source, + Expression _, Expression destinationMember) { var sourceMemberType = InterfaceType.GenericTypeArguments[0]; var sourceMember = this switch { { SourceMemberLambda: { } } => configuration.ReplaceParameters(SourceMemberLambda, source), { SourceMemberName: { } } => PropertyOrField(source, SourceMemberName), - _ when memberMap.SourceMembers.Length > 0 => memberMap.ChainSourceMembers(configuration, source, destinationMember), + _ when memberMap.SourceMembers.Length > 0 => memberMap.ChainSourceMembers(configuration, source, + destinationMember), _ => Throw(Constant(BuildExceptionMessage()), sourceMemberType) }; - return Call(ToType(_instance, InterfaceType), InterfaceType.GetMethod("Convert"), ToType(sourceMember, sourceMemberType), ContextParameter); + return Call(ToType(_instance, InterfaceType), InterfaceType.GetMethod("Convert"), + ToType(sourceMember, sourceMemberType), ContextParameter); + AutoMapperConfigurationException BuildExceptionMessage() - => new($"Cannot find a source member to pass to the value converter of type {ConcreteType}. Configure a source member to map from."); + => new( + $"Cannot find a source member to pass to the value converter of type {ConcreteType}. Configure a source member to map from."); } + public MemberInfo GetSourceMember(MemberMap memberMap) => this switch { { SourceMemberLambda: { } lambda } => lambda.GetMember(), { SourceMemberName: { } } => null, _ => memberMap.SourceMembers.Length == 1 ? memberMap.SourceMembers[0] : null }; + +#if FULL_OR_STANDARD + public LambdaExpression ProjectToExpression => null; + public IValueResolver CloseGenerics(TypeMap typeMap) => this; +#endif } + +[EditorBrowsable(EditorBrowsableState.Never)] public class ClassValueResolver : ValueResolverConfig, IValueResolver { - public ClassValueResolver(Type concreteType, Type interfaceType) : base(concreteType, interfaceType) { } - public ClassValueResolver(object instance, Type interfaceType) : base(instance, interfaceType) { } - public Expression GetExpression(IGlobalConfiguration configuration, MemberMap memberMap, Expression source, Expression destination, Expression destinationMember) + public ClassValueResolver(Type concreteType, Type interfaceType, string sourceMemberName = null) : base( + concreteType, interfaceType, null, sourceMemberName) + { + } + + public ClassValueResolver(object instance, Type interfaceType, string sourceMemberName = null) : base(instance, + interfaceType, sourceMemberName) + { + } + + public Expression GetExpression(IGlobalConfiguration configuration, MemberMap memberMap, Expression source, + Expression destination, Expression destinationMember) { var typeMap = memberMap.TypeMap; var resolverInstance = _instance ?? ServiceLocator(typeMap.MakeGenericType(ConcreteType)); @@ -550,56 +734,79 @@ public Expression GetExpression(IGlobalConfiguration configuration, MemberMap me { sourceMember = configuration.ReplaceParameters(SourceMemberLambda, source); } + var iValueResolver = InterfaceType; if (iValueResolver.ContainsGenericParameters) { var typeArgs = - iValueResolver.GenericTypeArguments.Zip(new[] { typeMap.SourceType, typeMap.DestinationType, sourceMember?.Type, destinationMember.Type }.Where(t => t != null), - (declaredType, runtimeType) => declaredType.ContainsGenericParameters ? runtimeType : declaredType).ToArray(); + iValueResolver.GenericTypeArguments.Zip( + new[] + { + typeMap.SourceType, typeMap.DestinationType, sourceMember?.Type, destinationMember.Type + } + .Where(t => t != null), + (declaredType, runtimeType) => + declaredType.ContainsGenericParameters ? runtimeType : declaredType) + .ToArray(); iValueResolver = iValueResolver.GetGenericTypeDefinition().MakeGenericType(typeArgs); } + var parameters = new[] { source, destination, sourceMember, destinationMember }.Where(p => p != null) .Zip(iValueResolver.GenericTypeArguments, ToType) .Append(ContextParameter) .ToArray(); return Call(ToType(resolverInstance, iValueResolver), "Resolve", parameters); } + public MemberInfo GetSourceMember(MemberMap _) => SourceMemberLambda?.GetMember(); + +#if FULL_OR_STANDARD + public LambdaExpression ProjectToExpression => null; + public IValueResolver CloseGenerics(TypeMap typeMap) => this; +#endif } + public abstract class TypeConverter { public abstract Expression GetExpression(IGlobalConfiguration configuration, ParameterExpression[] parameters); - public virtual void CloseGenerics(TypeMapConfiguration openMapConfig, TypePair closedTypes) { } + + public virtual void CloseGenerics(TypeMapConfiguration openMapConfig, TypePair closedTypes) + { + } + public virtual LambdaExpression ProjectToExpression => null; } -public class LambdaTypeConverter : TypeConverter + +public class LambdaTypeConverter(LambdaExpression lambda) : TypeConverter { - public LambdaTypeConverter(LambdaExpression lambda) => Lambda = lambda; - public LambdaExpression Lambda { get; } - public override Expression GetExpression(IGlobalConfiguration configuration, ParameterExpression[] parameters) => + public LambdaExpression Lambda { get; } = lambda; + + public override Expression GetExpression(IGlobalConfiguration configuration, ParameterExpression[] parameters) => configuration.ConvertReplaceParameters(Lambda, parameters); } -public class ExpressionTypeConverter : LambdaTypeConverter + +public class ExpressionTypeConverter(LambdaExpression lambda) : LambdaTypeConverter(lambda) { - public ExpressionTypeConverter(LambdaExpression lambda) : base(lambda){} public override LambdaExpression ProjectToExpression => Lambda; } -public class ClassTypeConverter : TypeConverter + +public class ClassTypeConverter(Type converterType, Type converterInterface) : TypeConverter { - public ClassTypeConverter(Type converterType, Type converterInterface) - { - ConverterType = converterType; - ConverterInterface = converterInterface; - } - public Type ConverterType { get; private set; } - public Type ConverterInterface { get; } + public Type ConverterType { get; private set; } = converterType; + public Type ConverterInterface { get; } = converterInterface; + public override Expression GetExpression(IGlobalConfiguration configuration, ParameterExpression[] parameters) => Call(ToType(ServiceLocator(ConverterType), ConverterInterface), "Convert", parameters); + public override void CloseGenerics(TypeMapConfiguration openMapConfig, TypePair closedTypes) { - var typeParams = (openMapConfig.SourceType.IsGenericTypeDefinition ? closedTypes.SourceType.GenericTypeArguments : Type.EmptyTypes) - .Concat(openMapConfig.DestinationType.IsGenericTypeDefinition ? closedTypes.DestinationType.GenericTypeArguments : Type.EmptyTypes); + var typeParams = (openMapConfig.SourceType.IsGenericTypeDefinition + ? closedTypes.SourceType.GenericTypeArguments + : []) + .Concat(openMapConfig.DestinationType.IsGenericTypeDefinition + ? closedTypes.DestinationType.GenericTypeArguments + : []); var neededParameters = ConverterType.GenericParametersCount(); - ConverterType = ConverterType.MakeGenericType(typeParams.Take(neededParameters).ToArray()); + ConverterType = ConverterType.MakeGenericType([..typeParams.Take(neededParameters)]); } } \ No newline at end of file diff --git a/src/AutoMapper/Features.cs b/src/AutoMapper/Features.cs index 5af0626f09..d2672bf5f1 100644 --- a/src/AutoMapper/Features.cs +++ b/src/AutoMapper/Features.cs @@ -39,7 +39,7 @@ public void Set(TFeature feature) } else { - _features ??= new(); + _features ??= []; _features.Add(feature); } } diff --git a/src/AutoMapper/Internal/InternalApi.cs b/src/AutoMapper/Internal/InternalApi.cs index 5be71f4b05..6f5e030c3f 100644 --- a/src/AutoMapper/Internal/InternalApi.cs +++ b/src/AutoMapper/Internal/InternalApi.cs @@ -2,6 +2,7 @@ using AutoMapper.Features; using AutoMapper.Internal.Mappers; using AutoMapper.QueryableExtensions.Impl; +using Microsoft.Extensions.DependencyInjection; namespace AutoMapper.Internal; @@ -32,11 +33,6 @@ public interface IGlobalConfigurationExpression : IMapperConfigurationExpression /// the validation callback void Validator(Validator validator); /// - /// Allow the same map to exist in different profiles. - /// The default is to throw an exception, true means the maps are merged. - /// - bool AllowAdditiveTypeMapCreation { get; set; } - /// /// How many levels deep should AutoMapper try to inline the execution plan for child classes. /// See the docs for details. /// @@ -158,6 +154,8 @@ public interface IGlobalConfiguration : IConfigurationProvider DefaultExpression GetDefault(Type type); ParameterReplaceVisitor ParameterReplaceVisitor(); ConvertParameterReplaceVisitor ConvertParameterReplaceVisitor(); + + string LicenseKey { get; } } [EditorBrowsable(EditorBrowsableState.Never)] public interface IProfileExpressionInternal : IProfileExpression diff --git a/src/AutoMapper/Internal/LockingConcurrentDictionary.cs b/src/AutoMapper/Internal/LockingConcurrentDictionary.cs index f88b58055f..5570e5b84c 100644 --- a/src/AutoMapper/Internal/LockingConcurrentDictionary.cs +++ b/src/AutoMapper/Internal/LockingConcurrentDictionary.cs @@ -1,14 +1,9 @@ using System.Collections.Concurrent; namespace AutoMapper.Internal; -public readonly struct LockingConcurrentDictionary +public readonly struct LockingConcurrentDictionary(Func valueFactory, int capacity = 31) { - private readonly Func> _valueFactory; - private readonly ConcurrentDictionary> _dictionary; - public LockingConcurrentDictionary(Func valueFactory, int capacity = 31) - { - _valueFactory = key => new(()=>valueFactory(key)); - _dictionary = new(Environment.ProcessorCount, capacity); - } + private readonly Func> _valueFactory = key => new(() => valueFactory(key)); + private readonly ConcurrentDictionary> _dictionary = new(Environment.ProcessorCount, capacity); public TValue GetOrAdd(in TKey key) => _dictionary.GetOrAdd(key, _valueFactory).Value; public bool IsDefault => _dictionary == null; } \ No newline at end of file diff --git a/src/AutoMapper/Internal/MemberPath.cs b/src/AutoMapper/Internal/MemberPath.cs index 5c87b3c43c..697060455a 100644 --- a/src/AutoMapper/Internal/MemberPath.cs +++ b/src/AutoMapper/Internal/MemberPath.cs @@ -2,7 +2,7 @@ [EditorBrowsable(EditorBrowsableState.Never)] public readonly record struct MemberPath(MemberInfo[] Members) { - public static readonly MemberPath Empty = new(Array.Empty()); + public static readonly MemberPath Empty = new(Members: []); public MemberPath(Stack members) : this(members.ToMemberInfos()){} public MemberInfo Last => Members[^1]; public MemberInfo First => Members[0]; @@ -10,7 +10,7 @@ public MemberPath(Stack members) : this(members.ToMemberInfos()){} public bool Equals(MemberPath other) => Members.SequenceEqual(other.Members); public override int GetHashCode() { - var hashCode = new HashCode(); + HashCode hashCode = new(); foreach(var member in Members) { hashCode.Add(member); @@ -33,5 +33,5 @@ public bool StartsWith(MemberPath path) } return true; } - public MemberPath Concat(IEnumerable memberInfos) => new(Members.Concat(memberInfos).ToArray()); + public MemberPath Concat(IEnumerable memberInfos) => new([..Members.Concat(memberInfos)]); } \ No newline at end of file diff --git a/src/AutoMapper/Internal/Polyfill.cs b/src/AutoMapper/Internal/Polyfill.cs new file mode 100644 index 0000000000..48290f321f --- /dev/null +++ b/src/AutoMapper/Internal/Polyfill.cs @@ -0,0 +1,112 @@ +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +#if FULL_OR_STANDARD +namespace AutoMapper; + +internal static class Polyfill +{ + public static bool TryAdd(this IDictionary dict, TKey key, TValue value) + { + if (dict.ContainsKey(key)) + return false; + + dict.Add(key, value); + + return true; + } + + public static TValue GetValueOrDefault(this IReadOnlyDictionary dict, TKey key) + => dict.TryGetValue(key, out var value) ? value : default; + + public static void TrimExcess(this Dictionary dict) + { + // No-op on .NET Standard 2.0 + } + + public static TValue GetOrAdd(this ConcurrentDictionary dict, TKey key, Func valueFactory, TArg factoryArgument) + { + if (key is null) + { + throw new System.ArgumentNullException(nameof(key)); + } + + if (valueFactory is null) + { + throw new System.ArgumentNullException(nameof(valueFactory)); + } + + if (!dict.TryGetValue(key, out var value)) + { + value = valueFactory(key, factoryArgument); + dict.TryAdd(key, value); + } + + return value; + } + + + public static IEnumerable DistinctBy(this IEnumerable source, Func keySelector) + => DistinctBy(source, keySelector, null); + + public static IEnumerable DistinctBy(this IEnumerable source, Func keySelector, IEqualityComparer comparer) + { + if (source is null) + { + throw new System.ArgumentNullException(nameof(source)); + } + if (keySelector is null) + { + throw new System.ArgumentNullException(nameof(keySelector)); + } + + if (IsEmptyArray(source)) + { + return []; + } + + return DistinctByIterator(source, keySelector, comparer); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsEmptyArray(IEnumerable source) => + source is TSource[] { Length: 0 }; + + private static IEnumerable DistinctByIterator(IEnumerable source, Func keySelector, IEqualityComparer comparer) + { + using IEnumerator enumerator = source.GetEnumerator(); + + if (enumerator.MoveNext()) + { + var set = new HashSet(comparer); + do + { + TSource element = enumerator.Current; + if (set.Add(keySelector(element))) + { + yield return element; + } + } + while (enumerator.MoveNext()); + } + } +} + +internal static class ArgumentNullException +{ + public static void ThrowIfNull([NotNull] object argument, + [CallerArgumentExpression(nameof(argument))] string paramName = null) + { + if (argument is null) + { + Throw(paramName); + } + } + + [DoesNotReturn] + internal static void Throw(string paramName) => + throw new System.ArgumentNullException(paramName); +} + +#endif \ No newline at end of file diff --git a/src/AutoMapper/Internal/PrimitiveHelper.cs b/src/AutoMapper/Internal/PrimitiveHelper.cs index 7f1dd6b922..f71720b898 100644 --- a/src/AutoMapper/Internal/PrimitiveHelper.cs +++ b/src/AutoMapper/Internal/PrimitiveHelper.cs @@ -23,7 +23,7 @@ public static List TryAdd(this List list, IEnumerable values) return list; } public static ReadOnlyCollection ToReadOnly(this T item) where T : Expression => new ReadOnlyCollectionBuilder{ item }.ToReadOnlyCollection(); - public static IReadOnlyCollection NullCheck(this IReadOnlyCollection source) => source ?? Array.Empty(); + public static IReadOnlyCollection NullCheck(this IReadOnlyCollection source) => source ?? []; public static IEnumerable Concat(this IReadOnlyCollection collection, IReadOnlyCollection otherCollection) { if (otherCollection == null || otherCollection.Count == 0) diff --git a/src/AutoMapper/Internal/ReflectionHelper.cs b/src/AutoMapper/Internal/ReflectionHelper.cs index 8035ff856b..e09077ec86 100644 --- a/src/AutoMapper/Internal/ReflectionHelper.cs +++ b/src/AutoMapper/Internal/ReflectionHelper.cs @@ -52,7 +52,7 @@ public static MemberInfo[] GetMemberPath(Type type, string[] memberNames, TypeMa var sourceDetails = typeMap?.SourceTypeDetails; if (sourceDetails != null && memberNames.Length == 1) { - return new[] { sourceDetails.GetMember(memberNames[0]) }; + return [sourceDetails.GetMember(memberNames[0])]; } var members = new MemberInfo[memberNames.Length]; Type previousType = type; @@ -88,7 +88,7 @@ public static MemberInfo FindProperty(LambdaExpression lambdaExpression) { switch (expressionToCheck) { - case MemberExpression { Member: var member, Expression: { NodeType: ExpressionType.Parameter or ExpressionType.Convert } }: + case MemberExpression { Member: var member, Expression.NodeType: ExpressionType.Parameter or ExpressionType.Convert }: return member; case UnaryExpression { Operand: var operand }: expressionToCheck = operand; @@ -105,7 +105,7 @@ public static MemberInfo FindProperty(LambdaExpression lambdaExpression) PropertyInfo property => property.PropertyType, MethodInfo method => method.ReturnType, FieldInfo field => field.FieldType, - null => throw new ArgumentNullException(nameof(member)), + null => throw new System.ArgumentNullException(nameof(member)), _ => throw new ArgumentOutOfRangeException(nameof(member)) }; } \ No newline at end of file diff --git a/src/AutoMapper/Internal/TypeDetails.cs b/src/AutoMapper/Internal/TypeDetails.cs index b69b2f226c..d0d3cdb004 100644 --- a/src/AutoMapper/Internal/TypeDetails.cs +++ b/src/AutoMapper/Internal/TypeDetails.cs @@ -4,17 +4,14 @@ namespace AutoMapper.Internal; /// [DebuggerDisplay("{Type}")] [EditorBrowsable(EditorBrowsableState.Never)] -public class TypeDetails +public sealed class TypeDetails(Type type, ProfileMap config) { + public Type Type { get; } = type; + public ProfileMap Config { get; } = config; private Dictionary _nameToMember; private ConstructorParameters[] _constructors; private MemberInfo[] _readAccessors; private MemberInfo[] _writeAccessors; - public TypeDetails(Type type, ProfileMap config) - { - Type = type; - Config = config; - } private ConstructorParameters[] GetConstructors() => GetConstructors(Type, Config).Where(c=>c.ParametersCount > 0).OrderByDescending(c => c.ParametersCount).ToArray(); public static IEnumerable GetConstructors(Type type, ProfileMap profileMap) => @@ -88,16 +85,11 @@ where targetType.IsInterface && targetType.ContainsGenericParameters select new GenericMethod(method, genericInterface)); } } - class GenericMethod : MemberInfo + sealed class GenericMethod(MethodInfo genericMethod, Type genericInterface) : MemberInfo { - readonly MethodInfo _genericMethod; - readonly Type _genericInterface; + readonly MethodInfo _genericMethod = genericMethod; + readonly Type _genericInterface = genericInterface; MethodInfo _closedMethod = ObjectToString; - public GenericMethod(MethodInfo genericMethod, Type genericInterface) - { - _genericMethod = genericMethod; - _genericInterface = genericInterface; - } public MethodInfo Close() { if (_closedMethod == ObjectToString) @@ -135,12 +127,12 @@ public static string[] PossibleNames(string memberName, List prefixes, L continue; } var withoutPrefix = memberName[prefix.Length..]; - result ??= new(); + result ??= []; result.Add(withoutPrefix); PostFixes(ref result, postfixes, withoutPrefix); } PostFixes(ref result, postfixes, memberName); - return result == null ? Array.Empty() : result.ToArray(); + return result == null ? [] : [..result]; static void PostFixes(ref List result, List postfixes, string name) { foreach (var postfix in postfixes) @@ -149,13 +141,11 @@ static void PostFixes(ref List result, List postfixes, string na { continue; } - result ??= new(); + result ??= []; result.Add(name[..^postfix.Length]); } } } - public Type Type { get; } - public ProfileMap Config { get; } public MemberInfo[] ReadAccessors => _readAccessors ??= BuildReadAccessors(); public MemberInfo[] WriteAccessors => _writeAccessors ??= BuildWriteAccessors(); public ConstructorParameters[] Constructors => _constructors ??= GetConstructors(); @@ -169,7 +159,7 @@ private MemberInfo[] BuildReadAccessors() { members = members.Concat(GetFields(FieldReadable)); } - return members.ToArray(); + return [..members]; } private MemberInfo[] BuildWriteAccessors() { @@ -181,7 +171,7 @@ private MemberInfo[] BuildWriteAccessors() { members = members.Concat(GetFields(FieldWritable)); } - return members.ToArray(); + return [..members]; } private static bool PropertyReadable(PropertyInfo propertyInfo) => propertyInfo.CanRead; private static bool FieldReadable(FieldInfo fieldInfo) => true; diff --git a/src/AutoMapper/Internal/TypePair.cs b/src/AutoMapper/Internal/TypePair.cs index 039ebb78a5..5cd2506573 100644 --- a/src/AutoMapper/Internal/TypePair.cs +++ b/src/AutoMapper/Internal/TypePair.cs @@ -1,8 +1,8 @@ namespace AutoMapper.Internal; - [DebuggerDisplay("{RequestedTypes.SourceType.Name}, {RequestedTypes.DestinationType.Name} : {RuntimeTypes.SourceType.Name}, {RuntimeTypes.DestinationType.Name}")] public readonly record struct MapRequest(TypePair RequestedTypes, TypePair RuntimeTypes, MemberMap MemberMap) { + public MapRequest(TypePair types) : this(types, types, MemberMap.Instance) { } public bool Equals(MapRequest other) => RequestedTypes.Equals(other.RequestedTypes) && RuntimeTypes.Equals(other.RuntimeTypes); public override int GetHashCode() => HashCode.Combine(RequestedTypes, RuntimeTypes); } @@ -25,9 +25,10 @@ public TypePair CloseGenericTypes(TypePair closedTypes) } var closedSourceType = SourceType.IsGenericTypeDefinition ? SourceType.MakeGenericType(sourceArguments) : SourceType; var closedDestinationType = DestinationType.IsGenericTypeDefinition ? DestinationType.MakeGenericType(destinationArguments) : DestinationType; - return new TypePair(closedSourceType, closedDestinationType); + return new(closedSourceType, closedDestinationType); } + public TypePair Reverse() => new(DestinationType, SourceType); public Type ITypeConverter() => ContainsGenericParameters ? null : typeof(ITypeConverter<,>).MakeGenericType(SourceType, DestinationType); public TypePair GetTypeDefinitionIfGeneric() => new(GetTypeDefinitionIfGeneric(SourceType), GetTypeDefinitionIfGeneric(DestinationType)); - private static Type GetTypeDefinitionIfGeneric(Type type) => type.IsGenericType ? type.GetGenericTypeDefinition() : type; + static Type GetTypeDefinitionIfGeneric(Type type) => type.IsGenericType ? type.GetGenericTypeDefinition() : type; } \ No newline at end of file diff --git a/src/AutoMapper/Licensing/Edition.cs b/src/AutoMapper/Licensing/Edition.cs new file mode 100644 index 0000000000..7fb62f3123 --- /dev/null +++ b/src/AutoMapper/Licensing/Edition.cs @@ -0,0 +1,9 @@ +namespace AutoMapper.Licensing; + +internal enum Edition +{ + Community = 0, + Standard = 1, + Professional = 2, + Enterprise = 3 +} \ No newline at end of file diff --git a/src/AutoMapper/Licensing/License.cs b/src/AutoMapper/Licensing/License.cs new file mode 100644 index 0000000000..abeab9ae9d --- /dev/null +++ b/src/AutoMapper/Licensing/License.cs @@ -0,0 +1,62 @@ +using System.Security.Claims; + +namespace AutoMapper.Licensing; + +internal class License +{ + internal License(params Claim[] claims) : this(new ClaimsPrincipal(new ClaimsIdentity(claims))) + { + + } + + public License(ClaimsPrincipal claims) + { + if (Guid.TryParse(claims.FindFirst("account_id")?.Value, out var accountId)) + { + AccountId = accountId; + } + + CustomerId = claims.FindFirst("customer_id")?.Value; + SubscriptionId = claims.FindFirst("sub_id")?.Value; + + if (long.TryParse(claims.FindFirst("iat")?.Value, out var iat)) + { + var startedAt = DateTimeOffset.FromUnixTimeSeconds(iat); + StartDate = startedAt; + } + + if (long.TryParse(claims.FindFirst("exp")?.Value, out var exp)) + { + var expiredAt = DateTimeOffset.FromUnixTimeSeconds(exp); + ExpirationDate = expiredAt; + } + + if (Enum.TryParse(claims.FindFirst("edition")?.Value, out var edition)) + { + Edition = edition; + } + + if (Enum.TryParse(claims.FindFirst("type")?.Value, out var productType)) + { + ProductType = productType; + } + + IsConfigured = AccountId != null + && CustomerId != null + && SubscriptionId != null + && StartDate != null + && ExpirationDate != null + && Edition != null + && ProductType != null; + } + + public Guid? AccountId { get; } + public string CustomerId { get; } + public string SubscriptionId { get; } + public DateTimeOffset? StartDate { get; } + public DateTimeOffset? ExpirationDate { get; } + public Edition? Edition { get; } + public ProductType? ProductType { get; } + + public bool IsConfigured { get; } +} \ No newline at end of file diff --git a/src/AutoMapper/Licensing/LicenseAccessor.cs b/src/AutoMapper/Licensing/LicenseAccessor.cs new file mode 100644 index 0000000000..c0417d82c2 --- /dev/null +++ b/src/AutoMapper/Licensing/LicenseAccessor.cs @@ -0,0 +1,81 @@ +using System.Security.Claims; +using System.Security.Cryptography; +using Microsoft.Extensions.Logging; +using Microsoft.IdentityModel.JsonWebTokens; +using Microsoft.IdentityModel.Tokens; +using Convert = System.Convert; + +namespace AutoMapper.Licensing; + +internal class LicenseAccessor +{ + private readonly IGlobalConfiguration _configuration; + private readonly ILogger _logger; + + public LicenseAccessor(IGlobalConfiguration configuration, ILoggerFactory loggerFactory) + { + _configuration = configuration; + _logger = loggerFactory.CreateLogger("LuckyPennySoftware.AutoMapper.License"); + } + + private License _license; + private readonly object _lock = new(); + + public License Current => _license ??= Initialize(); + + private License Initialize() + { + lock (_lock) + { + if (_license != null) + { + return _license; + } + + var key = _configuration.LicenseKey; + if (key == null) + { + return new License(); + } + + var licenseClaims = ValidateKey(key); + return licenseClaims.Any() + ? new License(new ClaimsPrincipal(new ClaimsIdentity(licenseClaims))) + : new License(); + } + } + + private Claim[] ValidateKey(string licenseKey) + { + var handler = new JsonWebTokenHandler(); + + var rsa = new RSAParameters + { + Exponent = Convert.FromBase64String("AQAB"), + Modulus = Convert.FromBase64String( + "2LTtdJV2b0mYoRqChRCfcqnbpKvsiCcDYwJ+qPtvQXWXozOhGo02/V0SWMFBdbZHUzpEytIiEcojo7Vbq5mQmt4lg92auyPKsWq6qSmCVZCUuL/kpYqLCit4yUC0YqZfw4H9zLf1yAIOgyXQf1x6g+kscDo1pWAniSl9a9l/LXRVEnGz+OfeUrN/5gzpracGUY6phx6T09UCRuzi4YqqO4VJzL877W0jCW2Q7jMzHxOK04VSjNc22CADuCd34mrFs23R0vVm1DVLYtPGD76/rGOcxO6vmRc7ydBAvt1IoUsrY0vQ2rahp51YPxqqhKPd8nNOomHWblCCA7YUeV3C1Q==") + };; + + var key = new RsaSecurityKey(rsa) + { + KeyId = "LuckyPennySoftwareLicenseKey/bbb13acb59904d89b4cb1c85f088ccf9" + }; + + var parms = new TokenValidationParameters + { + ValidIssuer = "https://luckypennysoftware.com", + ValidAudience = "LuckyPennySoftware", + IssuerSigningKey = key, + ValidateLifetime = false + }; + + var validateResult = handler.ValidateTokenAsync(licenseKey, parms).Result; + if (!validateResult.IsValid) + { + _logger.LogCritical(validateResult.Exception, "Error validating the Lucky Penny software license key"); + } + + return validateResult.ClaimsIdentity?.Claims.ToArray() ?? []; + } + +} \ No newline at end of file diff --git a/src/AutoMapper/Licensing/LicenseValidator.cs b/src/AutoMapper/Licensing/LicenseValidator.cs new file mode 100644 index 0000000000..52366beaa5 --- /dev/null +++ b/src/AutoMapper/Licensing/LicenseValidator.cs @@ -0,0 +1,61 @@ +using Microsoft.Extensions.Logging; + +namespace AutoMapper.Licensing; + +internal class LicenseValidator +{ + private readonly ILogger _logger; + + public LicenseValidator(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger("LuckyPennySoftware.AutoMapper.License"); + } + + public void Validate(License license) + { + var errors = new List(); + + if (license is not { IsConfigured: true }) + { + var message = "You do not have a valid license key for the Lucky Penny software AutoMapper. " + + "This is allowed for development and testing scenarios. " + + "If you are running in production you are required to have a licensed version. " + + "Please visit https://luckypennysoftware.com to obtain a valid license."; + + _logger.LogWarning(message); + return; + } + + _logger.LogDebug("The Lucky Penny license key details: {license}", license); + + var diff = DateTime.UtcNow.Date.Subtract(license.ExpirationDate.Value.Date).TotalDays; + if (diff > 0) + { + errors.Add($"Your license for the Lucky Penny software AutoMapper expired {diff} days ago."); + } + + if (license.ProductType.Value != ProductType.AutoMapper + && license.ProductType.Value != ProductType.Bundle) + { + errors.Add("Your Lucky Penny software license does not include AutoMapper."); + } + + if (errors.Count > 0) + { + foreach (var err in errors) + { + _logger.LogError(err); + } + + _logger.LogError( + "Please visit https://luckypennysoftware.com to obtain a valid license for the Lucky Penny software AutoMapper."); + } + else + { + _logger.LogInformation("You have a valid license key for the Lucky Penny software {type} {edition} edition. The license expires on {licenseExpiration}.", + license.ProductType, + license.Edition, + license.ExpirationDate); + } + } +} \ No newline at end of file diff --git a/src/AutoMapper/Licensing/ProductType.cs b/src/AutoMapper/Licensing/ProductType.cs new file mode 100644 index 0000000000..57c0c5a0c1 --- /dev/null +++ b/src/AutoMapper/Licensing/ProductType.cs @@ -0,0 +1,8 @@ +namespace AutoMapper.Licensing; + +internal enum ProductType +{ + AutoMapper = 0, + MediatR = 1, + Bundle = 2 +} \ No newline at end of file diff --git a/src/AutoMapper/Mapper.cs b/src/AutoMapper/Mapper.cs index 960f4f78f5..a56b375786 100644 --- a/src/AutoMapper/Mapper.cs +++ b/src/AutoMapper/Mapper.cs @@ -15,7 +15,7 @@ public interface IMapperBase /// /// Execute a mapping from the source object to a new destination object. /// - /// Source type to use, regardless of the runtime type + /// Source type to use /// Destination type to create /// Source object to map from /// Mapped destination object @@ -27,7 +27,7 @@ public interface IMapperBase /// Destination type /// Source object to map from /// Destination object to map into - /// The mapped destination object, same instance as the object + /// The mapped destination object TDestination Map(TSource source, TDestination destination); /// /// Execute a mapping from the source object to a new destination object with explicit objects @@ -44,7 +44,7 @@ public interface IMapperBase /// Destination object to map into /// Source type to use /// Destination type to use - /// Mapped destination object, same instance as the object + /// The mapped destination object object Map(object source, object destination, Type sourceType, Type destinationType); } public interface IMapper : IMapperBase @@ -74,7 +74,7 @@ public interface IMapper : IMapperBase /// Source object to map from /// Destination object to map into /// Mapping options - /// The mapped destination object, same instance as the object + /// The mapped destination object TDestination Map(TSource source, TDestination destination, Action> opts); /// /// Execute a mapping from the source object to a new destination object with explicit objects and supplied mapping options. @@ -93,7 +93,7 @@ public interface IMapper : IMapperBase /// Source type to use /// Destination type to use /// Mapping options - /// Mapped destination object, same instance as the object + /// The mapped destination object object Map(object source, object destination, Type sourceType, Type destinationType, Action opts); /// /// Configuration provider for performing maps @@ -137,19 +137,21 @@ internal interface IInternalRuntimeMapper : IRuntimeMapper ResolutionContext DefaultContext { get; } Factory ServiceCtor { get; } } -public class Mapper : IMapper, IInternalRuntimeMapper +public sealed class Mapper : IMapper, IInternalRuntimeMapper { private readonly IGlobalConfiguration _configuration; private readonly Factory _serviceCtor; + private readonly ResolutionContext _defaultContext; public Mapper(IConfigurationProvider configuration) : this(configuration, configuration.Internal().ServiceCtor) { } public Mapper(IConfigurationProvider configuration, Factory serviceCtor) { - _configuration = (IGlobalConfiguration)configuration ?? throw new ArgumentNullException(nameof(configuration)); - _serviceCtor = serviceCtor ?? throw new NullReferenceException(nameof(serviceCtor)); - DefaultContext = new(this); + ArgumentNullException.ThrowIfNull(configuration); + ArgumentNullException.ThrowIfNull(serviceCtor); + _configuration = (IGlobalConfiguration)configuration; + _serviceCtor = serviceCtor; + _defaultContext = new(this); } - internal ResolutionContext DefaultContext { get; } - ResolutionContext IInternalRuntimeMapper.DefaultContext => DefaultContext; + ResolutionContext IInternalRuntimeMapper.DefaultContext => _defaultContext; Factory IInternalRuntimeMapper.ServiceCtor => _serviceCtor; public IConfigurationProvider ConfigurationProvider => _configuration; public TDestination Map(object source) => Map(source, default(TDestination)); @@ -158,14 +160,14 @@ public Mapper(IConfigurationProvider configuration, Factory serviceCtor) public TDestination Map(TSource source, Action> opts) => Map(source, default, opts); public TDestination Map(TSource source, TDestination destination) => - MapCore(source, destination, DefaultContext); + MapCore(source, destination, _defaultContext); public TDestination Map(TSource source, TDestination destination, Action> opts) => MapWithOptions(source, destination, opts); public object Map(object source, Type sourceType, Type destinationType) => Map(source, null, sourceType, destinationType); public object Map(object source, Type sourceType, Type destinationType, Action opts) => Map(source, null, sourceType, destinationType, opts); public object Map(object source, object destination, Type sourceType, Type destinationType) => - MapCore(source, destination, DefaultContext, sourceType, destinationType); + MapCore(source, destination, _defaultContext, sourceType, destinationType); public object Map(object source, object destination, Type sourceType, Type destinationType, Action opts) => MapWithOptions(source, destination, opts, sourceType, destinationType); public IQueryable ProjectTo(IQueryable source, object parameters, params Expression>[] membersToExpand) diff --git a/src/AutoMapper/Mappers/AssignableMapper.cs b/src/AutoMapper/Mappers/AssignableMapper.cs index 855a832cf0..fe0f39f6b2 100644 --- a/src/AutoMapper/Mappers/AssignableMapper.cs +++ b/src/AutoMapper/Mappers/AssignableMapper.cs @@ -1,8 +1,11 @@ namespace AutoMapper.Internal.Mappers; -public class AssignableMapper : IObjectMapper +public sealed class AssignableMapper : IObjectMapper { public bool IsMatch(TypePair context) => context.DestinationType.IsAssignableFrom(context.SourceType); public Expression MapExpression(IGlobalConfiguration configuration, ProfileMap profileMap, MemberMap memberMap, Expression sourceExpression, Expression destExpression) => sourceExpression; +#if FULL_OR_STANDARD + public TypePair? GetAssociatedTypes(TypePair initialTypes) => null; +#endif } \ No newline at end of file diff --git a/src/AutoMapper/Mappers/CollectionMapper.cs b/src/AutoMapper/Mappers/CollectionMapper.cs index 9122e6d01a..c7769b20fa 100644 --- a/src/AutoMapper/Mappers/CollectionMapper.cs +++ b/src/AutoMapper/Mappers/CollectionMapper.cs @@ -3,7 +3,7 @@ using System.Collections.Specialized; namespace AutoMapper.Internal.Mappers; using static ReflectionHelper; -public class CollectionMapper : IObjectMapper +public sealed class CollectionMapper : IObjectMapper { static readonly MethodInfo IListAdd = typeof(IList).GetMethod(nameof(IList.Add)); public TypePair? GetAssociatedTypes(TypePair context) => new(GetElementType(context.SourceType), GetElementType(context.DestinationType)); @@ -49,14 +49,12 @@ Expression MapCollectionCore(Expression destExpression) var sourceElementType = GetEnumerableElementType(sourceType); if (destinationCollectionType == null || (sourceType == sourceElementType && destinationType == destinationElementType)) { - if (destinationType.IsAssignableFrom(sourceType)) - { - return sourceExpression; - } - throw new NotSupportedException($"Unknown collection. Consider a custom type converter from {sourceType} to {destinationType}."); + return destinationType.IsAssignableFrom(sourceType) ? + sourceExpression : + Throw(Constant(new NotSupportedException($"Unknown collection. Consider a custom type converter from {sourceType} to {destinationType}.")), destinationType); } var itemParam = Parameter(sourceElementType, "item"); - var itemExpr = configuration.MapExpression(profileMap, new TypePair(sourceElementType, destinationElementType), itemParam); + var itemExpr = configuration.MapExpression(profileMap, new(sourceElementType, destinationElementType), itemParam); Expression destination, assignNewExpression; UseDestinationValue(); var (variables, statements) = configuration.Scratchpad(); @@ -109,7 +107,12 @@ void GetDestinationType() return; } destinationElementType = GetEnumerableElementType(destinationType); - destinationCollectionType = typeof(ICollection<>).MakeGenericType(destinationElementType); +#if FULL_OR_STANDARD + destinationCollectionType = destinationType.IsGenericType(typeof(ISet<>)) ? typeof(HashSet<>) : typeof(ICollection<>); +#else + destinationCollectionType = destinationType.IsGenericType(typeof(IReadOnlySet<>)) ? typeof(HashSet<>) : typeof(ICollection<>); +#endif + destinationCollectionType = destinationCollectionType.MakeGenericType(destinationElementType); destExpression = Convert(mustUseDestination ? destExpression : Null, destinationCollectionType); addMethod = destinationCollectionType.GetMethod("Add"); } @@ -142,11 +145,11 @@ Expression CheckContext() } } private static Expression CreateNameValueCollection(Expression sourceExpression) => - New(typeof(NameValueCollection).GetConstructor(new[] { typeof(NameValueCollection) }), sourceExpression); + New(typeof(NameValueCollection).GetConstructor([typeof(NameValueCollection)]), sourceExpression); static class ArrayMapper { private static readonly MethodInfo ToArrayMethod = typeof(Enumerable).GetStaticMethod("ToArray"); - private static readonly MethodInfo CopyToMethod = typeof(Array).GetMethod("CopyTo", new[] { typeof(Array), typeof(int) }); + private static readonly MethodInfo CopyToMethod = typeof(Array).GetMethod("CopyTo", [typeof(Array), typeof(int)]); private static readonly MethodInfo CountMethod = typeof(Enumerable).StaticGenericMethod("Count", parametersCount: 1); private static readonly MethodInfo MapMultidimensionalMethod = typeof(ArrayMapper).GetStaticMethod(nameof(MapMultidimensional)); private static readonly ParameterExpression Index = Variable(typeof(int), "destinationArrayIndex"); @@ -156,7 +159,7 @@ private static Array MapMultidimensional(Array source, Type destinationElementTy { var sourceElementType = source.GetType().GetElementType(); var destinationArray = Array.CreateInstance(destinationElementType, Enumerable.Range(0, source.Rank).Select(source.GetLength).ToArray()); - var filler = new MultidimensionalArrayFiller(destinationArray); + MultidimensionalArrayFiller filler = new(destinationArray); foreach (var item in source) { filler.NewValue(context.Map(item, null, sourceElementType, destinationElementType, null)); @@ -195,7 +198,7 @@ public static Expression MapToArray(IGlobalConfiguration configuration, ProfileM createDestination = Assign(destination, NewArrayBounds(destinationElementType, statements)); } var itemParam = Parameter(sourceElementType, "sourceItem"); - var itemExpr = configuration.MapExpression(profileMap, new TypePair(sourceElementType, destinationElementType), itemParam); + var itemExpr = configuration.MapExpression(profileMap, new(sourceElementType, destinationElementType), itemParam); var setItem = Assign(ArrayAccess(destination, IncrementIndex), itemExpr); variables.Clear(); statements.Clear(); @@ -240,31 +243,25 @@ bool MustMap(Type sourceType, Type destinationType) => !destinationType.IsAssign } } } -public class MultidimensionalArrayFiller +public readonly struct MultidimensionalArrayFiller(Array destination) { - private readonly int[] _indices; - private readonly Array _destination; - public MultidimensionalArrayFiller(Array destination) - { - _indices = new int[destination.Rank]; - _destination = destination; - } + readonly int[] _indices = new int[destination.Rank]; public void NewValue(object value) { - var dimension = _destination.Rank - 1; + var dimension = destination.Rank - 1; var changedDimension = false; - while (_indices[dimension] == _destination.GetLength(dimension)) + while (_indices[dimension] == destination.GetLength(dimension)) { _indices[dimension] = 0; dimension--; if (dimension < 0) { - throw new InvalidOperationException("Not enough room in destination array " + _destination); + throw new InvalidOperationException("Not enough room in destination array " + destination); } _indices[dimension]++; changedDimension = true; } - _destination.SetValue(value, _indices); + destination.SetValue(value, _indices); if (changedDimension) { _indices[dimension + 1]++; diff --git a/src/AutoMapper/Mappers/ConstructorMapper.cs b/src/AutoMapper/Mappers/ConstructorMapper.cs index 325214a724..3deac9f68f 100644 --- a/src/AutoMapper/Mappers/ConstructorMapper.cs +++ b/src/AutoMapper/Mappers/ConstructorMapper.cs @@ -1,12 +1,15 @@ namespace AutoMapper.Internal.Mappers; -public class ConstructorMapper : IObjectMapper +public sealed class ConstructorMapper : IObjectMapper { public bool IsMatch(TypePair context) => GetConstructor(context.SourceType, context.DestinationType) != null; private static ConstructorInfo GetConstructor(Type sourceType, Type destinationType) => - destinationType.GetConstructor(TypeExtensions.InstanceFlags, null, new[] { sourceType }, null); + destinationType.GetConstructor(TypeExtensions.InstanceFlags, null, [sourceType], null); public Expression MapExpression(IGlobalConfiguration configuration, ProfileMap profileMap, MemberMap memberMap, Expression sourceExpression, Expression destExpression) { var constructor = GetConstructor(sourceExpression.Type, destExpression.Type); return New(constructor, ToType(sourceExpression, constructor.FirstParameterType())); } +#if FULL_OR_STANDARD + public TypePair? GetAssociatedTypes(TypePair initialTypes) => null; +#endif } \ No newline at end of file diff --git a/src/AutoMapper/Mappers/ConversionOperatorMapper.cs b/src/AutoMapper/Mappers/ConversionOperatorMapper.cs index f5d38535ff..469343015a 100644 --- a/src/AutoMapper/Mappers/ConversionOperatorMapper.cs +++ b/src/AutoMapper/Mappers/ConversionOperatorMapper.cs @@ -1,5 +1,5 @@ namespace AutoMapper.Internal.Mappers; -public class ConversionOperatorMapper : IObjectMapper +public sealed class ConversionOperatorMapper : IObjectMapper { private readonly string _operatorName; public ConversionOperatorMapper(string operatorName) => _operatorName = operatorName; @@ -13,11 +13,14 @@ private MethodInfo GetConversionOperator(Type sourceType, Type destinationType) return sourceMethod; } } - return destinationType.GetMethod(_operatorName, TypeExtensions.StaticFlags, null, new[] { sourceType }, null); + return destinationType.GetMethod(_operatorName, TypeExtensions.StaticFlags, null, [sourceType], null); } public Expression MapExpression(IGlobalConfiguration configuration, ProfileMap profileMap, MemberMap memberMap, Expression sourceExpression, Expression destExpression) { var conversionOperator = GetConversionOperator(sourceExpression.Type, destExpression.Type); return Call(conversionOperator, ToType(sourceExpression, conversionOperator.FirstParameterType())); } +#if FULL_OR_STANDARD + public TypePair? GetAssociatedTypes(TypePair initialTypes) => null; +#endif } diff --git a/src/AutoMapper/Mappers/ConvertMapper.cs b/src/AutoMapper/Mappers/ConvertMapper.cs index 1c3536e0d2..767ec457fd 100644 --- a/src/AutoMapper/Mappers/ConvertMapper.cs +++ b/src/AutoMapper/Mappers/ConvertMapper.cs @@ -1,5 +1,5 @@ namespace AutoMapper.Internal.Mappers; -public class ConvertMapper : IObjectMapper +public sealed class ConvertMapper : IObjectMapper { public static bool IsPrimitive(Type type) => type.IsPrimitive || type == typeof(string) || type == typeof(decimal); public bool IsMatch(TypePair types) => (types.SourceType == typeof(string) && types.DestinationType == typeof(DateTime)) || @@ -7,7 +7,10 @@ public bool IsMatch(TypePair types) => (types.SourceType == typeof(string) && ty public Expression MapExpression(IGlobalConfiguration configuration, ProfileMap profileMap, MemberMap memberMap, Expression sourceExpression, Expression destExpression) { - var convertMethod = typeof(Convert).GetMethod("To" + destExpression.Type.Name, new[] { sourceExpression.Type }); + var convertMethod = typeof(Convert).GetMethod("To" + destExpression.Type.Name, [sourceExpression.Type]); return Call(convertMethod, sourceExpression); } +#if FULL_OR_STANDARD + public TypePair? GetAssociatedTypes(TypePair initialTypes) => null; +#endif } \ No newline at end of file diff --git a/src/AutoMapper/Mappers/EnumToEnumMapper.cs b/src/AutoMapper/Mappers/EnumToEnumMapper.cs index 9eba8acac9..3a6827214e 100644 --- a/src/AutoMapper/Mappers/EnumToEnumMapper.cs +++ b/src/AutoMapper/Mappers/EnumToEnumMapper.cs @@ -1,5 +1,5 @@ namespace AutoMapper.Internal.Mappers; -public class EnumToEnumMapper : IObjectMapper +public sealed class EnumToEnumMapper : IObjectMapper { private static readonly MethodInfo TryParseMethod = typeof(Enum).StaticGenericMethod("TryParse", parametersCount: 3); public bool IsMatch(TypePair context) => context.IsEnumToEnum(); @@ -16,4 +16,7 @@ public Expression MapExpression(IGlobalConfiguration configuration, ProfileMap p statements.Add(Condition(tryParse, result, Convert(sourceExpression, destinationType))); return Block(variables, statements); } +#if FULL_OR_STANDARD + public TypePair? GetAssociatedTypes(TypePair initialTypes) => null; +#endif } \ No newline at end of file diff --git a/src/AutoMapper/Mappers/FromDynamicMapper.cs b/src/AutoMapper/Mappers/FromDynamicMapper.cs index b4056bb6ce..34928c66ad 100644 --- a/src/AutoMapper/Mappers/FromDynamicMapper.cs +++ b/src/AutoMapper/Mappers/FromDynamicMapper.cs @@ -2,7 +2,7 @@ using Microsoft.CSharp.RuntimeBinder; using Binder = Microsoft.CSharp.RuntimeBinder.Binder; namespace AutoMapper.Internal.Mappers; -public class FromDynamicMapper : IObjectMapper +public sealed class FromDynamicMapper : IObjectMapper { private static readonly MethodInfo MapMethodInfo = typeof(FromDynamicMapper).GetStaticMethod(nameof(Map)); private static object Map(object source, object destination, Type destinationType, ResolutionContext context, ProfileMap profileMap) @@ -27,8 +27,7 @@ private static object Map(object source, object destination, Type destinationTyp } private static object GetDynamically(string memberName, object target) { - var binder = Binder.GetMember(CSharpBinderFlags.None, memberName, null, - new[] { CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null) }); + var binder = Binder.GetMember(CSharpBinderFlags.None, memberName, null, [CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null)]); var callsite = CallSite>.Create(binder); return callsite.Target(callsite, target); } @@ -36,4 +35,7 @@ private static object GetDynamically(string memberName, object target) public Expression MapExpression(IGlobalConfiguration configuration, ProfileMap profileMap, MemberMap memberMap, Expression sourceExpression, Expression destExpression) => Call(MapMethodInfo, sourceExpression, destExpression.ToObject(), Constant(destExpression.Type), ContextParameter, Constant(profileMap)); +#if FULL_OR_STANDARD + public TypePair? GetAssociatedTypes(TypePair initialTypes) => null; +#endif } \ No newline at end of file diff --git a/src/AutoMapper/Mappers/FromStringDictionaryMapper.cs b/src/AutoMapper/Mappers/FromStringDictionaryMapper.cs index 333d652de0..95035d420c 100644 --- a/src/AutoMapper/Mappers/FromStringDictionaryMapper.cs +++ b/src/AutoMapper/Mappers/FromStringDictionaryMapper.cs @@ -1,6 +1,6 @@ using StringDictionary = System.Collections.Generic.IDictionary; namespace AutoMapper.Internal.Mappers; -public class FromStringDictionaryMapper : IObjectMapper +public sealed class FromStringDictionaryMapper : IObjectMapper { private static readonly MethodInfo MapDynamicMethod = typeof(FromStringDictionaryMapper).GetStaticMethod(nameof(MapDynamic)); public bool IsMatch(TypePair context) => typeof(StringDictionary).IsAssignableFrom(context.SourceType); @@ -81,4 +81,7 @@ object GetInnerDestination() } } } +#if FULL_OR_STANDARD + public TypePair? GetAssociatedTypes(TypePair initialTypes) => null; +#endif } \ No newline at end of file diff --git a/src/AutoMapper/Mappers/IObjectMapper.cs b/src/AutoMapper/Mappers/IObjectMapper.cs index 93a241a7ba..a0907ccd7d 100644 --- a/src/AutoMapper/Mappers/IObjectMapper.cs +++ b/src/AutoMapper/Mappers/IObjectMapper.cs @@ -23,7 +23,11 @@ public interface IObjectMapper /// Map expression Expression MapExpression(IGlobalConfiguration configuration, ProfileMap profileMap, MemberMap memberMap, Expression sourceExpression, Expression destExpression); - TypePair? GetAssociatedTypes(TypePair initialTypes) => null; + TypePair? GetAssociatedTypes(TypePair initialTypes) +#if NET8_0_OR_GREATER + => null +#endif + ; } /// /// Base class for simple object mappers that don't want to use expressions. @@ -63,4 +67,8 @@ public Expression MapExpression(IGlobalConfiguration configuration, ProfileMap p Constant(sourceExpression.Type), Constant(destExpression.Type), ContextParameter); + +#if FULL_OR_STANDARD + public TypePair? GetAssociatedTypes(TypePair initialTypes) => null; +#endif } \ No newline at end of file diff --git a/src/AutoMapper/Mappers/KeyValueMapper.cs b/src/AutoMapper/Mappers/KeyValueMapper.cs index 70a867639c..aba9503455 100644 --- a/src/AutoMapper/Mappers/KeyValueMapper.cs +++ b/src/AutoMapper/Mappers/KeyValueMapper.cs @@ -1,5 +1,5 @@ namespace AutoMapper.Internal.Mappers; -public class KeyValueMapper : IObjectMapper +public sealed class KeyValueMapper : IObjectMapper { public bool IsMatch(TypePair context) => IsKeyValue(context.SourceType) && IsKeyValue(context.DestinationType); public static bool IsKeyValue(Type type) => type.IsGenericType(typeof(KeyValuePair<,>)); @@ -8,10 +8,13 @@ public Expression MapExpression(IGlobalConfiguration configuration, ProfileMap p var sourceArguments = sourceExpression.Type.GenericTypeArguments; var destinationType = destExpression.Type; var destinationArguments = destinationType.GenericTypeArguments; - var keys = new TypePair(sourceArguments[0], destinationArguments[0]); - var values = new TypePair(sourceArguments[1], destinationArguments[1]); + TypePair keys = new(sourceArguments[0], destinationArguments[0]); + TypePair values = new(sourceArguments[1], destinationArguments[1]); var mapKeys = configuration.MapExpression(profileMap, keys, ExpressionBuilder.Property(sourceExpression, "Key")); var mapValues = configuration.MapExpression(profileMap, values, ExpressionBuilder.Property(sourceExpression, "Value")); return New(destinationType.GetConstructor(destinationArguments), mapKeys, mapValues); } +#if FULL_OR_STANDARD + public TypePair? GetAssociatedTypes(TypePair initialTypes) => null; +#endif } \ No newline at end of file diff --git a/src/AutoMapper/Mappers/MapperRegistry.cs b/src/AutoMapper/Mappers/MapperRegistry.cs index 876dffd3a7..7d80eec611 100644 --- a/src/AutoMapper/Mappers/MapperRegistry.cs +++ b/src/AutoMapper/Mappers/MapperRegistry.cs @@ -2,8 +2,8 @@ namespace AutoMapper.Internal.Mappers; internal static class MapperRegistry { - public static List Mappers() => new(capacity: 18) - { + public static List Mappers() => + [ new CollectionMapper(),// matches IEnumerable, requires a setter, ICollection<> or IList new AssignableMapper(),// except collections, which are copied; most likely match new NullableSourceMapper(),// map from the underlying type @@ -18,9 +18,9 @@ internal static class MapperRegistry new ConstructorMapper(),// new Destination(source) new ConversionOperatorMapper("op_Implicit"),// implicit operator Destination or implicit operator Source new ConversionOperatorMapper("op_Explicit"),// explicit operator Destination or explicit operator Source - new FromStringDictionaryMapper(),// property values to typed object - new ToStringDictionaryMapper(),// typed object to property values new FromDynamicMapper(),// dynamic to typed object new ToDynamicMapper(),// typed object to dynamic - }; + new FromStringDictionaryMapper(),// property values to typed object + new ToStringDictionaryMapper(),// typed object to property values + ]; } \ No newline at end of file diff --git a/src/AutoMapper/Mappers/NullableDestinationMapper.cs b/src/AutoMapper/Mappers/NullableDestinationMapper.cs index 04ffbf2cda..3defec9d80 100644 --- a/src/AutoMapper/Mappers/NullableDestinationMapper.cs +++ b/src/AutoMapper/Mappers/NullableDestinationMapper.cs @@ -1,6 +1,6 @@ namespace AutoMapper.Internal.Mappers; -public class NullableDestinationMapper : IObjectMapper +public sealed class NullableDestinationMapper : IObjectMapper { public bool IsMatch(TypePair context) => context.DestinationType.IsNullableType(); public Expression MapExpression(IGlobalConfiguration configuration, ProfileMap profileMap, MemberMap memberMap, Expression sourceExpression, Expression destExpression) => diff --git a/src/AutoMapper/Mappers/NullableSourceMapper.cs b/src/AutoMapper/Mappers/NullableSourceMapper.cs index 7f574b7621..969da15201 100644 --- a/src/AutoMapper/Mappers/NullableSourceMapper.cs +++ b/src/AutoMapper/Mappers/NullableSourceMapper.cs @@ -1,6 +1,6 @@ namespace AutoMapper.Internal.Mappers; -public class NullableSourceMapper : IObjectMapper +public sealed class NullableSourceMapper : IObjectMapper { public bool IsMatch(TypePair context) => context.SourceType.IsNullableType(); public Expression MapExpression(IGlobalConfiguration configuration, ProfileMap profileMap, MemberMap memberMap, Expression sourceExpression, Expression destExpression) => diff --git a/src/AutoMapper/Mappers/ParseStringMapper.cs b/src/AutoMapper/Mappers/ParseStringMapper.cs index 93cd16b351..033926c5cd 100644 --- a/src/AutoMapper/Mappers/ParseStringMapper.cs +++ b/src/AutoMapper/Mappers/ParseStringMapper.cs @@ -1,9 +1,12 @@ namespace AutoMapper.Internal.Mappers; -public class ParseStringMapper : IObjectMapper +public sealed class ParseStringMapper : IObjectMapper { public bool IsMatch(TypePair context) => context.SourceType == typeof(string) && HasParse(context.DestinationType); static bool HasParse(Type type) => type == typeof(Guid) || type == typeof(TimeSpan) || type == typeof(DateTimeOffset); public Expression MapExpression(IGlobalConfiguration configuration, ProfileMap profileMap, MemberMap memberMap, Expression sourceExpression, Expression destExpression) => - Call(destExpression.Type.GetMethod("Parse", new[] { typeof(string) }), sourceExpression); + Call(destExpression.Type.GetMethod("Parse", [typeof(string)]), sourceExpression); +#if FULL_OR_STANDARD + public TypePair? GetAssociatedTypes(TypePair initialTypes) => null; +#endif } \ No newline at end of file diff --git a/src/AutoMapper/Mappers/StringToEnumMapper.cs b/src/AutoMapper/Mappers/StringToEnumMapper.cs index 4b13286e56..987f315232 100644 --- a/src/AutoMapper/Mappers/StringToEnumMapper.cs +++ b/src/AutoMapper/Mappers/StringToEnumMapper.cs @@ -1,6 +1,7 @@ +#if NET8_0_OR_GREATER using System.Runtime.Serialization; namespace AutoMapper.Internal.Mappers; -public class StringToEnumMapper : IObjectMapper +public sealed class StringToEnumMapper : IObjectMapper { private static readonly MethodInfo EqualsMethod = typeof(StringToEnumMapper).GetMethod(nameof(StringCompareOrdinalIgnoreCase)); private static readonly MethodInfo ParseMethod = typeof(Enum).StaticGenericMethod("Parse", parametersCount: 2); @@ -28,10 +29,72 @@ internal static Expression CheckEnumMember(Expression sourceExpression, Type enu var enumToObject = Constant(Enum.ToObject(enumType, memberInfo.GetValue(null))); var attributeConstant = Constant(attributeValue); var (body, testValue) = comparison == null ? (attributeConstant, enumToObject) : (ToType(enumToObject, enumType), attributeConstant); - switchCases ??= new(); + switchCases ??= []; switchCases.Add(SwitchCase(body, testValue)); } return switchCases == null ? defaultExpression : Switch(sourceExpression, defaultExpression, comparison, switchCases); } public static bool StringCompareOrdinalIgnoreCase(string x, string y) => StringComparer.OrdinalIgnoreCase.Equals(x, y); -} \ No newline at end of file +} +#else +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Reflection; +using System.Runtime.Serialization; +namespace AutoMapper.Internal.Mappers +{ + using static Execution.ExpressionBuilder; + using static Expression; + public class StringToEnumMapper : IObjectMapper + { + private static readonly MethodInfo EqualsMethod = typeof(StringToEnumMapper).GetMethod("StringCompareOrdinalIgnoreCase"); + private static readonly MethodInfo ParseMethod = typeof(Enum).GetMethod("Parse", new[] { typeof(Type), typeof(string), typeof(bool) }); + private static readonly MethodInfo IsNullOrEmptyMethod = typeof(string).GetMethod("IsNullOrEmpty"); + public bool IsMatch(TypePair context) => context.SourceType == typeof(string) && context.DestinationType.IsEnum; + public Expression MapExpression(IGlobalConfiguration configurationProvider, ProfileMap profileMap, + MemberMap memberMap, Expression sourceExpression, Expression destExpression) + { + var destinationType = destExpression.Type; + List switchCases = null; + foreach (var memberInfo in destinationType.GetFields(TypeExtensions.StaticFlags)) + { + var attributeValue = memberInfo.GetCustomAttribute()?.Value; + if (attributeValue != null) + { + var switchCase = SwitchCase( + ToType(Constant(Enum.ToObject(destinationType, memberInfo.GetValue(null))), destinationType), Constant(attributeValue)); + switchCases ??= new(); + switchCases.Add(switchCase); + } + } + var enumParse = ToType(Call(ParseMethod, Constant(destinationType), sourceExpression, True), destinationType); + var parse = switchCases != null ? Switch(sourceExpression, enumParse, EqualsMethod, switchCases) : enumParse; + return Condition(Call(IsNullOrEmptyMethod, sourceExpression), Default(destinationType), parse); + } + + internal static Expression CheckEnumMember(Expression sourceExpression, Type enumType, Expression defaultExpression, MethodInfo comparison = null) + { + List switchCases = null; + foreach (var memberInfo in enumType.GetFields(TypeExtensions.StaticFlags)) + { + var attributeValue = memberInfo.GetCustomAttribute()?.Value; + if (attributeValue == null) + { + continue; + } + var enumToObject = Constant(Enum.ToObject(enumType, memberInfo.GetValue(null))); + var attributeConstant = Constant(attributeValue); + var (body, testValue) = comparison == null ? (attributeConstant, enumToObject) : (ToType(enumToObject, enumType), attributeConstant); + switchCases ??= []; + switchCases.Add(SwitchCase(body, testValue)); + } + return switchCases == null ? defaultExpression : Switch(sourceExpression, defaultExpression, comparison, switchCases); + } + + public TypePair? GetAssociatedTypes(TypePair initialTypes) => null; + + public static bool StringCompareOrdinalIgnoreCase(string x, string y) => StringComparer.OrdinalIgnoreCase.Equals(x, y); + } +} +#endif \ No newline at end of file diff --git a/src/AutoMapper/Mappers/ToDynamicMapper.cs b/src/AutoMapper/Mappers/ToDynamicMapper.cs index 4fdae9eeba..7b1e37c021 100644 --- a/src/AutoMapper/Mappers/ToDynamicMapper.cs +++ b/src/AutoMapper/Mappers/ToDynamicMapper.cs @@ -2,7 +2,7 @@ using Microsoft.CSharp.RuntimeBinder; using Binder = Microsoft.CSharp.RuntimeBinder.Binder; namespace AutoMapper.Internal.Mappers; -public class ToDynamicMapper : IObjectMapper +public sealed class ToDynamicMapper : IObjectMapper { private static readonly MethodInfo MapMethodInfo = typeof(ToDynamicMapper).GetStaticMethod(nameof(Map)); private static object Map(object source, object destination, Type destinationType, ResolutionContext context, ProfileMap profileMap) @@ -28,10 +28,10 @@ private static object Map(object source, object destination, Type destinationTyp private static void SetDynamically(string memberName, object target, object value) { var binder = Binder.SetMember(CSharpBinderFlags.None, memberName, null, - new[]{ + [ CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null), CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null) - }); + ]); var callsite = CallSite>.Create(binder); callsite.Target(callsite, target, value); } @@ -39,4 +39,7 @@ private static void SetDynamically(string memberName, object target, object valu public Expression MapExpression(IGlobalConfiguration configuration, ProfileMap profileMap, MemberMap memberMap, Expression sourceExpression, Expression destExpression) => Call(MapMethodInfo, sourceExpression.ToObject(), destExpression, Constant(destExpression.Type), ContextParameter, Constant(profileMap)); +#if FULL_OR_STANDARD + public TypePair? GetAssociatedTypes(TypePair initialTypes) => null; +#endif } \ No newline at end of file diff --git a/src/AutoMapper/Mappers/ToStringDictionaryMapper.cs b/src/AutoMapper/Mappers/ToStringDictionaryMapper.cs index 21d4400f3f..761af30019 100644 --- a/src/AutoMapper/Mappers/ToStringDictionaryMapper.cs +++ b/src/AutoMapper/Mappers/ToStringDictionaryMapper.cs @@ -1,5 +1,5 @@ namespace AutoMapper.Internal.Mappers; -public class ToStringDictionaryMapper : IObjectMapper +public sealed class ToStringDictionaryMapper : IObjectMapper { private static readonly MethodInfo MembersDictionaryMethodInfo = typeof(ToStringDictionaryMapper).GetStaticMethod(nameof(MembersDictionary)); public bool IsMatch(TypePair context) => typeof(IDictionary).IsAssignableFrom(context.DestinationType); @@ -7,4 +7,7 @@ public Expression MapExpression(IGlobalConfiguration configuration, ProfileMap p Call(MembersDictionaryMethodInfo, sourceExpression.ToObject(), Constant(profileMap)); private static Dictionary MembersDictionary(object source, ProfileMap profileMap) => profileMap.CreateTypeDetails(source.GetType()).ReadAccessors.ToDictionary(p => p.Name, p => p.GetMemberValue(source)); +#if FULL_OR_STANDARD + public TypePair? GetAssociatedTypes(TypePair initialTypes) => null; +#endif } \ No newline at end of file diff --git a/src/AutoMapper/Mappers/ToStringMapper.cs b/src/AutoMapper/Mappers/ToStringMapper.cs index 243b05db0f..76abc6c00b 100644 --- a/src/AutoMapper/Mappers/ToStringMapper.cs +++ b/src/AutoMapper/Mappers/ToStringMapper.cs @@ -1,5 +1,5 @@ namespace AutoMapper.Internal.Mappers; -public class ToStringMapper : IObjectMapper +public sealed class ToStringMapper : IObjectMapper { public bool IsMatch(TypePair context) => context.DestinationType == typeof(string); public Expression MapExpression(IGlobalConfiguration configuration, ProfileMap profileMap, MemberMap memberMap, Expression sourceExpression, Expression destExpression) @@ -8,4 +8,7 @@ public Expression MapExpression(IGlobalConfiguration configuration, ProfileMap p var toStringCall = Call(sourceExpression, ObjectToString); return sourceType.IsEnum ? StringToEnumMapper.CheckEnumMember(sourceExpression, sourceType, toStringCall) : toStringCall; } +#if FULL_OR_STANDARD + public TypePair? GetAssociatedTypes(TypePair initialTypes) => null; +#endif } \ No newline at end of file diff --git a/src/AutoMapper/Mappers/UnderlyingEnumTypeMapper.cs b/src/AutoMapper/Mappers/UnderlyingEnumTypeMapper.cs index 388abc8571..acca725f54 100644 --- a/src/AutoMapper/Mappers/UnderlyingEnumTypeMapper.cs +++ b/src/AutoMapper/Mappers/UnderlyingEnumTypeMapper.cs @@ -1,8 +1,11 @@ namespace AutoMapper.Internal.Mappers; -public class UnderlyingTypeEnumMapper : IObjectMapper +public sealed class UnderlyingTypeEnumMapper : IObjectMapper { public bool IsMatch(TypePair context) => context.IsEnumToUnderlyingType() || context.IsUnderlyingTypeToEnum(); public Expression MapExpression(IGlobalConfiguration configuration, ProfileMap profileMap, MemberMap memberMap, Expression sourceExpression, Expression destExpression) => sourceExpression; +#if FULL_OR_STANDARD + public TypePair? GetAssociatedTypes(TypePair initialTypes) => null; +#endif } \ No newline at end of file diff --git a/src/AutoMapper/MemberMap.cs b/src/AutoMapper/MemberMap.cs index c1a5a3dcf8..2a5805bccc 100644 --- a/src/AutoMapper/MemberMap.cs +++ b/src/AutoMapper/MemberMap.cs @@ -5,8 +5,9 @@ namespace AutoMapper; [EditorBrowsable(EditorBrowsableState.Never)] public class MemberMap : IValueResolver { - protected MemberMap(TypeMap typeMap = null) => TypeMap = typeMap; - internal static readonly MemberMap Instance = new(); + private protected Type _sourceType; + protected MemberMap(TypeMap typeMap, Type destinationType) => (TypeMap, DestinationType) = (typeMap, destinationType); + internal static readonly MemberMap Instance = new(null, null); public TypeMap TypeMap { get; protected set; } public LambdaExpression CustomMapExpression => Resolver?.ProjectToExpression; public bool IsResolveConfigured => Resolver != null && Resolver != this; @@ -14,16 +15,17 @@ public class MemberMap : IValueResolver public void SetResolver(IValueResolver resolver) { Resolver = resolver; + _sourceType = resolver.ResolvedType; Ignored = false; } - public virtual Type SourceType => default; - public virtual MemberInfo[] SourceMembers { get => Array.Empty(); set { } } - public virtual IncludedMember IncludedMember => null; + public virtual Type SourceType => _sourceType ??= GetSourceType(); + public virtual MemberInfo[] SourceMembers { get => []; set { } } + public virtual IncludedMember IncludedMember { get => default; protected set { } } public virtual string DestinationName => default; - public virtual Type DestinationType { get => default; protected set { } } + public Type DestinationType { get; protected set; } public virtual TypePair Types() => new(SourceType, DestinationType); public bool CanResolveValue => !Ignored && Resolver != null; - public bool IsMapped => Ignored || CanResolveValue; + public bool IsMapped => Ignored || Resolver != null; public virtual bool Ignored { get => default; set { } } public virtual bool? ExplicitExpansion { get => default; set { } } public virtual bool Inline { get; set; } = true; @@ -34,7 +36,7 @@ public void SetResolver(IValueResolver resolver) public virtual LambdaExpression PreCondition { get => default; set { } } public virtual LambdaExpression Condition { get => default; set { } } public IValueResolver Resolver { get; protected set; } - public virtual IReadOnlyCollection ValueTransformers => Array.Empty(); + public virtual IReadOnlyCollection ValueTransformers => []; public MemberInfo SourceMember => Resolver?.GetSourceMember(this); public string GetSourceMemberName() => Resolver?.SourceMemberName ?? SourceMember?.Name; public bool MustUseDestination => UseDestinationValue is true || !CanBeSet; @@ -61,10 +63,41 @@ public void MapByConvention(MemberInfo[] sourceMembers) SourceMembers = sourceMembers; Resolver = this; } + protected bool ApplyInheritedMap(MemberMap inheritedMap) + { + if(Ignored || IsResolveConfigured) + { + return false; + } + if(inheritedMap.Ignored) + { + Ignored = true; + return true; + } + if(inheritedMap.IsResolveConfigured) + { + _sourceType = inheritedMap._sourceType; + Resolver = inheritedMap.Resolver.CloseGenerics(TypeMap); + return true; + } + if(Resolver == null) + { + _sourceType = inheritedMap._sourceType; + MapByConvention(inheritedMap.SourceMembers); + return true; + } + return false; + } Expression IValueResolver.GetExpression(IGlobalConfiguration configuration, MemberMap memberMap, Expression source, Expression destination, Expression destinationMember) => ChainSourceMembers(configuration, source, destinationMember); MemberInfo IValueResolver.GetSourceMember(MemberMap memberMap) => SourceMembers[0]; Type IValueResolver.ResolvedType => SourceMembers[^1].GetMemberType(); + +#if FULL_OR_STANDARD + public string SourceMemberName => null; + public LambdaExpression ProjectToExpression => null; + public IValueResolver CloseGenerics(TypeMap typeMap) => this; +#endif } public readonly record struct ValueTransformerConfiguration(Type ValueType, LambdaExpression TransformerExpression) { @@ -79,5 +112,5 @@ public static class ValueTransformerConfigurationExtensions /// Value transformer list /// Transformation expression public static void Add(this List valueTransformers, Expression> transformer) => - valueTransformers.Add(new ValueTransformerConfiguration(typeof(TValue), transformer)); + valueTransformers.Add(new(typeof(TValue), transformer)); } \ No newline at end of file diff --git a/src/AutoMapper/PathMap.cs b/src/AutoMapper/PathMap.cs index 7c9d29ecae..83dce8cbed 100644 --- a/src/AutoMapper/PathMap.cs +++ b/src/AutoMapper/PathMap.cs @@ -1,7 +1,8 @@ namespace AutoMapper; [DebuggerDisplay("{DestinationExpression}")] [EditorBrowsable(EditorBrowsableState.Never)] -public class PathMap : MemberMap +public sealed class PathMap(LambdaExpression destinationExpression, MemberPath memberPath, TypeMap typeMap) + : MemberMap(typeMap, memberPath.Last.GetMemberType()) { public PathMap(PathMap pathMap, TypeMap typeMap, IncludedMember includedMember) : this(pathMap.DestinationExpression, pathMap.MemberPath, typeMap) { @@ -10,18 +11,12 @@ public PathMap(PathMap pathMap, TypeMap typeMap, IncludedMember includedMember) Condition = pathMap.Condition; Ignored = pathMap.Ignored; } - public PathMap(LambdaExpression destinationExpression, MemberPath memberPath, TypeMap typeMap) : base(typeMap) - { - MemberPath = memberPath; - DestinationExpression = destinationExpression; - } public override Type SourceType => Resolver.ResolvedType; - public LambdaExpression DestinationExpression { get; } - public MemberPath MemberPath { get; } - public override Type DestinationType => MemberPath.Last.GetMemberType(); + public LambdaExpression DestinationExpression { get; } = destinationExpression; + public MemberPath MemberPath { get; } = memberPath; public override string DestinationName => MemberPath.ToString(); public override bool CanBeSet => ReflectionHelper.CanBeSet(MemberPath.Last); public override bool Ignored { get; set; } - public override IncludedMember IncludedMember { get; } + public override IncludedMember IncludedMember { get; protected set; } public override LambdaExpression Condition { get; set; } } \ No newline at end of file diff --git a/src/AutoMapper/ProfileMap.cs b/src/AutoMapper/ProfileMap.cs index ddf92c71be..477892d079 100644 --- a/src/AutoMapper/ProfileMap.cs +++ b/src/AutoMapper/ProfileMap.cs @@ -3,9 +3,9 @@ namespace AutoMapper; [DebuggerDisplay("{Name}")] [EditorBrowsable(EditorBrowsableState.Never)] -public class ProfileMap +public sealed class ProfileMap { - private static readonly HashSet EmptyHashSet = new(); + private static readonly HashSet EmptyHashSet = []; private TypeMapConfiguration[] _typeMapConfigs; private Dictionary _openTypeMapConfigs; private Dictionary _typeDetails; @@ -27,9 +27,17 @@ public ProfileMap(IProfileConfiguration profile, IGlobalConfigurationExpression ValueTransformers = profile.ValueTransformers.Concat(configuration?.ValueTransformers).ToArray(); var profileInternal = (IProfileExpressionInternal)profile; MemberConfiguration = profileInternal.MemberConfiguration; - MemberConfiguration.Merge(configuration.Internal()?.MemberConfiguration); + if(configuration == null) + { + MemberConfiguration.SourceNamingConvention ??= PascalCaseNamingConvention.Instance; + MemberConfiguration.DestinationNamingConvention ??= PascalCaseNamingConvention.Instance; + } + else + { + MemberConfiguration.Merge(configuration.Internal().MemberConfiguration); + } var globalIgnores = profile.GlobalIgnores.Concat(globalProfile?.GlobalIgnores); - GlobalIgnores = globalIgnores == Array.Empty() ? EmptyHashSet : new HashSet(globalIgnores); + GlobalIgnores = globalIgnores == Array.Empty() ? EmptyHashSet : [..globalIgnores]; SourceExtensionMethods = profile.SourceExtensionMethods.Concat(globalProfile?.SourceExtensionMethods).ToArray(); AllPropertyMapActions = profile.AllPropertyMapActions.Concat(globalProfile?.AllPropertyMapActions).ToArray(); AllTypeMapActions = profile.AllTypeMapActions.Concat(globalProfile?.AllTypeMapActions).ToArray(); @@ -88,13 +96,13 @@ internal void Clear() public Func ShouldMapProperty { get; } public Func ShouldMapMethod { get; } public Func ShouldUseConstructor { get; } - public IEnumerable> AllPropertyMapActions { get; } + public IEnumerable AllPropertyMapActions { get; } public IEnumerable> AllTypeMapActions { get; } public HashSet GlobalIgnores { get; } public MemberConfiguration MemberConfiguration { get; } public IEnumerable SourceExtensionMethods { get; } - public List Prefixes { get; } = new(); - public List Postfixes { get; } = new(); + public List Prefixes { get; } = []; + public List Postfixes { get; } = []; public IReadOnlyCollection ValueTransformers { get; } public TypeDetails CreateTypeDetails(Type type) { @@ -129,7 +137,7 @@ public void Register(IGlobalConfiguration configuration) private void BuildTypeMap(IGlobalConfiguration configuration, TypeMapConfiguration config) { var sourceMembers = configuration.SourceMembers; - var typeMap = new TypeMap(config.SourceType, config.DestinationType, this, config, sourceMembers); + TypeMap typeMap = new(config.SourceType, config.DestinationType, this, config, sourceMembers); config.Configure(typeMap, sourceMembers); configuration.RegisterTypeMap(typeMap); } @@ -182,21 +190,26 @@ private void Configure(TypeMap typeMap, IGlobalConfiguration configuration) if (typeMap.HasTypeConverter) { return; - } - foreach (var action in AllTypeMapActions) + } + MappingExpression expression = new(typeMap); + foreach(var action in AllTypeMapActions) { - var expression = new MappingExpression(typeMap.Types, typeMap.ConfiguredMemberList); action(typeMap, expression); - expression.Configure(typeMap, configuration.SourceMembers); - } - foreach (var action in AllPropertyMapActions) - { - foreach (var propertyMap in typeMap.PropertyMaps) - { - var memberExpression = new MemberConfigurationExpression(propertyMap.DestinationMember, typeMap.SourceType); - action(propertyMap, memberExpression); - memberExpression.Configure(typeMap); - } + } + expression.Configure(typeMap, configuration.SourceMembers); + foreach(var propertyMap in typeMap.PropertyMaps) + { + MemberConfigurationExpression memberExpression = null; + foreach(var action in AllPropertyMapActions) + { + if (!action.Condition(propertyMap)) + { + continue; + } + memberExpression ??= new(propertyMap.DestinationMember, typeMap.SourceType); + action.Action(propertyMap, memberExpression); + } + memberExpression?.Configure(typeMap); } ApplyBaseMaps(typeMap, typeMap, configuration); ApplyDerivedMaps(typeMap, typeMap, configuration); @@ -207,7 +220,7 @@ public TypeMap CreateClosedGenericTypeMap(TypeMapConfiguration openMapConfig, Ty TypeMap closedMap; lock (configuration) { - closedMap = new TypeMap(closedTypes.SourceType, closedTypes.DestinationType, this, openMapConfig); + closedMap = new(closedTypes.SourceType, closedTypes.DestinationType, this, openMapConfig); } openMapConfig.Configure(closedMap, configuration.SourceMembers); Configure(closedMap, configuration); @@ -232,13 +245,13 @@ private void ApplyMemberMaps(TypeMap currentMap, IGlobalConfiguration configurat foreach (var includedMemberExpression in currentMap.GetAllIncludedMembers()) { var includedMap = configuration.GetIncludedTypeMap(includedMemberExpression.Body.Type, currentMap.DestinationType); - var includedMember = new IncludedMember(includedMap, includedMemberExpression); + IncludedMember includedMember = new(includedMap, includedMemberExpression); if (currentMap.AddMemberMap(includedMember)) { ApplyMemberMaps(includedMap, configuration); foreach (var inheritedIncludedMember in includedMap.IncludedMembersTypeMaps) { - currentMap.AddMemberMap(includedMember.Chain(inheritedIncludedMember)); + currentMap.AddMemberMap(includedMember.Chain(inheritedIncludedMember, configuration)); } } } @@ -258,34 +271,23 @@ public bool MapDestinationPropertyToSource(TypeDetails sourceTypeDetails, Type d } [EditorBrowsable(EditorBrowsableState.Never)] [DebuggerDisplay("{MemberExpression}, {TypeMap}")] -public class IncludedMember : IEquatable +public sealed record IncludedMember(TypeMap TypeMap, LambdaExpression MemberExpression, ParameterExpression Variable, LambdaExpression ProjectToCustomSource) { public IncludedMember(TypeMap typeMap, LambdaExpression memberExpression) : this(typeMap, memberExpression, - Variable(memberExpression.Body.Type, string.Join("", memberExpression.GetMembersChain().Select(m => m.Name))), memberExpression) - { - } - private IncludedMember(TypeMap typeMap, LambdaExpression memberExpression, ParameterExpression variable, LambdaExpression projectToCustomSource) - { - TypeMap = typeMap; - MemberExpression = memberExpression; - Variable = variable; - ProjectToCustomSource = projectToCustomSource; - } - public IncludedMember Chain(IncludedMember other) + Expression.Variable(memberExpression.Body.Type, string.Join("", memberExpression.GetMembersChain().Select(m => m.Name))), memberExpression){} + public IncludedMember Chain(IncludedMember other, IGlobalConfiguration configuration = null) { if (other == null) { return this; } - return new(other.TypeMap, Chain(other.MemberExpression), other.Variable, Chain(MemberExpression, other.MemberExpression)); + return new(other.TypeMap, Chain(other.MemberExpression, other, configuration), other.Variable, Chain(MemberExpression, other.MemberExpression)); } public static LambdaExpression Chain(LambdaExpression customSource, LambdaExpression lambda) => Lambda(lambda.ReplaceParameters(customSource.Body), customSource.Parameters); - public TypeMap TypeMap { get; } - public LambdaExpression MemberExpression { get; } - public ParameterExpression Variable { get; } - public LambdaExpression ProjectToCustomSource { get; } - public LambdaExpression Chain(LambdaExpression lambda) => Lambda(lambda.ReplaceParameters(Variable), lambda.Parameters); + public LambdaExpression Chain(LambdaExpression lambda) => Chain(lambda, null, null); + LambdaExpression Chain(LambdaExpression lambda, IncludedMember includedMember, IGlobalConfiguration configuration) => + Lambda(lambda.ReplaceParameters(Variable).NullCheck(configuration, includedMember: includedMember), lambda.Parameters); public bool Equals(IncludedMember other) => TypeMap == other?.TypeMap; public override int GetHashCode() => TypeMap.GetHashCode(); } \ No newline at end of file diff --git a/src/AutoMapper/PropertyMap.cs b/src/AutoMapper/PropertyMap.cs index 41639057ff..543584f79f 100644 --- a/src/AutoMapper/PropertyMap.cs +++ b/src/AutoMapper/PropertyMap.cs @@ -2,23 +2,18 @@ namespace AutoMapper; [DebuggerDisplay("{DestinationMember.Name}")] [EditorBrowsable(EditorBrowsableState.Never)] -public class PropertyMap : MemberMap +public sealed class PropertyMap : MemberMap { private MemberMapDetails _details; - private Type _sourceType; - public PropertyMap(MemberInfo destinationMember, Type destinationMemberType, TypeMap typeMap) : base(typeMap) - { + public PropertyMap(MemberInfo destinationMember, Type destinationMemberType, TypeMap typeMap) : base(typeMap, destinationMemberType) => DestinationMember = destinationMember; - DestinationType = destinationMemberType; - } - public PropertyMap(PropertyMap inheritedMappedProperty, TypeMap typeMap) : base(typeMap) + public PropertyMap(PropertyMap inheritedMappedProperty, TypeMap typeMap) : base(typeMap, inheritedMappedProperty.DestinationType) { DestinationMember = inheritedMappedProperty.DestinationMember; if (DestinationMember.DeclaringType.ContainsGenericParameters) { DestinationMember = typeMap.DestinationSetters.Single(m => m.Name == DestinationMember.Name); } - DestinationType = inheritedMappedProperty.DestinationType; if (DestinationType.ContainsGenericParameters) { DestinationType = DestinationMember.GetMemberType(); @@ -30,38 +25,15 @@ public PropertyMap(PropertyMap includedMemberMap, TypeMap typeMap, IncludedMembe private MemberMapDetails Details => _details ??= new(); public MemberInfo DestinationMember { get; } public override string DestinationName => DestinationMember?.Name; - public override Type DestinationType { get; protected set; } - public override MemberInfo[] SourceMembers { get; set; } = Array.Empty(); - public override bool CanBeSet => ReflectionHelper.CanBeSet(DestinationMember); + public override MemberInfo[] SourceMembers { get; set; } = []; + public override bool CanBeSet => DestinationMember.CanBeSet(); public override bool Ignored { get; set; } - public override Type SourceType => _sourceType ??= GetSourceType(); - public void ApplyInheritedPropertyMap(PropertyMap inheritedMappedProperty) + public void ApplyInheritedPropertyMap(PropertyMap inheritedMap) { - if (Ignored) - { - return; - } - if (!IsResolveConfigured) - { - if (inheritedMappedProperty.Ignored) - { - Ignored = true; - return; - } - if (inheritedMappedProperty.IsResolveConfigured) - { - _sourceType = inheritedMappedProperty._sourceType; - Resolver = inheritedMappedProperty.Resolver.CloseGenerics(TypeMap); - } - else if (Resolver == null) - { - _sourceType = inheritedMappedProperty._sourceType; - MapByConvention(inheritedMappedProperty.SourceMembers); - } - } - if (inheritedMappedProperty._details != null) + ApplyInheritedMap(inheritedMap); + if (!Ignored && inheritedMap._details != null) { - Details.ApplyInheritedPropertyMap(inheritedMappedProperty._details); + Details.ApplyInheritedPropertyMap(inheritedMap._details); } } public override IncludedMember IncludedMember => _details?.IncludedMember; @@ -96,13 +68,13 @@ public void ApplyInheritedPropertyMap(MemberMapDetails inheritedMappedProperty) ExplicitExpansion ??= inheritedMappedProperty.ExplicitExpansion; if (inheritedMappedProperty.ValueTransformers != null) { - ValueTransformers ??= new(); + ValueTransformers ??= []; ValueTransformers.InsertRange(0, inheritedMappedProperty.ValueTransformers); } } public void AddValueTransformation(ValueTransformerConfiguration valueTransformerConfiguration) { - ValueTransformers ??= new(); + ValueTransformers ??= []; ValueTransformers.Add(valueTransformerConfiguration); } } diff --git a/src/AutoMapper/QueryableExtensions/Extensions.cs b/src/AutoMapper/QueryableExtensions/Extensions.cs index 8b50d06579..c1d3b276e6 100644 --- a/src/AutoMapper/QueryableExtensions/Extensions.cs +++ b/src/AutoMapper/QueryableExtensions/Extensions.cs @@ -72,14 +72,14 @@ static IQueryable ToCore(this IQueryable source, Type destinationType, IConfigur configuration.Internal().ProjectionBuilder.GetProjection(source.ElementType, destinationType, parameters, memberPathsToExpand.Select(m => new MemberPath(m)).ToArray()) .Chain(source, Select); } -public class MemberVisitor : ExpressionVisitor +public sealed class MemberVisitor : ExpressionVisitor { - private readonly List _members = new(); + private readonly List _members = []; public static MemberInfo[] GetMemberPath(Expression expression) { - var memberVisitor = new MemberVisitor(); + MemberVisitor memberVisitor = new(); memberVisitor.Visit(expression); - return memberVisitor._members.ToArray(); + return [.. memberVisitor._members]; } protected override Expression VisitMember(MemberExpression node) { diff --git a/src/AutoMapper/QueryableExtensions/NullsafeQueryRewriter.cs b/src/AutoMapper/QueryableExtensions/NullsafeQueryRewriter.cs index 0bf827b787..ec77ceecbe 100644 --- a/src/AutoMapper/QueryableExtensions/NullsafeQueryRewriter.cs +++ b/src/AutoMapper/QueryableExtensions/NullsafeQueryRewriter.cs @@ -43,7 +43,7 @@ internal class NullsafeQueryRewriter : ExpressionVisitor protected override Expression VisitMember(MemberExpression node) { if (node == null) - throw new ArgumentNullException(nameof(node)); + throw new System.ArgumentNullException(nameof(node)); var target = Visit(node.Expression); @@ -60,7 +60,7 @@ protected override Expression VisitMember(MemberExpression node) protected override Expression VisitMethodCall(MethodCallExpression node) { if (node == null) - throw new ArgumentNullException(nameof(node)); + throw new System.ArgumentNullException(nameof(node)); var target = Visit(node.Object); diff --git a/src/AutoMapper/QueryableExtensions/ProjectionBuilder.cs b/src/AutoMapper/QueryableExtensions/ProjectionBuilder.cs index 586f65de10..7f73c791c9 100644 --- a/src/AutoMapper/QueryableExtensions/ProjectionBuilder.cs +++ b/src/AutoMapper/QueryableExtensions/ProjectionBuilder.cs @@ -15,20 +15,13 @@ public interface IProjectionMapper Expression Project(IGlobalConfiguration configuration, in ProjectionRequest request, Expression resolvedSource, LetPropertyMaps letPropertyMaps); } [EditorBrowsable(EditorBrowsableState.Never)] -public class ProjectionBuilder : IProjectionBuilder +public sealed class ProjectionBuilder : IProjectionBuilder { internal static List DefaultProjectionMappers() => - new(capacity: 5) - { - new AssignableProjectionMapper(), - new EnumerableProjectionMapper(), - new NullableSourceProjectionMapper(), - new StringProjectionMapper(), - new EnumProjectionMapper(), - }; - private readonly LockingConcurrentDictionary _projectionCache; - private readonly IGlobalConfiguration _configuration; - private readonly IProjectionMapper[] _projectionMappers; + [new AssignableProjectionMapper(), new EnumerableProjectionMapper(), new NullableSourceProjectionMapper(), new StringProjectionMapper(), new EnumProjectionMapper()]; + readonly LockingConcurrentDictionary _projectionCache; + readonly IGlobalConfiguration _configuration; + readonly IProjectionMapper[] _projectionMappers; public ProjectionBuilder(IGlobalConfiguration configuration, IProjectionMapper[] projectionMappers) { _configuration = configuration; @@ -37,7 +30,7 @@ public ProjectionBuilder(IGlobalConfiguration configuration, IProjectionMapper[] } public QueryExpressions GetProjection(Type sourceType, Type destinationType, object parameters, MemberPath[] membersToExpand) { - var projectionRequest = new ProjectionRequest(sourceType, destinationType, membersToExpand, Array.Empty()); + ProjectionRequest projectionRequest = new(sourceType, destinationType, membersToExpand, []); var cachedExpressions = _projectionCache.GetOrAdd(projectionRequest); if (parameters == null && !_configuration.EnableNullPropagationForQueryMapping) { @@ -45,168 +38,210 @@ public QueryExpressions GetProjection(Type sourceType, Type destinationType, obj } return cachedExpressions.Prepare(_configuration.EnableNullPropagationForQueryMapping, parameters); } - private QueryExpressions CreateProjection(ProjectionRequest request) => - CreateProjection(request, new FirstPassLetPropertyMaps(_configuration, MemberPath.Empty, new())); - public QueryExpressions CreateProjection(in ProjectionRequest request, LetPropertyMaps letPropertyMaps) + QueryExpressions CreateProjection(ProjectionRequest request) + { + var (typeMap, polymorphicMaps) = PolymorphicMaps(request); + var letPropertyMaps = polymorphicMaps.Length > 0 ? new LetPropertyMaps(_configuration, MemberPath.Empty, []) : new FirstPassLetPropertyMaps(_configuration, MemberPath.Empty, []); + return CreateProjection(request, letPropertyMaps, typeMap, polymorphicMaps); + } + (TypeMap, TypeMap[]) PolymorphicMaps(in ProjectionRequest request) { - var instanceParameter = Parameter(request.SourceType, "dto"+ request.SourceType.Name); var typeMap = _configuration.ResolveTypeMap(request.SourceType, request.DestinationType) ?? throw TypeMap.MissingMapException(request.SourceType, request.DestinationType); - var projection = CreateProjectionCore(request, instanceParameter, typeMap, letPropertyMaps); - return letPropertyMaps.Count > 0 ? - letPropertyMaps.GetSubQueryExpression(this, projection, typeMap, request, instanceParameter) : - new(projection, instanceParameter); + return (typeMap, PolymorphicMaps(typeMap)); + } + TypeMap[] PolymorphicMaps(TypeMap typeMap) => _configuration.GetIncludedTypeMaps(typeMap.IncludedDerivedTypes + .Where(tp => tp.SourceType != typeMap.SourceType && !tp.DestinationType.IsAbstract).DistinctBy(tp => tp.SourceType).ToArray()); + public QueryExpressions CreateProjection(in ProjectionRequest request, LetPropertyMaps letPropertyMaps) + { + var (typeMap, polymorphicMaps) = PolymorphicMaps(request); + return CreateProjection(request, letPropertyMaps, typeMap, polymorphicMaps); + } + QueryExpressions CreateProjection(in ProjectionRequest request, LetPropertyMaps letPropertyMaps, TypeMap typeMap, TypeMap[] polymorphicMaps) + { + var instanceParameter = Parameter(request.SourceType, "dto" + request.SourceType.Name); + var projection = CreateProjection(request, letPropertyMaps, typeMap, polymorphicMaps, instanceParameter); + return letPropertyMaps.Count > 0 ? letPropertyMaps.GetSubQueryExpression(this, projection, typeMap, request, instanceParameter) : new(projection, instanceParameter); } - private Expression CreateProjectionCore(ProjectionRequest request, Expression instanceParameter, TypeMap typeMap, LetPropertyMaps letPropertyMaps) + Expression CreateProjection(in ProjectionRequest request, LetPropertyMaps letPropertyMaps, TypeMap typeMap, TypeMap[] polymorphicMaps, Expression source) { - var customProjection = typeMap.CustomMapExpression?.ReplaceParameters(instanceParameter); - if (customProjection != null) + var destinationType = typeMap.DestinationType; + var projection = (polymorphicMaps.Length > 0 && destinationType.IsAbstract) ? Default(destinationType) : CreateProjectionCore(request, letPropertyMaps, typeMap, source); + foreach(var derivedMap in polymorphicMaps) { - return customProjection; + var sourceType = derivedMap.SourceType; + var derivedRequest = request.InnerRequest(sourceType, derivedMap.DestinationType); + var derivedProjection = CreateProjectionCore(derivedRequest, letPropertyMaps, derivedMap, TypeAs(source, sourceType)); + projection = Condition(TypeIs(source, sourceType), derivedProjection, projection, projection.Type); } - var propertiesProjections = new List(); - int depth; - if (OverMaxDepth()) + return projection; + Expression CreateProjectionCore(ProjectionRequest request, LetPropertyMaps letPropertyMaps, TypeMap typeMap, Expression instanceParameter) { - if (typeMap.Profile.AllowNullDestinationValues) + var customProjection = typeMap.CustomMapExpression?.ReplaceParameters(instanceParameter); + if(customProjection != null) { - return null; + return customProjection; } - } - else - { - ProjectProperties(); - } - var constructorExpression = CreateDestination(); - var expression = MemberInit(constructorExpression, propertiesProjections); - return expression; - bool OverMaxDepth() - { - depth = letPropertyMaps.IncrementDepth(request); - return typeMap.MaxDepth > 0 && depth >= typeMap.MaxDepth; - } - void ProjectProperties() - { - foreach (var propertyMap in typeMap.PropertyMaps.Where(pm => - pm.CanResolveValue && pm.DestinationMember.CanBeSet() && !typeMap.ConstructorParameterMatches(pm.DestinationName)) - .OrderBy(pm => pm.DestinationMember.MetadataToken)) + List propertiesProjections = []; + int depth; + if(OverMaxDepth()) { - var propertyProjection = TryProjectMember(propertyMap); - if (propertyProjection != null) + if(typeMap.Profile.AllowNullDestinationValues) { - propertiesProjections.Add(Bind(propertyMap.DestinationMember, propertyProjection)); + return null; } } - } - Expression TryProjectMember(MemberMap memberMap, Expression defaultSource = null) - { - var memberProjection = new MemberProjection(memberMap); - letPropertyMaps.Push(memberProjection); - var memberExpression = ShouldExpand() ? ProjectMemberCore() : null; - letPropertyMaps.Pop(); - return memberExpression; - bool ShouldExpand() => memberMap.ExplicitExpansion != true || request.ShouldExpand(letPropertyMaps.GetCurrentPath()); - Expression ProjectMemberCore() + else { - var memberTypeMap = _configuration.ResolveTypeMap(memberMap.SourceType, memberMap.DestinationType); - var resolvedSource = ResolveSource(); - memberProjection.Expression ??= resolvedSource; - var memberRequest = request.InnerRequest(resolvedSource.Type, memberMap.DestinationType); - if (memberRequest.AlreadyExists && depth >= _configuration.RecursiveQueriesMaxDepth) - { - return null; - } - Expression mappedExpression; - if (memberTypeMap != null) + ProjectProperties(); + } + var constructorExpression = CreateDestination(); + var expression = MemberInit(constructorExpression, propertiesProjections); + return expression; + bool OverMaxDepth() + { + depth = letPropertyMaps.IncrementDepth(request); + return typeMap.MaxDepth > 0 && depth >= typeMap.MaxDepth; + } + void ProjectProperties() + { + foreach(var propertyMap in typeMap.PropertyMaps) { - mappedExpression = CreateProjectionCore(memberRequest, resolvedSource, memberTypeMap, letPropertyMaps); - if (mappedExpression != null && memberTypeMap.CustomMapExpression == null && memberMap.AllowsNullDestinationValues && - resolvedSource is not ParameterExpression && !resolvedSource.Type.IsCollection()) + if(!propertyMap.CanResolveValue || !propertyMap.CanBeSet || typeMap.ConstructorParameterMatches(propertyMap.DestinationName)) { - // Handles null source property so it will not create an object with possible non-nullable properties which would result in an exception. - mappedExpression = resolvedSource.IfNullElse(Constant(null, mappedExpression.Type), mappedExpression); + continue; + } + + try + { + var propertyProjection = TryProjectMember(propertyMap); + if(propertyProjection != null) + { + propertiesProjections.Add(Bind(propertyMap.DestinationMember, propertyProjection)); + } + } + catch (Exception e) when (e is not AutoMapperConfigurationException) + { + throw new AutoMapperMappingException("Error building queryable mapping strategy.", e, propertyMap); } } - else - { - var projectionMapper = GetProjectionMapper(); - mappedExpression = projectionMapper.Project(_configuration, memberRequest, resolvedSource, letPropertyMaps); - } - return mappedExpression == null ? null : memberMap.ApplyTransformers(mappedExpression, _configuration); - Expression ResolveSource() + } + Expression TryProjectMember(MemberMap memberMap, Expression defaultSource = null) + { + MemberProjection memberProjection = new(memberMap); + letPropertyMaps.Push(memberProjection); + var memberExpression = ShouldExpand() ? ProjectMemberCore() : null; + letPropertyMaps.Pop(); + return memberExpression; + bool ShouldExpand() => memberMap.ExplicitExpansion != true || request.ShouldExpand(letPropertyMaps.GetCurrentPath()); + Expression ProjectMemberCore() { - var customSource = memberMap.IncludedMember?.ProjectToCustomSource; - var resolvedSource = memberMap switch + var memberTypeMap = _configuration.ResolveTypeMap(memberMap.SourceType, memberMap.DestinationType); + var resolvedSource = ResolveSource(); + memberProjection.Expression ??= resolvedSource; + var memberRequest = request.InnerRequest(resolvedSource.Type, memberMap.DestinationType); + if(memberRequest.AlreadyExists && depth >= _configuration.RecursiveQueriesMaxDepth) { - { CustomMapExpression: LambdaExpression mapFrom } => MapFromExpression(mapFrom), - { SourceMembers.Length: > 0 } => memberMap.ChainSourceMembers(CheckCustomSource()), - _ => defaultSource ?? throw CannotMap(memberMap, request.SourceType) - }; - if (NullSubstitute()) + return null; + } + Expression mappedExpression; + if(memberTypeMap != null) + { + mappedExpression = CreateProjection(memberRequest, letPropertyMaps, memberTypeMap, PolymorphicMaps(memberTypeMap), resolvedSource); + if(mappedExpression != null && memberTypeMap.CustomMapExpression == null && memberMap.AllowsNullDestinationValues && + resolvedSource is not ParameterExpression && !resolvedSource.Type.IsCollection()) + { + // Handles null source property so it will not create an object with possible non-nullable properties which would result in an exception. + mappedExpression = resolvedSource.IfNullElse(Default(mappedExpression.Type), mappedExpression); + } + } + else { - return memberMap.NullSubstitute(resolvedSource); + var projectionMapper = GetProjectionMapper(); + mappedExpression = projectionMapper.Project(_configuration, memberRequest, resolvedSource, letPropertyMaps); } - return resolvedSource; - Expression MapFromExpression(LambdaExpression mapFrom) + return mappedExpression == null ? null : memberMap.ApplyTransformers(mappedExpression, _configuration); + Expression ResolveSource() { - if (memberTypeMap == null || mapFrom.IsMemberPath(out _) || mapFrom.Body is ParameterExpression) + var customSource = memberMap.IncludedMember?.ProjectToCustomSource; + var resolvedSource = memberMap switch { - return mapFrom.ReplaceParameters(CheckCustomSource()); + { CustomMapExpression: LambdaExpression mapFrom } => MapFromExpression(mapFrom), + { SourceMembers.Length: > 0 } => memberMap.ChainSourceMembers(CheckCustomSource()), + _ => defaultSource ?? throw CannotMap(memberMap, request.SourceType) + }; + if(NullSubstitute()) + { + return memberMap.NullSubstitute(resolvedSource); } - if (customSource == null) + return resolvedSource; + Expression MapFromExpression(LambdaExpression mapFrom) { - memberProjection.Expression = mapFrom; - return letPropertyMaps.GetSubQueryMarker(mapFrom); + if(memberTypeMap == null || letPropertyMaps.IsDefault || mapFrom.IsMemberPath(out _) || mapFrom.Body is ParameterExpression) + { + return mapFrom.ReplaceParameters(CheckCustomSource()); + } + if(customSource == null) + { + memberProjection.Expression = mapFrom; + return letPropertyMaps.GetSubQueryMarker(mapFrom); + } + var newMapFrom = IncludedMember.Chain(customSource, mapFrom); + memberProjection.Expression = newMapFrom; + return letPropertyMaps.GetSubQueryMarker(newMapFrom); } - var newMapFrom = IncludedMember.Chain(customSource, mapFrom); - memberProjection.Expression = newMapFrom; - return letPropertyMaps.GetSubQueryMarker(newMapFrom); - } - bool NullSubstitute() => memberMap.NullSubstitute != null && resolvedSource is MemberExpression && (resolvedSource.Type.IsNullableType() || resolvedSource.Type == typeof(string)); - Expression CheckCustomSource() - { - if (customSource == null) + bool NullSubstitute() => memberMap.NullSubstitute != null && resolvedSource is MemberExpression && (resolvedSource.Type.IsNullableType() || resolvedSource.Type == typeof(string)); + Expression CheckCustomSource() { - return instanceParameter; + if(customSource == null) + { + return instanceParameter; + } + return customSource.IsMemberPath(out _) || letPropertyMaps.IsDefault ? customSource.ReplaceParameters(instanceParameter) : letPropertyMaps.GetSubQueryMarker(customSource); } - return customSource.IsMemberPath(out _) ? customSource.ReplaceParameters(instanceParameter) : letPropertyMaps.GetSubQueryMarker(customSource); } - } - IProjectionMapper GetProjectionMapper() - { - var context = memberMap.Types(); - foreach (var mapper in _projectionMappers) + IProjectionMapper GetProjectionMapper() { - if (mapper.IsMatch(context)) + var context = memberMap.Types(); + foreach(var mapper in _projectionMappers) { - return mapper; + if(mapper.IsMatch(context)) + { + return mapper; + } } + throw CannotMap(memberMap, resolvedSource.Type); } - throw CannotMap(memberMap, resolvedSource.Type); } } + NewExpression CreateDestination() => typeMap switch + { + { CustomCtorExpression: LambdaExpression ctorExpression } => (NewExpression)ctorExpression.ReplaceParameters(instanceParameter), + { ConstructorMap: { CanResolve: true } constructorMap } => + New(constructorMap.Ctor, constructorMap.CtorParams.Select(map => + { + try + { + return TryProjectMember(map, map.DefaultValue(null)) ?? Default(map.DestinationType); + } + catch (Exception e) when (e is not AutoMapperConfigurationException) + { + throw new AutoMapperMappingException("Error building constructor projection strategy.", e, map); + } + })), + _ => New(typeMap.DestinationType) + }; } - NewExpression CreateDestination() => typeMap switch - { - { CustomCtorExpression: LambdaExpression ctorExpression } => (NewExpression)ctorExpression.ReplaceParameters(instanceParameter), - { ConstructorMap: { CanResolve: true } constructorMap } => - New(constructorMap.Ctor, constructorMap.CtorParams.Select(map => TryProjectMember(map, map.DefaultValue(null)) ?? Default(map.DestinationType))), - _ => New(typeMap.DestinationType) - }; } - private static AutoMapperMappingException CannotMap(MemberMap memberMap, Type sourceType) => new( + static AutoMapperMappingException CannotMap(MemberMap memberMap, Type sourceType) => new( $"Unable to create a map expression from {memberMap.SourceMember?.DeclaringType?.Name}.{memberMap.SourceMember?.Name} ({sourceType}) to {memberMap.DestinationType.Name}.{memberMap.DestinationName} ({memberMap.DestinationType})", null, memberMap); [EditorBrowsable(EditorBrowsableState.Never)] - class FirstPassLetPropertyMaps : LetPropertyMaps + sealed class FirstPassLetPropertyMaps(IGlobalConfiguration configuration, MemberPath parentPath, TypePairCount builtProjections) : LetPropertyMaps(configuration, parentPath, builtProjections) { - readonly Stack _currentPath = new(); - readonly List _savedPaths = new(); - readonly MemberPath _parentPath; - public FirstPassLetPropertyMaps(IGlobalConfiguration configuration, MemberPath parentPath, TypePairCount builtProjections) : base(configuration, builtProjections) - => _parentPath = parentPath; + readonly List _savedPaths = []; public override Expression GetSubQueryMarker(LambdaExpression letExpression) { - var subQueryPath = new SubQueryPath(_currentPath.Reverse().ToArray(), letExpression); + SubQueryPath subQueryPath = new([.._currentPath.Reverse()], letExpression); var existingPath = _savedPaths.SingleOrDefault(s => s.IsEquivalentTo(subQueryPath)); if (existingPath.Marker != null) { @@ -215,11 +250,8 @@ public override Expression GetSubQueryMarker(LambdaExpression letExpression) _savedPaths.Add(subQueryPath); return subQueryPath.Marker; } - public override void Push(MemberProjection memberProjection) => _currentPath.Push(memberProjection); - public override MemberPath GetCurrentPath() => _parentPath.Concat( - _currentPath.Reverse().Select(p => (p.MemberMap as PropertyMap)?.DestinationMember).Where(p => p != null)); - public override void Pop() => _currentPath.Pop(); public override int Count => _savedPaths.Count; + public override bool IsDefault => false; public override LetPropertyMaps New() => new FirstPassLetPropertyMaps(Configuration, GetCurrentPath(), BuiltProjections); public override QueryExpressions GetSubQueryExpression(ProjectionBuilder builder, Expression projection, TypeMap typeMap, in ProjectionRequest request, ParameterExpression instanceParameter) { @@ -237,7 +269,7 @@ public override QueryExpressions GetSubQueryExpression(ProjectionBuilder builder } var secondParameter = Parameter(letType, "dtoLet"); ReplaceSubQueries(); - var letClause = builder.CreateProjectionCore(request, instanceParameter, letTypeMap, base.New()); + var letClause = builder.CreateProjection(request, base.New(), letTypeMap, [], instanceParameter); return new(Lambda(projection, secondParameter), Lambda(letClause, instanceParameter)); void ReplaceSubQueries() { @@ -260,18 +292,7 @@ public Expression GetSourceExpression(Expression parameter) for (int index = 0; index < Members.Length - 1; index++) { var sourceMember = Members[index].Expression; - if (sourceMember is LambdaExpression lambda) - { - sourceExpression = lambda.ReplaceParameters(sourceExpression); - } - else - { - var chain = sourceMember.GetChain(); - if (chain.TryPeek(out var first)) - { - sourceExpression = sourceMember.Replace(first.Target, sourceExpression); - } - } + sourceExpression = sourceMember is LambdaExpression lambda ? lambda.ReplaceParameters(sourceExpression) : sourceMember; } return sourceExpression; } @@ -279,11 +300,10 @@ public Expression GetSourceExpression(Expression parameter) internal bool IsEquivalentTo(SubQueryPath other) => LetExpression == other.LetExpression && Members.Length == other.Members.Length && Members.Take(Members.Length - 1).Zip(other.Members, (left, right) => left.MemberMap == right.MemberMap).All(item => item); } - class GePropertiesVisitor : ExpressionVisitor + sealed class GePropertiesVisitor(Expression target) : ExpressionVisitor { - private readonly Expression _target; - public List Members { get; } = new(); - public GePropertiesVisitor(Expression target) => _target = target; + readonly Expression _target = target; + public List Members { get; } = []; protected override Expression VisitMember(MemberExpression node) { if(node.Expression == _target) @@ -294,19 +314,14 @@ protected override Expression VisitMember(MemberExpression node) } public static IEnumerable Retrieve(Expression expression, Expression target) { - var visitor = new GePropertiesVisitor(target); + GePropertiesVisitor visitor = new(target); visitor.Visit(expression); return visitor.Members.Select(member => new PropertyDescription(member.Name, member.GetMemberType())); } } - class ReplaceMemberAccessesVisitor : ExpressionVisitor + sealed class ReplaceMemberAccessesVisitor(Expression oldObject, Expression newObject) : ExpressionVisitor { - private readonly Expression _oldObject, _newObject; - public ReplaceMemberAccessesVisitor(Expression oldObject, Expression newObject) - { - _oldObject = oldObject; - _newObject = newObject; - } + readonly Expression _oldObject = oldObject, _newObject = newObject; protected override Expression VisitMember(MemberExpression node) { if(node.Expression != _oldObject) @@ -321,10 +336,13 @@ protected override Expression VisitMember(MemberExpression node) [EditorBrowsable(EditorBrowsableState.Never)] public class LetPropertyMaps { - protected LetPropertyMaps(IGlobalConfiguration configuration, TypePairCount builtProjections) + protected private readonly Stack _currentPath = []; + readonly MemberPath _parentPath; + protected internal LetPropertyMaps(IGlobalConfiguration configuration, MemberPath parentPath, TypePairCount builtProjections) { Configuration = configuration; BuiltProjections = builtProjections; + _parentPath = parentPath; } protected TypePairCount BuiltProjections { get; } public int IncrementDepth(in ProjectionRequest request) @@ -337,14 +355,16 @@ public int IncrementDepth(in ProjectionRequest request) return depth; } public virtual Expression GetSubQueryMarker(LambdaExpression letExpression) => letExpression.Body; - public virtual void Push(MemberProjection memberProjection) { } - public virtual MemberPath GetCurrentPath() => MemberPath.Empty; - public virtual void Pop() {} + public void Push(MemberProjection memberProjection) => _currentPath.Push(memberProjection); + public MemberPath GetCurrentPath() => _parentPath.Concat( + _currentPath.Reverse().Select(p => (p.MemberMap as PropertyMap)?.DestinationMember).Where(p => p != null)); + public void Pop() => _currentPath.Pop(); public virtual int Count => 0; public IGlobalConfiguration Configuration { get; } - public virtual LetPropertyMaps New() => new(Configuration, BuiltProjections); + public virtual LetPropertyMaps New() => new(Configuration, GetCurrentPath(), BuiltProjections); public virtual QueryExpressions GetSubQueryExpression(ProjectionBuilder builder, Expression projection, TypeMap typeMap, in ProjectionRequest request, ParameterExpression instanceParameter) => default; + public virtual bool IsDefault => true; } [EditorBrowsable(EditorBrowsableState.Never)] public readonly record struct QueryExpressions(LambdaExpression Projection, LambdaExpression LetClause = null) @@ -357,57 +377,54 @@ internal QueryExpressions Prepare(bool enableNullPropagationForQueryMapping, obj return new(Prepare(Projection), Prepare(LetClause)); LambdaExpression Prepare(Expression cachedExpression) { - var result = parameters == null ? cachedExpression : ParameterExpressionVisitor.SetParameters(parameters, cachedExpression); + var result = parameters == null ? cachedExpression : ParameterVisitor.SetParameters(parameters, cachedExpression); return (LambdaExpression)(enableNullPropagationForQueryMapping ? NullsafeQueryRewriter.NullCheck(result) : result); } } } -public class MemberProjection +public sealed record MemberProjection(MemberMap MemberMap) { - public MemberProjection(MemberMap memberMap) => MemberMap = memberMap; public Expression Expression { get; set; } - public MemberMap MemberMap { get; } } -abstract class ParameterExpressionVisitor : ExpressionVisitor +abstract class ParameterVisitor : ExpressionVisitor { public static Expression SetParameters(object parameters, Expression expression) { - var visitor = parameters is ParameterBag dictionary ? (ParameterExpressionVisitor)new ConstantExpressionReplacementVisitor(dictionary) : new ObjectParameterExpressionReplacementVisitor(parameters); + ParameterVisitor visitor = parameters is ParameterBag dictionary ? new ConstantVisitor(dictionary) : new PropertyVisitor(parameters); return visitor.Visit(expression); } protected abstract Expression GetValue(string name); protected override Expression VisitMember(MemberExpression node) { - if (!node.Member.DeclaringType.Has()) + var member = node.Member; + if (!member.DeclaringType.Has()) { return base.VisitMember(node); } - var parameterName = node.Member.Name; + var parameterName = member.Name; var parameterValue = GetValue(parameterName); if (parameterValue == null) { const string VbPrefix = "$VB$Local_"; - if (!parameterName.StartsWith(VbPrefix, StringComparison.Ordinal) || (parameterValue = GetValue(parameterName.Substring(VbPrefix.Length))) == null) + if (!parameterName.StartsWith(VbPrefix, StringComparison.Ordinal) || (parameterValue = GetValue(parameterName[VbPrefix.Length..])) == null) { return base.VisitMember(node); } } - return ToType(parameterValue, node.Member.GetMemberType()); + return ToType(parameterValue, member.GetMemberType()); } - class ObjectParameterExpressionReplacementVisitor : ParameterExpressionVisitor + sealed class PropertyVisitor(object parameters) : ParameterVisitor { - private readonly object _parameters; - public ObjectParameterExpressionReplacementVisitor(object parameters) => _parameters = parameters; + readonly object _parameters = parameters; protected override Expression GetValue(string name) { var matchingMember = _parameters.GetType().GetProperty(name); return matchingMember != null ? Property(Constant(_parameters), matchingMember) : null; } } - class ConstantExpressionReplacementVisitor : ParameterExpressionVisitor + sealed class ConstantVisitor(ParameterBag paramValues) : ParameterVisitor { - private readonly ParameterBag _paramValues; - public ConstantExpressionReplacementVisitor(ParameterBag paramValues) => _paramValues = paramValues; + readonly ParameterBag _paramValues = paramValues; protected override Expression GetValue(string name) => _paramValues.TryGetValue(name, out object parameterValue) ? Constant(parameterValue) : null; } } @@ -426,7 +443,7 @@ public bool Equals(ProjectionRequest other) => SourceType == other.SourceType && MembersToExpand.SequenceEqual(other.MembersToExpand); public override int GetHashCode() { - var hashCode = new HashCode(); + HashCode hashCode = new(); hashCode.Add(SourceType); hashCode.Add(DestinationType); foreach (var member in MembersToExpand) diff --git a/src/AutoMapper/QueryableExtensions/ProjectionMappers/AssignableProjectionMapper.cs b/src/AutoMapper/QueryableExtensions/ProjectionMappers/AssignableProjectionMapper.cs index f92c42f1a7..a996e2f1da 100644 --- a/src/AutoMapper/QueryableExtensions/ProjectionMappers/AssignableProjectionMapper.cs +++ b/src/AutoMapper/QueryableExtensions/ProjectionMappers/AssignableProjectionMapper.cs @@ -1,6 +1,6 @@ namespace AutoMapper.QueryableExtensions.Impl; [EditorBrowsable(EditorBrowsableState.Never)] -public class AssignableProjectionMapper : IProjectionMapper +public sealed class AssignableProjectionMapper : IProjectionMapper { public bool IsMatch(TypePair context) => context.DestinationType.IsAssignableFrom(context.SourceType); public Expression Project(IGlobalConfiguration configuration, in ProjectionRequest request, Expression resolvedSource, LetPropertyMaps letPropertyMaps) diff --git a/src/AutoMapper/QueryableExtensions/ProjectionMappers/EnumProjectionMapper.cs b/src/AutoMapper/QueryableExtensions/ProjectionMappers/EnumProjectionMapper.cs index f2524549d7..37c327dc40 100644 --- a/src/AutoMapper/QueryableExtensions/ProjectionMappers/EnumProjectionMapper.cs +++ b/src/AutoMapper/QueryableExtensions/ProjectionMappers/EnumProjectionMapper.cs @@ -1,6 +1,6 @@ namespace AutoMapper.QueryableExtensions.Impl; [EditorBrowsable(EditorBrowsableState.Never)] -public class EnumProjectionMapper : IProjectionMapper +public sealed class EnumProjectionMapper : IProjectionMapper { public Expression Project(IGlobalConfiguration configuration, in ProjectionRequest request, Expression resolvedSource, LetPropertyMaps letPropertyMaps) => Convert(resolvedSource, request.DestinationType); diff --git a/src/AutoMapper/QueryableExtensions/ProjectionMappers/EnumerableProjectionMapper.cs b/src/AutoMapper/QueryableExtensions/ProjectionMappers/EnumerableProjectionMapper.cs index 16b9a3ed6e..dc4ec7aa6e 100644 --- a/src/AutoMapper/QueryableExtensions/ProjectionMappers/EnumerableProjectionMapper.cs +++ b/src/AutoMapper/QueryableExtensions/ProjectionMappers/EnumerableProjectionMapper.cs @@ -1,7 +1,7 @@ namespace AutoMapper.QueryableExtensions.Impl; using static ReflectionHelper; [EditorBrowsable(EditorBrowsableState.Never)] -public class EnumerableProjectionMapper : IProjectionMapper +public sealed class EnumerableProjectionMapper : IProjectionMapper { private static readonly MethodInfo SelectMethod = typeof(Enumerable).StaticGenericMethod("Select", parametersCount: 2); private static readonly MethodInfo ToArrayMethod = typeof(Enumerable).GetStaticMethod("ToArray"); @@ -33,7 +33,7 @@ public Expression Project(IGlobalConfiguration configuration, in ProjectionReque } else { - var ctorInfo = destinationType.GetConstructor(new[] { sourceExpression.Type }); + var ctorInfo = destinationType.GetConstructor([sourceExpression.Type]); if (ctorInfo is not null) { sourceExpression = New(ctorInfo, sourceExpression); diff --git a/src/AutoMapper/QueryableExtensions/ProjectionMappers/NullableSourceProjectionMapper.cs b/src/AutoMapper/QueryableExtensions/ProjectionMappers/NullableSourceProjectionMapper.cs index f3b7f77e66..dd0c137a0b 100644 --- a/src/AutoMapper/QueryableExtensions/ProjectionMappers/NullableSourceProjectionMapper.cs +++ b/src/AutoMapper/QueryableExtensions/ProjectionMappers/NullableSourceProjectionMapper.cs @@ -1,5 +1,5 @@ namespace AutoMapper.QueryableExtensions.Impl; -internal class NullableSourceProjectionMapper : IProjectionMapper +public sealed class NullableSourceProjectionMapper : IProjectionMapper { public Expression Project(IGlobalConfiguration configuration, in ProjectionRequest request, Expression resolvedSource, LetPropertyMaps letPropertyMaps) => Coalesce(resolvedSource, New(request.DestinationType)); diff --git a/src/AutoMapper/QueryableExtensions/ProjectionMappers/StringProjectionMapper.cs b/src/AutoMapper/QueryableExtensions/ProjectionMappers/StringProjectionMapper.cs index a635ce7c2d..06f4291b6c 100644 --- a/src/AutoMapper/QueryableExtensions/ProjectionMappers/StringProjectionMapper.cs +++ b/src/AutoMapper/QueryableExtensions/ProjectionMappers/StringProjectionMapper.cs @@ -1,7 +1,7 @@ namespace AutoMapper.QueryableExtensions.Impl; [EditorBrowsable(EditorBrowsableState.Never)] -public class StringProjectionMapper : IProjectionMapper +public sealed class StringProjectionMapper : IProjectionMapper { public bool IsMatch(TypePair context) => context.DestinationType == typeof(string); public Expression Project(IGlobalConfiguration configuration, in ProjectionRequest request, Expression resolvedSource, LetPropertyMaps letPropertyMaps) diff --git a/src/AutoMapper/ResolutionContext.cs b/src/AutoMapper/ResolutionContext.cs index 759b12b11e..9f411f1264 100644 --- a/src/AutoMapper/ResolutionContext.cs +++ b/src/AutoMapper/ResolutionContext.cs @@ -1,9 +1,9 @@ +using System.Runtime.CompilerServices; namespace AutoMapper; - /// /// Context information regarding resolution of a destination value /// -public class ResolutionContext : IInternalRuntimeMapper +public sealed class ResolutionContext : IInternalRuntimeMapper { private Dictionary _instanceCache; private Dictionary _typeDepth; @@ -15,7 +15,13 @@ internal ResolutionContext(IInternalRuntimeMapper mapper, IMappingOperationOptio _options = options; } /// + /// The state passed in the options of the Map call. + /// Mutually exclusive with per Map call. + /// + public object State => _options?.State; + /// /// The items passed in the options of the Map call. + /// Mutually exclusive with per Map call. /// public Dictionary Items { @@ -45,7 +51,7 @@ public Dictionary InstanceCache get { CheckDefault(); - return _instanceCache ??= new(); + return _instanceCache ??= []; } } /// @@ -56,7 +62,7 @@ private Dictionary TypeDepth get { CheckDefault(); - return _typeDepth ??= new(); + return _typeDepth ??= []; } } TDestination IMapperBase.Map(object source) => ((IMapperBase)this).Map(source, default(TDestination)); @@ -103,4 +109,8 @@ private void CheckDefault() } private static void ThrowInvalidMap() => throw new InvalidOperationException("Context.Items are only available when using a Map overload that takes Action! Consider using Context.TryGetItems instead."); } -public readonly record struct ContextCacheKey(object Source, Type DestinationType); \ No newline at end of file +public readonly record struct ContextCacheKey(object Source, Type DestinationType) +{ + public override int GetHashCode() => HashCode.Combine(DestinationType, RuntimeHelpers.GetHashCode(Source)); + public bool Equals(ContextCacheKey other) => DestinationType == other.DestinationType && Source == other.Source; +} \ No newline at end of file diff --git a/src/AutoMapper/ServiceCollectionExtensions.cs b/src/AutoMapper/ServiceCollectionExtensions.cs new file mode 100644 index 0000000000..16eb00b33c --- /dev/null +++ b/src/AutoMapper/ServiceCollectionExtensions.cs @@ -0,0 +1,89 @@ +using Microsoft.Extensions.Logging; + +namespace Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using AutoMapper; +using AutoMapper.Internal; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; + +/// +/// Extensions to scan for AutoMapper classes and register the configuration, mapping, and extensions with the service collection: +/// +/// Finds classes and initializes a new , +/// Scans for , , and implementations and registers them as , +/// Registers as , and +/// Registers as a configurable (default is ) +/// +/// After calling AddAutoMapper you can resolve an instance from a scoped service provider, or as a dependency +/// To use you can resolve the instance directly for from an instance. +/// +public static class ServiceCollectionExtensions +{ + static readonly Type[] AmTypes = [typeof(IValueResolver<,,>), typeof(IMemberValueResolver<,,,>), typeof(ITypeConverter<,>), typeof(IValueConverter<,>), typeof(IMappingAction<,>)]; + public static IServiceCollection AddAutoMapper(this IServiceCollection services, Action configAction) + => AddAutoMapperClasses(services, (sp, cfg) => configAction?.Invoke(cfg), null); + + public static IServiceCollection AddAutoMapper(this IServiceCollection services, Action configAction, params Assembly[] assemblies) + => AddAutoMapperClasses(services, (sp, cfg) => configAction?.Invoke(cfg), assemblies); + + public static IServiceCollection AddAutoMapper(this IServiceCollection services, Action configAction, params Assembly[] assemblies) + => AddAutoMapperClasses(services, configAction, assemblies); + + public static IServiceCollection AddAutoMapper(this IServiceCollection services, Action configAction, IEnumerable assemblies, ServiceLifetime serviceLifetime = ServiceLifetime.Transient) + => AddAutoMapperClasses(services, (sp, cfg) => configAction?.Invoke(cfg), assemblies, serviceLifetime); + + public static IServiceCollection AddAutoMapper(this IServiceCollection services, Action configAction, IEnumerable assemblies, ServiceLifetime serviceLifetime = ServiceLifetime.Transient) + => AddAutoMapperClasses(services, configAction, assemblies, serviceLifetime); + + public static IServiceCollection AddAutoMapper(this IServiceCollection services, Action configAction, params Type[] profileAssemblyMarkerTypes) + => AddAutoMapperClasses(services, (sp, cfg) => configAction?.Invoke(cfg), profileAssemblyMarkerTypes.Select(t => t.GetTypeInfo().Assembly)); + + public static IServiceCollection AddAutoMapper(this IServiceCollection services, Action configAction, params Type[] profileAssemblyMarkerTypes) + => AddAutoMapperClasses(services, configAction, profileAssemblyMarkerTypes.Select(t => t.GetTypeInfo().Assembly)); + + public static IServiceCollection AddAutoMapper(this IServiceCollection services, Action configAction, + IEnumerable profileAssemblyMarkerTypes, ServiceLifetime serviceLifetime = ServiceLifetime.Transient) + => AddAutoMapperClasses(services, (sp, cfg) => configAction?.Invoke(cfg), profileAssemblyMarkerTypes.Select(t => t.GetTypeInfo().Assembly), serviceLifetime); + + public static IServiceCollection AddAutoMapper(this IServiceCollection services, Action configAction, + IEnumerable profileAssemblyMarkerTypes, ServiceLifetime serviceLifetime = ServiceLifetime.Transient) + => AddAutoMapperClasses(services, configAction, profileAssemblyMarkerTypes.Select(t => t.GetTypeInfo().Assembly), serviceLifetime); + + private static IServiceCollection AddAutoMapperClasses(IServiceCollection services, Action configAction, + IEnumerable assembliesToScan, ServiceLifetime serviceLifetime = ServiceLifetime.Transient) + { + if (configAction != null) + { + services.AddOptions().Configure((options, sp) => configAction(sp, options)); + } + if (assembliesToScan != null) + { + assembliesToScan = assembliesToScan.Where(a => !a.IsDynamic && a != typeof(Mapper).Assembly).Distinct(); + services.Configure(options => options.AddMaps(assembliesToScan)); + foreach (var type in assembliesToScan.SelectMany(a => a.GetTypes().Where(type => type.IsClass && !type.IsAbstract && IsAmType(type)))) + { + // use try add to avoid double-registration + services.TryAddTransient(type); + } + } + // Just return if we've already added AutoMapper to avoid double-registration + if (services.Any(sd => sd.ServiceType == typeof(IMapper))) + { + return services; + } + services.AddSingleton(sp => + { + // A mapper configuration is required + var options = sp.GetRequiredService>(); + var loggerFactory = sp.GetRequiredService(); + return new MapperConfiguration(options.Value, loggerFactory); + }); + services.Add(new(typeof(IMapper), sp => new Mapper(sp.GetRequiredService(), sp.GetService), serviceLifetime)); + return services; + bool IsAmType(Type type) => Array.Exists(AmTypes, openType => type.GetGenericInterface(openType) != null); + } +} \ No newline at end of file diff --git a/src/AutoMapper/TypeMap.cs b/src/AutoMapper/TypeMap.cs index 08f981772d..07ef03c48b 100644 --- a/src/AutoMapper/TypeMap.cs +++ b/src/AutoMapper/TypeMap.cs @@ -7,7 +7,7 @@ namespace AutoMapper; /// [DebuggerDisplay("{SourceType.Name} -> {DestinationType.Name}")] [EditorBrowsable(EditorBrowsableState.Never)] -public class TypeMap +public sealed class TypeMap { static readonly LambdaExpression EmptyLambda = Lambda(ExpressionBuilder.Empty); static readonly MethodInfo CreateProxyMethod = typeof(ObjectFactory).GetStaticMethod(nameof(ObjectFactory.CreateInterfaceProxy)); @@ -24,7 +24,7 @@ public TypeMap(Type sourceType, Type destinationType, ProfileMap profile, TypeMa } SourceTypeDetails = profile.CreateTypeDetails(sourceType); DestinationTypeDetails = profile.CreateTypeDetails(destinationType); - sourceMembers ??= new(); + sourceMembers ??= []; var isReverseMap = typeMapConfiguration?.IsReverseMap is true; foreach (var destinationProperty in DestinationTypeDetails.WriteAccessors) { @@ -43,9 +43,10 @@ public TypeMap(Type sourceType, Type destinationType, ProfileMap profile, TypeMa } } public string CheckRecord() => ConstructorMap?.Ctor is ConstructorInfo ctor && ctor.IsFamily && ctor.Has() ? - " When mapping to records, consider using only public constructors. See https://docs.automapper.org/en/latest/Construction.html." : null; + " When mapping to records, consider using only public constructors. See https://docs.automapper.io/en/latest/Construction.html." : null; public Features Features => Details.Features; private TypeMapDetails Details => _details ??= new(); + public bool HasDetails => _details != null; public void CheckProjection() { if (Projection) @@ -71,14 +72,14 @@ internal bool CanConstructorMap() => Profile.ConstructorMappingEnabled && !Desti public ProfileMap Profile { get; } public LambdaExpression CustomMapExpression => TypeConverter?.ProjectToExpression; public LambdaExpression CustomCtorFunction { get => _details?.CustomCtorFunction; set => Details.CustomCtorFunction = value; } - public LambdaExpression CustomCtorExpression => CustomCtorFunction?.Parameters.Count == 1 ? CustomCtorFunction : null; + public LambdaExpression CustomCtorExpression => CustomCtorFunction is { Parameters: [_] } ? CustomCtorFunction : null; public bool IncludeAllDerivedTypes { get => (_details?.IncludeAllDerivedTypes).GetValueOrDefault(); set => Details.IncludeAllDerivedTypes = value; } public MemberList ConfiguredMemberList { get => (_details?.ConfiguredMemberList).GetValueOrDefault(); - set + set { - if (value == default) + if (_details == null && value == default) { return; } @@ -121,8 +122,8 @@ public IEnumerable MemberMaps public bool HasTypeConverter => TypeConverter != null; public Execution.TypeConverter TypeConverter { get; set; } public bool ShouldCheckForValid => ConfiguredMemberList != MemberList.None && !HasTypeConverter; - public LambdaExpression[] IncludedMembers { get => _details?.IncludedMembers ?? Array.Empty(); set => Details.IncludedMembers = value; } - public string[] IncludedMembersNames { get => _details?.IncludedMembersNames ?? Array.Empty(); set => Details.IncludedMembersNames = value; } + public LambdaExpression[] IncludedMembers { get => _details?.IncludedMembers ?? []; set => Details.IncludedMembers = value; } + public string[] IncludedMembersNames { get => _details?.IncludedMembersNames ?? []; set => Details.IncludedMembersNames = value; } public IReadOnlyCollection IncludedMembersTypeMaps => (_details?.IncludedMembersTypeMaps).NullCheck(); public Type MakeGenericType(Type type) => type.IsGenericTypeDefinition ? type.MakeGenericType(SourceType.GenericTypeArguments.Concat(DestinationType.GenericTypeArguments).Take(type.GenericParametersCount()).ToArray()) : @@ -133,13 +134,13 @@ public IEnumerable GetAllIncludedMembers() => IncludedMembersN public bool ConstructorParameterMatches(string destinationPropertyName) => ConstructorMapping && ConstructorMap[destinationPropertyName] != null; private void AddPropertyMap(MemberInfo destProperty, Type destinationPropertyType, List sourceMembers) { - var propertyMap = new PropertyMap(destProperty, destinationPropertyType, this); - propertyMap.MapByConvention(sourceMembers.ToArray()); + PropertyMap propertyMap = new(destProperty, destinationPropertyType, this); + propertyMap.MapByConvention([..sourceMembers]); AddPropertyMap(propertyMap); } private void AddPropertyMap(PropertyMap propertyMap) { - _propertyMaps ??= new(); + _propertyMaps ??= []; _propertyMaps.Add(propertyMap); } public string[] GetUnmappedPropertyNames() @@ -156,14 +157,14 @@ public string[] GetUnmappedPropertyNames() else { var ignoredSourceMembers = _details?.SourceMemberConfigs? - .Where(smc => smc.IsIgnored()) + .Where(smc => smc.Ignored) .Select(pm => pm.SourceMember.Name); properties = Profile.CreateTypeDetails(SourceType).ReadAccessors .Select(p => p.Name) .Except(MappedMembers().Select(m => m.GetSourceMemberName())) .Except(IncludedMembersNames) .Except(IncludedMembers.Select(m => m.GetMember()?.Name)) - .Except(ignoredSourceMembers ?? Array.Empty()); + .Except(ignoredSourceMembers ?? []); } return properties.Where(memberName => !Profile.GlobalIgnores.Any(memberName.StartsWith)).ToArray(); IEnumerable MappedMembers() => MemberMaps.Where(pm => pm.IsMapped); @@ -203,8 +204,15 @@ public void Seal(IGlobalConfiguration configuration) return; } _sealed = true; - _details?.Seal(configuration, this); - MapExpression = Projection ? EmptyLambda : CreateMapperLambda(configuration); + try + { + _details?.Seal(configuration, this); + MapExpression = Projection ? EmptyLambda : CreateMapperLambda(configuration); + } + catch (Exception e) when (e is not AutoMapperConfigurationException) + { + throw new AutoMapperMappingException("Error creating mapping strategy.", e, this); + } SourceTypeDetails = null; DestinationTypeDetails = null; } @@ -305,10 +313,11 @@ public void Seal(IGlobalConfiguration configuration, TypeMap thisMap) { foreach (var inheritedTypeMap in InheritedTypeMaps) { - var includedMaps = inheritedTypeMap?._details?.IncludedMembersTypeMaps; + inheritedTypeMap.Seal(configuration); + var includedMaps = inheritedTypeMap._details?.IncludedMembersTypeMaps; if (includedMaps != null) { - IncludedMembersTypeMaps ??= new(); + IncludedMembersTypeMaps ??= []; IncludedMembersTypeMaps.TryAdd(includedMaps); } } @@ -332,27 +341,27 @@ public void Seal(IGlobalConfiguration configuration, TypeMap thisMap) } public void IncludeDerivedTypes(TypePair derivedTypes) { - IncludedDerivedTypes ??= new(); + IncludedDerivedTypes ??= []; IncludedDerivedTypes.TryAdd(derivedTypes); } public void AddBeforeMapAction(LambdaExpression beforeMap) { - BeforeMapActions ??= new(); + BeforeMapActions ??= []; BeforeMapActions.TryAdd(beforeMap); } public void AddAfterMapAction(LambdaExpression afterMap) { - AfterMapActions ??= new(); + AfterMapActions ??= []; AfterMapActions.TryAdd(afterMap); } public void AddValueTransformation(ValueTransformerConfiguration valueTransformerConfiguration) { - ValueTransformerConfigs ??= new(); + ValueTransformerConfigs ??= []; ValueTransformerConfigs.Add(valueTransformerConfiguration); } public PathMap FindOrCreatePathMapFor(LambdaExpression destinationExpression, MemberPath path, TypeMap typeMap) { - PathMaps ??= new(); + PathMaps ??= []; var pathMap = GetPathMap(path); if (pathMap == null) { @@ -379,22 +388,22 @@ private PathMap GetPathMap(MemberPath memberPath) private void AddPathMap(PathMap pathMap) => PathMaps.Add(pathMap); public void IncludeBaseTypes(TypePair baseTypes) { - IncludedBaseTypes ??= new(); + IncludedBaseTypes ??= []; IncludedBaseTypes.TryAdd(baseTypes); } internal void CopyInheritedMapsTo(TypeMap typeMap) { - typeMap.Details.InheritedTypeMaps ??= new(); + typeMap.Details.InheritedTypeMaps ??= []; typeMap._details.InheritedTypeMaps.TryAdd(InheritedTypeMaps); } public bool AddMemberMap(IncludedMember includedMember) { - IncludedMembersTypeMaps ??= new(); + IncludedMembersTypeMaps ??= []; return IncludedMembersTypeMaps.TryAdd(includedMember); } public SourceMemberConfig FindOrCreateSourceMemberConfigFor(MemberInfo sourceMember) { - SourceMemberConfigs ??= new(); + SourceMemberConfigs ??= []; var config = GetSourceMemberConfig(sourceMember); if (config == null) { @@ -416,7 +425,7 @@ private SourceMemberConfig GetSourceMemberConfig(MemberInfo sourceMember) } public void AddInheritedMap(TypeMap inheritedTypeMap) { - InheritedTypeMaps ??= new(); + InheritedTypeMaps ??= []; InheritedTypeMaps.TryAdd(inheritedTypeMap); } private void ApplyIncludedMemberTypeMap(IncludedMember includedMember, TypeMap thisMap) @@ -427,7 +436,7 @@ private void ApplyIncludedMemberTypeMap(IncludedMember includedMember, TypeMap t .Select(p => new PropertyMap(p, thisMap, includedMember)) .ToArray(); var notOverridenPathMaps = NotOverridenPathMaps(typeMap); - var appliedConstructorMap = thisMap.ConstructorMap?.ApplyIncludedMember(includedMember); + var appliedConstructorMap = thisMap.ConstructorMap?.ApplyMap(typeMap, includedMember); if (includedMemberMaps.Length == 0 && notOverridenPathMaps.Length == 0 && appliedConstructorMap is not true) { return; @@ -456,6 +465,7 @@ private void ApplyInheritedTypeMap(TypeMap inheritedTypeMap, TypeMap thisMap) { ApplyInheritedPropertyMaps(inheritedTypeMap, thisMap); } + thisMap.ConstructorMap?.ApplyMap(inheritedTypeMap); var inheritedDetails = inheritedTypeMap._details; if (inheritedDetails == null) { @@ -473,7 +483,7 @@ private void ApplyInheritedTypeMap(TypeMap inheritedTypeMap, TypeMap thisMap) } if (inheritedDetails.ValueTransformerConfigs != null) { - ValueTransformerConfigs ??= new(); + ValueTransformerConfigs ??= []; ValueTransformerConfigs.InsertRange(0, inheritedDetails.ValueTransformerConfigs); } return; @@ -498,7 +508,7 @@ void ApplyInheritedPropertyMaps(TypeMap inheritedTypeMap, TypeMap thisMap) } void ApplyInheritedSourceMembers(TypeMapDetails inheritedTypeMap) { - SourceMemberConfigs ??= new(); + SourceMemberConfigs ??= []; foreach (var inheritedSourceConfig in inheritedTypeMap.SourceMemberConfigs) { if (GetSourceMemberConfig(inheritedSourceConfig.SourceMember) == null) @@ -512,12 +522,12 @@ void ApplyInheritedMapActions(IEnumerable beforeMap, IEnumerab { if (beforeMap != null) { - BeforeMapActions ??= new(); + BeforeMapActions ??= []; BeforeMapActions.TryAdd(beforeMap); } if (afterMap != null) { - AfterMapActions ??= new(); + AfterMapActions ??= []; AfterMapActions.TryAdd(afterMap); } } @@ -525,9 +535,9 @@ private PathMap[] NotOverridenPathMaps(TypeMap inheritedTypeMap) { if (inheritedTypeMap.PathMaps.Count == 0) { - return Array.Empty(); + return []; } - PathMaps ??= new(); + PathMaps ??= []; return inheritedTypeMap.PathMaps.Where(baseConfig => GetPathMap(baseConfig.MemberPath) == null).ToArray(); } } diff --git a/src/Benchmark/BenchEngine.cs b/src/Benchmark/BenchEngine.cs index 7cc527f21f..bd3d074050 100644 --- a/src/Benchmark/BenchEngine.cs +++ b/src/Benchmark/BenchEngine.cs @@ -14,17 +14,17 @@ public BenchEngine(IObjectToObjectMapper mapper, string mode) public void Start() { _mapper.Initialize(); - //_mapper.Map(); + _mapper.Map(); - //var timer = Stopwatch.StartNew(); + var timer = Stopwatch.StartNew(); - //for (int i = 0; i < 1_000_000; i++) - //{ - // _mapper.Map(); - //} + for(int i = 0; i < 1_000_000; i++) + { + _mapper.Map(); + } - //timer.Stop(); + timer.Stop(); - //Console.WriteLine("{2:D3} ms {0}: - {1}", _mapper.Name, _mode, (int)timer.Elapsed.TotalMilliseconds); + Console.WriteLine("{2:D3} ms {0}: - {1}", _mapper.Name, _mode, (int)timer.Elapsed.TotalMilliseconds); } } \ No newline at end of file diff --git a/src/Benchmark/Benchmark.csproj b/src/Benchmark/Benchmark.csproj index eccda679ed..0667bca2d9 100644 --- a/src/Benchmark/Benchmark.csproj +++ b/src/Benchmark/Benchmark.csproj @@ -1,7 +1,7 @@  - net7.0 + net10.0 Exe diff --git a/src/Benchmark/FlatteningMapper.cs b/src/Benchmark/FlatteningMapper.cs index b43760d5b4..6156086e88 100644 --- a/src/Benchmark/FlatteningMapper.cs +++ b/src/Benchmark/FlatteningMapper.cs @@ -1,4 +1,6 @@ using AutoMapper; +using Microsoft.Extensions.Logging.Abstractions; + namespace Benchmark.Flattening; static class Config @@ -50,7 +52,7 @@ private static IMapper CreateMapper() cfg.AddProfile(new ComplexTypesProfile()); cfg.AddProfile(new ConstructorProfile()); cfg.AddProfile(new FlatteningProfile()); - }); + }, new NullLoggerFactory()); //config.AssertConfigurationIsValid(); return config.CreateMapper(); } diff --git a/src/Benchmark/Program.cs b/src/Benchmark/Program.cs index fd1d9f7037..ff5418f1c2 100644 --- a/src/Benchmark/Program.cs +++ b/src/Benchmark/Program.cs @@ -13,7 +13,7 @@ public static void Main(string[] args) { "Complex", new IObjectToObjectMapper[] { new ComplexTypeMapper(), new ManualComplexTypeMapper() } }, { "Deep", new IObjectToObjectMapper[] { new DeepTypeMapper(), new ManualDeepTypeMapper() } } }; - //while (true) + while (true) { foreach (var pair in mappers) { @@ -22,7 +22,7 @@ public static void Main(string[] args) new BenchEngine(mapper, pair.Key).Start(); } } - //Console.ReadLine(); + Console.ReadLine(); } } } diff --git a/src/IntegrationTests/AutoMapper.IntegrationTests.csproj b/src/IntegrationTests/AutoMapper.IntegrationTests.csproj index d6422a78b7..c45d47acaa 100644 --- a/src/IntegrationTests/AutoMapper.IntegrationTests.csproj +++ b/src/IntegrationTests/AutoMapper.IntegrationTests.csproj @@ -1,18 +1,21 @@  - net7.0 + net10.0 $(NoWarn);618 + ..\..\AutoMapper.snk + true - - - - - + + + + + + diff --git a/src/IntegrationTests/BuiltInTypes/ByteArray.cs b/src/IntegrationTests/BuiltInTypes/ByteArray.cs index 2fbe292b10..71e95dae18 100644 --- a/src/IntegrationTests/BuiltInTypes/ByteArray.cs +++ b/src/IntegrationTests/BuiltInTypes/ByteArray.cs @@ -1,6 +1,6 @@ namespace AutoMapper.IntegrationTests.BuiltInTypes; -public class ByteArrayColumns : IntegrationTest +public class ByteArrayColumns(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { public class Customer { @@ -47,7 +47,7 @@ protected override void Seed(Context context) [Fact] public void Can_map_with_projection() { - using (var context = new Context()) + using (var context = Fixture.CreateContext()) { var customerVms = ProjectTo(context.Customers).ToList(); customerVms.ForEach(x => diff --git a/src/IntegrationTests/BuiltInTypes/ConvertUsing.cs b/src/IntegrationTests/BuiltInTypes/ConvertUsing.cs index 2e6aa15799..d08d7a8d34 100644 --- a/src/IntegrationTests/BuiltInTypes/ConvertUsing.cs +++ b/src/IntegrationTests/BuiltInTypes/ConvertUsing.cs @@ -1,6 +1,6 @@ namespace AutoMapper.IntegrationTests.BuiltInTypes; -public class ConvertUsingWithNullables : IntegrationTest +public class ConvertUsingWithNullables(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { public class MyProfile : Profile { @@ -53,7 +53,7 @@ public class TestContext : LocalDbContext [Fact] public void Should_project_ok() { - using(var context = new TestContext()) + using(var context = Fixture.CreateContext()) { var results = ProjectTo(context.MyTable).ToList(); results[0].Id.ShouldBe(1); @@ -66,7 +66,7 @@ public void Should_project_ok() } } -public class ConvertUsingBug : IntegrationTest +public class ConvertUsingBug(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { public class Parent { @@ -113,14 +113,14 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) [Fact] public void can_map_with_projection() { - using (var db = new ApplicationDBContext()) + using (var db = Fixture.CreateContext()) { var result = ProjectTo(db.Parents); } } public class DatabaseInitializer : DropCreateDatabaseAlways { } } -public class StringTypeConverter : IntegrationTest +public class StringTypeConverter(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { protected override MapperConfiguration CreateConfiguration() => new(cfg => { @@ -130,7 +130,7 @@ public class StringTypeConverter : IntegrationTest(context.Planning); query.Single().Subject.ShouldBe("Test"); } diff --git a/src/IntegrationTests/BuiltInTypes/DateTimeToNullableDateTime.cs b/src/IntegrationTests/BuiltInTypes/DateTimeToNullableDateTime.cs index 6372859839..8227ba79a1 100644 --- a/src/IntegrationTests/BuiltInTypes/DateTimeToNullableDateTime.cs +++ b/src/IntegrationTests/BuiltInTypes/DateTimeToNullableDateTime.cs @@ -1,6 +1,6 @@ namespace AutoMapper.IntegrationTests.BuiltInTypes; -public class DateTimeToNullableDateTime : IntegrationTest +public class DateTimeToNullableDateTime(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { public class Parent { @@ -32,7 +32,7 @@ protected override void Seed(TestContext testContext) [Fact] public void Should_not_fail() { - using (var context = new TestContext()) + using (var context = Fixture.CreateContext()) { ProjectTo(context.Parents).Single().Date.ShouldBe(_expected); } diff --git a/src/IntegrationTests/BuiltInTypes/Enums.cs b/src/IntegrationTests/BuiltInTypes/Enums.cs index 5f7064fd3b..e43f645018 100644 --- a/src/IntegrationTests/BuiltInTypes/Enums.cs +++ b/src/IntegrationTests/BuiltInTypes/Enums.cs @@ -1,6 +1,6 @@ namespace AutoMapper.IntegrationTests.BuiltInTypes; -public class EnumToUnderlyingType : IntegrationTest +public class EnumToUnderlyingType(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { public class Customer { @@ -31,13 +31,13 @@ protected override void Seed(Context context) [Fact] public void Can_map_with_projection() { - using (var context = new Context()) + using (var context = Fixture.CreateContext()) { ProjectTo(context.Customers).First().ConsoleColor.ShouldBe((int)ConsoleColor.Yellow); } } } -public class UnderlyingTypeToEnum : IntegrationTest +public class UnderlyingTypeToEnum(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { public class Customer { @@ -68,13 +68,13 @@ protected override void Seed(Context context) [Fact] public void Can_map_with_projection() { - using (var context = new Context()) + using (var context = Fixture.CreateContext()) { ProjectTo(context.Customers).First().ConsoleColor.ShouldBe(ConsoleColor.Yellow); } } } -public class EnumToEnum : IntegrationTest +public class EnumToEnum(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { public class Customer { @@ -105,7 +105,7 @@ protected override void Seed(Context context) [Fact] public void Can_map_with_projection() { - using (var context = new Context()) + using (var context = Fixture.CreateContext()) { ProjectTo(context.Customers).First().ConsoleColor.ShouldBe(ConsoleColor.DarkYellow); } diff --git a/src/IntegrationTests/BuiltInTypes/NullableToNonNullable.cs b/src/IntegrationTests/BuiltInTypes/NullableToNonNullable.cs index 346e0f2dcb..fa64310b52 100644 --- a/src/IntegrationTests/BuiltInTypes/NullableToNonNullable.cs +++ b/src/IntegrationTests/BuiltInTypes/NullableToNonNullable.cs @@ -1,6 +1,6 @@ namespace AutoMapper.IntegrationTests.BuiltInTypes; -public class NullableLongToLong : IntegrationTest +public class NullableLongToLong(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { public class Customer { @@ -44,7 +44,7 @@ protected override void Seed(Context context) [Fact] public void Can_map_with_projection() { - using (var context = new Context()) + using (var context = Fixture.CreateContext()) { var model = ProjectTo(context.Customers).Single(); model.Id.ShouldBe(1); @@ -54,7 +54,7 @@ public void Can_map_with_projection() } } -public class NullableIntToLong : IntegrationTest +public class NullableIntToLong(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { public class Customer { @@ -98,7 +98,7 @@ protected override void Seed(Context context) [Fact] public void Can_map_with_projection() { - using(var context = new Context()) + using(var context = Fixture.CreateContext()) { var model = ProjectTo(context.Customers).Single(); model.Id.ShouldBe(1); diff --git a/src/IntegrationTests/BuiltInTypes/ProjectEnumerableOfIntToHashSet.cs b/src/IntegrationTests/BuiltInTypes/ProjectEnumerableOfIntToHashSet.cs index bc26d9c5fa..06f14e33ca 100644 --- a/src/IntegrationTests/BuiltInTypes/ProjectEnumerableOfIntToHashSet.cs +++ b/src/IntegrationTests/BuiltInTypes/ProjectEnumerableOfIntToHashSet.cs @@ -1,6 +1,6 @@ namespace AutoMapper.IntegrationTests.BuiltInTypes; -public class ProjectEnumerableOfIntToHashSet : IntegrationTest +public class ProjectEnumerableOfIntToHashSet(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { public class Customer { @@ -52,7 +52,7 @@ protected override void Seed(Context context) [Fact] public void Can_map_with_projection() { - using (var context = new Context()) + using (var context = Fixture.CreateContext()) { var customer = ProjectTo(context.Customers).Single(); customer.ItemsIds.SequenceEqual(new int[] { 1, 2, 3 }).ShouldBeTrue(); diff --git a/src/IntegrationTests/BuiltInTypes/ProjectEnumerableOfIntToList.cs b/src/IntegrationTests/BuiltInTypes/ProjectEnumerableOfIntToList.cs index e8a4a0dfc4..da0aea1d5a 100644 --- a/src/IntegrationTests/BuiltInTypes/ProjectEnumerableOfIntToList.cs +++ b/src/IntegrationTests/BuiltInTypes/ProjectEnumerableOfIntToList.cs @@ -1,6 +1,6 @@ namespace AutoMapper.IntegrationTests.BuiltInTypes; -public class ProjectEnumerableOfIntToList : IntegrationTest +public class ProjectEnumerableOfIntToList(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { public class Customer { @@ -52,7 +52,7 @@ protected override void Seed(Context context) [Fact] public void Can_map_with_projection() { - using(var context = new Context()) + using(var context = Fixture.CreateContext()) { var customer = ProjectTo(context.Customers).Single(); customer.ItemsIds.SequenceEqual(new int[] { 1, 2, 3 }).ShouldBeTrue(); diff --git a/src/IntegrationTests/ChildClassTests.cs b/src/IntegrationTests/ChildClassTests.cs index b83ed2d058..952eca4d47 100644 --- a/src/IntegrationTests/ChildClassTests.cs +++ b/src/IntegrationTests/ChildClassTests.cs @@ -51,7 +51,7 @@ protected override void Seed(TestContext testContext) } - public class UnitTest : IntegrationTest + public class UnitTest(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { protected override MapperConfiguration CreateConfiguration() => new(cfg => { @@ -62,7 +62,7 @@ public class UnitTest : IntegrationTest [Fact] public void AutoMapperEFRelationsTest() { - using (var context = new TestContext()) + using (var context = Fixture.CreateContext()) { var baseEntitiy = context.Bases.Include(b => b.Sub).FirstOrDefault(); baseEntitiy.ShouldNotBeNull(); @@ -70,7 +70,7 @@ public void AutoMapperEFRelationsTest() baseEntitiy.Sub.Sub1.ShouldBe("sub1"); } - using (var context = new TestContext()) + using (var context = Fixture.CreateContext()) { var baseDTO = context.Bases.Select(b => new BaseDTO { diff --git a/src/IntegrationTests/ConstructorDefaultValue.cs b/src/IntegrationTests/ConstructorDefaultValue.cs index c580d8346a..e7e2560b1a 100644 --- a/src/IntegrationTests/ConstructorDefaultValue.cs +++ b/src/IntegrationTests/ConstructorDefaultValue.cs @@ -1,5 +1,5 @@ namespace AutoMapper.IntegrationTests; -public class ConstructorDefaultValue : IntegrationTest +public class ConstructorDefaultValue(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { public class Customer { @@ -26,7 +26,42 @@ protected override void Seed(Context context) [Fact] public void Can_map_with_projection() { - using var context = new Context(); + using var context = Fixture.CreateContext(); ProjectTo(context.Customers).Single().Value.ShouldBe(5); } +} +public class StructConstructorMapping(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) +{ + public class Customer + { + public int Id { get; set; } + public DateTime Date { get; set; } + } + public class CustomerViewModel + { + public DateOnly Date { get; set; } + } + public class Context : LocalDbContext + { + public DbSet Customers { get; set; } + } + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(Context context) + { + context.Customers.Add(new Customer { Date = new(1984, 5, 23) }); + base.Seed(context); + } + } + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateProjection(); + cfg.CreateProjection(); + }); + [Fact] + public void Can_map_with_projection() + { + using var context = Fixture.CreateContext(); + ProjectTo(context.Customers).Single().Date.ShouldBe(new(1984, 5, 23)); + } } \ No newline at end of file diff --git a/src/IntegrationTests/CustomMapFrom/CustomMapFromTest.cs b/src/IntegrationTests/CustomMapFrom/CustomMapFromTest.cs index 0f14e239c3..09429ca127 100644 --- a/src/IntegrationTests/CustomMapFrom/CustomMapFromTest.cs +++ b/src/IntegrationTests/CustomMapFrom/CustomMapFromTest.cs @@ -1,13 +1,13 @@ namespace AutoMapper.IntegrationTests.CustomMapFrom; -public class CustomMapFromTest : IntegrationTest +public class CustomMapFromTest(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { protected override MapperConfiguration CreateConfiguration() => new(cfg => cfg.CreateProjection() .ForMember(x => x.FullAddress, o => o.MapFrom(c => c.Address.Street + ", " + c.Address.City + " " + c.Address.State))); [Fact] public void can_map_with_projection() { - using (var context = new Context()) + using (var context = Fixture.CreateContext()) { var customerVms = context.Customers.Select(c => new CustomerViewModel { diff --git a/src/IntegrationTests/CustomMapFrom/MapObjectPropertyFromSubQuery.cs b/src/IntegrationTests/CustomMapFrom/MapObjectPropertyFromSubQuery.cs index 0e2d5d7280..f4ba1e6fd8 100644 --- a/src/IntegrationTests/CustomMapFrom/MapObjectPropertyFromSubQuery.cs +++ b/src/IntegrationTests/CustomMapFrom/MapObjectPropertyFromSubQuery.cs @@ -1,6 +1,93 @@ namespace AutoMapper.IntegrationTests.CustomMapFrom; - -public class MemberWithSubQueryProjections : IntegrationTest +public class MultipleLevelsSubquery(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) +{ + [Fact] + public void Should_work() + { + using var context = Fixture.CreateContext(); + var resultQuery = ProjectTo(context.Foos); + resultQuery.Single().MyBar.MyBaz.FirstWidget.Id.ShouldBe(1); + } + protected override MapperConfiguration CreateConfiguration() => new(c => + { + c.CreateMap().ForMember(f => f.MyBar, opts => opts.MapFrom(src => src.Bar)); + c.CreateMap().ForMember(f => f.MyBaz, opts => opts.MapFrom(src => src.Baz)); + c.CreateMap().ForMember(f => f.FirstWidget, opts => opts.MapFrom(src => src.Widgets.FirstOrDefault())); + c.CreateMap(); + }); + public class Context : LocalDbContext + { + public virtual DbSet Foos { get; set; } + public virtual DbSet Bazs { get; set; } + } + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(Context context) + { + var testBaz = new Baz(); + testBaz.Widgets.Add(new Widget()); + testBaz.Widgets.Add(new Widget()); + var testBar = new Bar(); + testBar.Foos.Add(new Foo()); + testBaz.Bars.Add(testBar); + context.Bazs.Add(testBaz); + } + } + public class Foo + { + public int Id { get; set; } + public int BarId { get; set; } + public virtual Bar Bar { get; set; } + } + public class Bar + { + public Bar() => Foos = new HashSet(); + public int Id { get; set; } + public int BazId { get; set; } + public virtual Baz Baz { get; set; } + public virtual ICollection Foos { get; set; } + } + public class Baz + { + public Baz() + { + Bars = new HashSet(); + Widgets = new HashSet(); + } + public int Id { get; set; } + public virtual ICollection Bars { get; set; } + public virtual ICollection Widgets { get; set; } + } + public partial class Widget + { + public int Id { get; set; } + public int BazId { get; set; } + public virtual Baz Baz { get; set; } + } + public class FooModel + { + public int Id { get; set; } + public int BarId { get; set; } + public BarModel MyBar { get; set; } + } + public class BarModel + { + public int Id { get; set; } + public int BazId { get; set; } + public BazModel MyBaz { get; set; } + } + public class BazModel + { + public int Id { get; set; } + public WidgetModel FirstWidget { get; set; } + } + public class WidgetModel + { + public int Id { get; set; } + public int BazId { get; set; } + } +} +public class MemberWithSubQueryProjections(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { public class Customer { @@ -58,7 +145,7 @@ protected override void Seed(Context context) [Fact] public void Should_work() { - using (var context = new Context()) + using (var context = Fixture.CreateContext()) { var resultQuery = ProjectTo(context.Customers); var result = resultQuery.Single(); @@ -69,7 +156,7 @@ public void Should_work() } } } -public class MemberWithSubQueryProjectionsNoMap : IntegrationTest +public class MemberWithSubQueryProjectionsNoMap(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { public class Customer { @@ -121,7 +208,7 @@ protected override void Seed(Context context) [Fact] public void Should_work() { - using (var context = new Context()) + using (var context = Fixture.CreateContext()) { var resultQuery = ProjectTo(context.Customers); var result = resultQuery.Single(); @@ -131,7 +218,7 @@ public void Should_work() } } } -public class MapObjectPropertyFromSubQueryTypeNameMax : IntegrationTest +public class MapObjectPropertyFromSubQueryTypeNameMax(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { protected override MapperConfiguration CreateConfiguration() => new(cfg => { @@ -144,7 +231,7 @@ public class MapObjectPropertyFromSubQueryTypeNameMax : IntegrationTest(context.Products); var counter = new FirstOrDefaultCounter(); @@ -241,7 +328,7 @@ public class ClientContext : LocalDbContext } } -public class MapObjectPropertyFromSubQueryExplicitExpansion : IntegrationTest +public class MapObjectPropertyFromSubQueryExplicitExpansion(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { protected override MapperConfiguration CreateConfiguration() => new(cfg => { @@ -258,7 +345,7 @@ public class MapObjectPropertyFromSubQueryExplicitExpansion : IntegrationTest(context.Products); var counter = new FirstOrDefaultCounter(); @@ -330,7 +417,7 @@ public class ClientContext : LocalDbContext } } -public class MapObjectPropertyFromSubQuery : IntegrationTest +public class MapObjectPropertyFromSubQuery(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { protected override MapperConfiguration CreateConfiguration() => new(cfg => { @@ -343,7 +430,7 @@ public class MapObjectPropertyFromSubQuery : IntegrationTest(context.Products); var counter = new FirstOrDefaultCounter(); @@ -418,7 +505,7 @@ public class ClientContext : LocalDbContext } } -public class MapObjectPropertyFromSubQueryWithInnerObject : IntegrationTest +public class MapObjectPropertyFromSubQueryWithInnerObject(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { protected override MapperConfiguration CreateConfiguration() => new(cfg => { @@ -432,7 +519,7 @@ public class MapObjectPropertyFromSubQueryWithInnerObject : IntegrationTest(context.ProductArticles); var counter = new FirstOrDefaultCounter(); @@ -513,7 +600,7 @@ public class ClientContext : LocalDbContext } } -public class MapObjectPropertyFromSubQueryWithCollection : IntegrationTest +public class MapObjectPropertyFromSubQueryWithCollection(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { protected override MapperConfiguration CreateConfiguration() => new(cfg => { @@ -527,7 +614,7 @@ public class MapObjectPropertyFromSubQueryWithCollection : IntegrationTest(context.ProductArticles); var counter = new FirstOrDefaultCounter(); @@ -613,7 +700,8 @@ public class ClientContext : LocalDbContext } } -public class MapObjectPropertyFromSubQueryWithCollectionSameName : NonValidatingSpecBase, IAsyncLifetime +public class MapObjectPropertyFromSubQueryWithCollectionSameName(DatabaseFixture databaseFixture) + : IntegrationTest(databaseFixture) { protected override MapperConfiguration CreateConfiguration() => new(cfg => { @@ -629,7 +717,7 @@ public class MapObjectPropertyFromSubQueryWithCollectionSameName : NonValidating [Fact] public void Should_cache_the_subquery() { - using (var context = new ClientContext()) + using (var context = Fixture.CreateContext()) { var projection = ProjectTo(context.ProductArticles); var counter = new FirstOrDefaultCounter(); @@ -728,17 +816,9 @@ public class ClientContext : LocalDbContext public DbSet Products { get; set; } public DbSet ProductArticles { get; set; } } - public async Task InitializeAsync() - { - var initializer = new DatabaseInitializer(); - - await initializer.Migrate(); - } - - public Task DisposeAsync() => Task.CompletedTask; } -public class SubQueryWithMapFromNullable : IntegrationTest +public class SubQueryWithMapFromNullable(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { // Source Types public class Cable @@ -831,7 +911,7 @@ protected override void Seed(ClientContext context) [Fact] public void Should_project_ok() { - using (var context = new ClientContext()) + using (var context = Fixture.CreateContext()) { var projection = ProjectTo(context.Cables); var result = projection.Single(); @@ -841,7 +921,7 @@ public void Should_project_ok() } } -public class MapObjectPropertyFromSubQueryCustomSource : IntegrationTest +public class MapObjectPropertyFromSubQueryCustomSource(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { protected override MapperConfiguration CreateConfiguration() => new(cfg => { @@ -912,7 +992,7 @@ protected override void Seed(ClientContext context) [Fact] public void Should_project_ok() { - using (var context = new ClientContext()) + using (var context = Fixture.CreateContext()) { var projection = ProjectTo(context.ProductReviews); var results = projection.ToArray(); @@ -923,7 +1003,7 @@ public void Should_project_ok() } } -public class MemberWithSubQueryIdentity : IntegrationTest +public class MemberWithSubQueryIdentity(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { protected override MapperConfiguration CreateConfiguration() => new MapperConfiguration(cfg => { @@ -936,7 +1016,7 @@ public class MemberWithSubQueryIdentity : IntegrationTest(new ClientContext().AEntities); + var query = ProjectTo(Fixture.CreateContext().AEntities); var result = query.Single(); result.DtoSubWrapper.DtoSub.ShouldNotBeNull(); result.DtoSubWrapper.DtoSub.SubString.ShouldBe("Test"); @@ -1016,4 +1096,1477 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) } public DbSet AEntities { get; set; } } +} + +public class MultipleLevelsSubqueryWithInheritance(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) +{ + [Fact] + public void Should_work() + { + using var context = Fixture.CreateContext(); + var resultQuery = ProjectTo(context.Foos); + resultQuery.Single().MyBar.MyBaz.FirstWidget.Id.ShouldBe(1); + } + protected override MapperConfiguration CreateConfiguration() => new(c => + { + c.CreateMap().ForMember(f => f.MyBar, opts => opts.MapFrom(src => src.Bar)); + c.CreateMap().ForMember(f => f.MyBaz, opts => opts.MapFrom(src => src.Baz)); + c.CreateMap().ForMember(f => f.FirstWidget, opts => opts.MapFrom(src => src.Widgets.FirstOrDefault())); + c.CreateMap(); + }); + public class Context : LocalDbContext + { + public virtual DbSet Foos { get; set; } + public virtual DbSet Bazs { get; set; } + } + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(Context context) + { + var testBaz = new Baz(); + testBaz.Widgets.Add(new Widget()); + testBaz.Widgets.Add(new Widget()); + var testBar = new Bar(); + testBar.Foos.Add(new Foo()); + testBaz.Bars.Add(testBar); + context.Bazs.Add(testBaz); + } + } + public class Foo + { + public int Id { get; set; } + public int BarId { get; set; } + public virtual Bar Bar { get; set; } + } + public class Bar + { + public Bar() => Foos = new HashSet(); + public int Id { get; set; } + public int BazId { get; set; } + public virtual Baz Baz { get; set; } + public virtual ICollection Foos { get; set; } + } + public class Baz + { + public Baz() + { + Bars = new HashSet(); + Widgets = new HashSet(); + } + public int Id { get; set; } + public virtual ICollection Bars { get; set; } + public virtual ICollection Widgets { get; set; } + } + public partial class Widget + { + public int Id { get; set; } + public int BazId { get; set; } + public virtual Baz Baz { get; set; } + } + public class FooModel + { + public int Id { get; set; } + public int BarId { get; set; } + public BarModel MyBar { get; set; } + } + public class BarModel + { + public int Id { get; set; } + public int BazId { get; set; } + public BazModel MyBaz { get; set; } + } + public class BazModel + { + public int Id { get; set; } + public WidgetModel FirstWidget { get; set; } + } + public class WidgetModel + { + public int Id { get; set; } + public int BazId { get; set; } + } +} +public class MemberWithSubQueryProjectionsWithInheritance(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) +{ + public class Customer + { + [Key] + public int Id { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public ICollection Items { get; set; } + } + public class CustomerA : Customer + { + } + public class CustomerB : Customer + { + public string B { get; set; } + } + public class Item + { + public int Id { get; set; } + public int Code { get; set; } + } + public class ItemA : Item + { + public string A { get; set; } + } + public class ItemModel + { + public int Id { get; set; } + public int Code { get; set; } + } + public class ItemModelA : ItemModel + { + public string A { get; set; } + } + public class CustomerViewModel + { + public CustomerNameModel Name { get; set; } + public ItemModel FirstItem { get; set; } + } + public class CustomerAViewModel : CustomerViewModel + { + public ItemModelA FirstItemA { get; set; } + } + public class CustomerBViewModel : CustomerViewModel + { + public string B { get; set; } + } + public class CustomerNameModel + { + public string FirstName { get; set; } + public string LastName { get; set; } + } + public class Context : LocalDbContext + { + public DbSet Customers { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity(); + modelBuilder.Entity(); + modelBuilder.Entity(); + } + } + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(Context context) + { + context.Customers.Add(new CustomerA + { + FirstName = "Alice", + LastName = "Smith", + Items = new[] { new Item { Code = 1 }, new ItemA { Code = 3, A = "a", }, new Item { Code = 5 } } + }); + context.Customers.Add(new CustomerB + { + FirstName = "Bob", + LastName = "Smith", + B = "b", + Items = new[] { new Item { Code = 1 }, new Item { Code = 3 }, new Item { Code = 5 } } + }); context.Customers.Add(new Customer + { + FirstName = "Jim", + LastName = "Smith", + Items = new[] { new Item { Code = 1 }, new Item { Code = 3 }, new Item { Code = 5 } } + }); + base.Seed(context); + } + } + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap() + .ForMember(dst => dst.Name, opt => opt.MapFrom(src => src.LastName != null ? src : null)) + .ForMember(dst => dst.FirstItem, opt => opt.MapFrom(src => src.Items.FirstOrDefault())) + .Include() + .Include(); + + + cfg.CreateMap() + .ForMember(dst => dst.FirstItemA, opt => opt.MapFrom(src => src.Items.OfType().FirstOrDefault())); + cfg.CreateMap(); + cfg.CreateMap(); + cfg.CreateMap() + .Include(); + cfg.CreateMap(); + }); + [Fact] + public void Should_work() + { + using (var context = Fixture.CreateContext()) + { + var resultQuery = ProjectTo(context.Customers.OrderBy(p => p.FirstName)); + var list = resultQuery.ToList(); + + var resultA = list[0].ShouldBeOfType(); + resultA.Name.FirstName.ShouldBe("Alice"); + resultA.Name.LastName.ShouldBe("Smith"); + resultA.FirstItem.Code.ShouldBe(1); + resultA.FirstItemA.A.ShouldBe("a"); + + var resultB = list[1].ShouldBeOfType(); + resultB.Name.FirstName.ShouldBe("Bob"); + resultB.Name.LastName.ShouldBe("Smith"); + resultB.FirstItem.Code.ShouldBe(1); + resultB.B.ShouldBe("b"); + + var result = list[2].ShouldBeOfType(); + result.Name.FirstName.ShouldBe("Jim"); + result.Name.LastName.ShouldBe("Smith"); + result.FirstItem.Code.ShouldBe(1); + } + } +} +public class MemberWithSubQueryProjectionsNoMapWithInheritance(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) +{ + public class Customer + { + [Key] + public int Id { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public ICollection Items { get; set; } + } + public class CustomerA : Customer + { + public string A { get; set; } + } + public class CustomerB : Customer + { + public string B { get; set; } + } + public class Item + { + public int Id { get; set; } + public int Code { get; set; } + } + public class ItemModel + { + public int Id { get; set; } + public int Code { get; set; } + } + public class CustomerViewModel + { + public string Name { get; set; } + public ItemModel FirstItem { get; set; } + } + public class CustomerAViewModel : CustomerViewModel + { + public string A { get; set; } + } + public class CustomerBViewModel : CustomerViewModel + { + public string B { get; set; } + } + public class Context : LocalDbContext + { + public DbSet Customers { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity(); + modelBuilder.Entity(); + } + } + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(Context context) + { + context.Customers.Add(new CustomerA + { + FirstName = "Alice", + LastName = "Smith", + A = "a", + Items = new[] { new Item { Code = 1 }, new Item { Code = 3 }, new Item { Code = 5 } } + }); + context.Customers.Add(new CustomerB + { + FirstName = "Bob", + LastName = "Smith", + B = "b", + Items = new[] { new Item { Code = 1 }, new Item { Code = 3 }, new Item { Code = 5 } } + }); context.Customers.Add(new Customer + { + FirstName = "Jim", + LastName = "Smith", + Items = new[] { new Item { Code = 1 }, new Item { Code = 3 }, new Item { Code = 5 } } + }); + base.Seed(context); + } + } + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap() + .ForMember(dst => dst.Name, opt => opt.MapFrom(src => src.LastName != null ? src.LastName : null)) + .ForMember(dst => dst.FirstItem, opt => opt.MapFrom(src => src.Items.FirstOrDefault())) + .Include() + .Include(); + + cfg.CreateMap(); + cfg.CreateMap(); + cfg.CreateMap(); + }); + [Fact] + public void Should_work() + { + using (var context = Fixture.CreateContext()) + { + var resultQuery = ProjectTo(context.Customers.OrderBy(p => p.FirstName)); + var list = resultQuery.ToList(); + + var resultA = list[0].ShouldBeOfType(); + resultA.Name.ShouldBe("Smith"); + resultA.FirstItem.Code.ShouldBe(1); + resultA.A.ShouldBe("a"); + + var resultB = list[1].ShouldBeOfType(); + resultB.Name.ShouldBe("Smith"); + resultB.FirstItem.Code.ShouldBe(1); + resultB.B.ShouldBe("b"); + + var result = list[2].ShouldBeOfType(); + result.Name.ShouldBe("Smith"); + result.FirstItem.Code.ShouldBe(1); + } + } +} +public class MapObjectPropertyFromSubQueryTypeNameMaxWithInheritance(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) +{ + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap() + .ForMember(d => d.Price, o => o.MapFrom(source => source.Articles.Where(x => x.IsDefault && x.NationId == 1 && source.ECommercePublished).FirstOrDefault())) + .Include() + .Include(); + + cfg.CreateMap(); + cfg.CreateMap(); + cfg.CreateMap() + .ForMember(d => d.RegionId, o => o.MapFrom(s => s.NationId)); + }); + + [Fact] + public void Should_cache_the_subquery() + { + using (var context = Fixture.CreateContext()) + { + var projection = ProjectTo(context.Products.OrderBy(p => p.Name)); + var counter = new FirstOrDefaultCounter(); + counter.Visit(projection.Expression); + counter.Count.ShouldBe(12); + var list = projection.ToList(); + + var productAModel = list[0].ShouldBeOfType(); + productAModel.Price.RegionId.ShouldBe((short)1); + productAModel.Price.IsDefault.ShouldBeTrue(); + productAModel.A.ShouldBe("a"); + + var productBModel = list[1].ShouldBeOfType(); + productBModel.Price.RegionId.ShouldBe((short)1); + productBModel.Price.IsDefault.ShouldBeTrue(); + productBModel.B.ShouldBe("b"); + + var productModel = list[2].ShouldBeOfType(); + productModel.Price.RegionId.ShouldBe((short)1); + productModel.Price.IsDefault.ShouldBeTrue(); + } + } + + class FirstOrDefaultCounter : ExpressionVisitor + { + public int Count; + + protected override Expression VisitMethodCall(MethodCallExpression node) + { + if (node.Method.Name == "FirstOrDefault") + { + Count++; + } + return base.VisitMethodCall(node); + } + } + + public partial class Article + { + public int Id { get; set; } + public int ProductId { get; set; } + public bool IsDefault { get; set; } + public short NationId { get; set; } + public virtual Product Product { get; set; } + } + + public partial class Product + { + public int Id { get; set; } + public string Name { get; set; } + public bool ECommercePublished { get; set; } + public virtual ICollection
Articles { get; set; } + public int Value { get; } + [NotMapped] + public int NotMappedValue { get; set; } + public int VeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnName1 { get; set; } + public int VeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnName2 { get; set; } + public int VeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnName3 { get; set; } + public int VeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnName4 { get; set; } + public int VeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnName5 { get; set; } + public int VeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnName6 { get; set; } + public int VeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnName7 { get; set; } + public int VeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnName8 { get; set; } + public int VeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnName9 { get; set; } + public int VeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnName10 { get; set; } + public int VeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnName11 { get; set; } + } + + public class ProductA : Product + { + public string A { get; set; } + } + public class ProductB : Product + { + public string B { get; set; } + } + + public class PriceModel + { + public int Id { get; set; } + public short RegionId { get; set; } + public bool IsDefault { get; set; } + } + + public class ProductModel + { + public int Id { get; set; } + public PriceModel Price { get; set; } + public int VeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnName1 { get; set; } + public int VeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnName2 { get; set; } + public int VeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnName3 { get; set; } + public int VeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnName4 { get; set; } + public int VeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnName5 { get; set; } + public int VeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnName6 { get; set; } + public int VeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnName7 { get; set; } + public int VeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnName8 { get; set; } + public int VeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnName9 { get; set; } + public int VeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnName10 { get; set; } + public int VeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnName11 { get; set; } + } + + public class ProductAModel : ProductModel + { + public string A { get; set; } + } + public class ProductBModel : ProductModel + { + public string B { get; set; } + } + + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(ClientContext context) + { + context.Products.Add(new ProductA { Name = "P1", A = "a", ECommercePublished = true, Articles = new[] { new Article { IsDefault = true, NationId = 1, ProductId = 1 } } }); + context.Products.Add(new ProductB { Name = "P2", B = "b", ECommercePublished = true, Articles = new[] { new Article { IsDefault = true, NationId = 1, ProductId = 1 } } }); + context.Products.Add(new Product { Name = "P3", ECommercePublished = true, Articles = new[] { new Article { IsDefault = true, NationId = 1, ProductId = 1 } } }); + } + } + + public class ClientContext : LocalDbContext + { + public DbSet Products { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity(); + modelBuilder.Entity(); + } + } +} +public class MapObjectPropertyFromSubQueryExplicitExpansionWithInheritance(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) +{ + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap() + .ForMember(d => d.Price, o => + { + o.MapFrom(source => source.Articles.Where(x => x.IsDefault && x.NationId == 1 && source.ECommercePublished).FirstOrDefault()); + o.ExplicitExpansion(); + }) + .Include() + .Include(); + + cfg.CreateMap(); + cfg.CreateMap(); + + cfg.CreateMap() + .ForMember(d => d.RegionId, o => o.MapFrom(s => s.NationId)); + }); + + [Fact] + public void Should_map_ok() + { + using (var context = Fixture.CreateContext()) + { + var projection = ProjectTo(context.Products.OrderBy(p => p.Name)); + var counter = new FirstOrDefaultCounter(); + counter.Visit(projection.Expression); + counter.Count.ShouldBe(0); + var list = projection.ToList(); + + var productAModel = list[0].ShouldBeOfType(); + productAModel.Price.ShouldBeNull(); + productAModel.Name.ShouldBe("P1"); + productAModel.A.ShouldBe("a"); + + var productBModel = list[1].ShouldBeOfType(); + productBModel.Price.ShouldBeNull(); + productBModel.Name.ShouldBe("P2"); + productBModel.B.ShouldBe("b"); + + var productModel = list[2].ShouldBeOfType(); + productModel.Price.ShouldBeNull(); + productModel.Name.ShouldBe("P3"); + } + } + + class FirstOrDefaultCounter : ExpressionVisitor + { + public int Count; + + protected override Expression VisitMethodCall(MethodCallExpression node) + { + if (node.Method.Name == "FirstOrDefault") + { + Count++; + } + return base.VisitMethodCall(node); + } + } + + public partial class Article + { + public int Id { get; set; } + public int ProductId { get; set; } + public bool IsDefault { get; set; } + public short NationId { get; set; } + public virtual Product Product { get; set; } + } + + public partial class Product + { + public int Id { get; set; } + public string Name { get; set; } + public bool ECommercePublished { get; set; } + public virtual ICollection
Articles { get; set; } + public int Value { get; } + } + + public class ProductA : Product + { + public string A { get; set; } + } + public class ProductB : Product + { + public string B { get; set; } + } + + public class PriceModel + { + public int Id { get; set; } + public short RegionId { get; set; } + public bool IsDefault { get; set; } + } + + public class ProductModel + { + public string Name { get; set; } + public PriceModel Price { get; set; } + } + + public class ProductAModel : ProductModel + { + public string A { get; set; } + } + public class ProductBModel : ProductModel + { + public string B { get; set; } + } + + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(ClientContext context) + { + context.Products.Add(new ProductA { ECommercePublished = true, Name = "P1", A = "a", Articles = new[] { new Article { IsDefault = true, NationId = 1, ProductId = 1 } } }); + context.Products.Add(new ProductB { ECommercePublished = true, Name = "P2", B = "b", Articles = new[] { new Article { IsDefault = true, NationId = 1, ProductId = 1 } } }); + context.Products.Add(new Product { ECommercePublished = true, Name = "P3", Articles = new[] { new Article { IsDefault = true, NationId = 1, ProductId = 1 } } }); + } + } + + public class ClientContext : LocalDbContext + { + public DbSet Products { get; set; } + public DbSet
Articles { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity(); + modelBuilder.Entity(); + } + } +} +public class MapObjectPropertyFromSubQueryWithInheritance(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) +{ + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap() + .ForMember(d => d.Price, o => o.MapFrom(source => source.Articles.Where(x => x.IsDefault && x.NationId == 1 && source.ECommercePublished).FirstOrDefault())) + .Include() + .Include(); + cfg.CreateMap(); + cfg.CreateMap(); + cfg.CreateMap() + .ForMember(d => d.RegionId, o => o.MapFrom(s => s.NationId)); + }); + + [Fact] + public void Should_cache_the_subquery() + { + using (var context = Fixture.CreateContext()) + { + var projection = ProjectTo(context.Products.OrderBy(p => p.Name)); + var counter = new FirstOrDefaultCounter(); + counter.Visit(projection.Expression); + counter.Count.ShouldBe(12); + var list = projection.ToList(); + + var productAModel = list[0].ShouldBeOfType(); + productAModel.Price.RegionId.ShouldBe((short)1); + productAModel.Price.IsDefault.ShouldBeTrue(); + productAModel.Name.ShouldBe("P1"); + productAModel.A.ShouldBe("a"); + + var productBModel = list[1].ShouldBeOfType(); + productBModel.Price.RegionId.ShouldBe((short)1); + productBModel.Price.IsDefault.ShouldBeTrue(); + productBModel.Name.ShouldBe("P2"); + productBModel.B.ShouldBe("b"); + + var productModel = list[2].ShouldBeOfType(); + productModel.Price.RegionId.ShouldBe((short)1); + productModel.Price.IsDefault.ShouldBeTrue(); + productModel.Name.ShouldBe("P3"); + } + } + + class FirstOrDefaultCounter : ExpressionVisitor + { + public int Count; + + protected override Expression VisitMethodCall(MethodCallExpression node) + { + if (node.Method.Name == "FirstOrDefault") + { + Count++; + } + return base.VisitMethodCall(node); + } + } + + public partial class Article + { + public int Id { get; set; } + public int ProductId { get; set; } + public bool IsDefault { get; set; } + public short NationId { get; set; } + public virtual Product Product { get; set; } + } + + public partial class Product + { + public int Id { get; set; } + public string Name { get; set; } + public bool ECommercePublished { get; set; } + public virtual ICollection
Articles { get; set; } + public int Value { get; } + [NotMapped] + public int NotMappedValue { get; set; } + } + public partial class ProductA : Product + { + public string A { get; set; } + } + public partial class ProductB : Product + { + public string B { get; set; } + } + public class PriceModel + { + public int Id { get; set; } + public short RegionId { get; set; } + public bool IsDefault { get; set; } + } + + public class ProductModel + { + public int Id { get; set; } + public string Name { get; set; } + public PriceModel Price { get; set; } + } + public class ProductAModel : ProductModel + { + public string A { get; set; } + } + public class ProductBModel : ProductModel + { + public string B { get; set; } + } + + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(ClientContext context) + { + context.Products.Add(new ProductA { Name = "P1", A = "a", ECommercePublished = true, Articles = new[] { new Article { IsDefault = true, NationId = 1, ProductId = 1 } } }); + context.Products.Add(new ProductB { Name = "P2", B = "b", ECommercePublished = true, Articles = new[] { new Article { IsDefault = true, NationId = 1, ProductId = 1 } } }); + context.Products.Add(new Product { Name = "P3", ECommercePublished = true, Articles = new[] { new Article { IsDefault = true, NationId = 1, ProductId = 1 } } }); + } + } + + public class ClientContext : LocalDbContext + { + public DbSet Products { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity(); + modelBuilder.Entity(); + } + } +} +public class MapObjectPropertyFromSubQueryWithInnerObjectWithInheritance(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) +{ + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap() + .Include() + .Include(); + + cfg.CreateMap(); + cfg.CreateMap(); + cfg.CreateMap() + .ForMember(d => d.Price, o => o.MapFrom(source => source.Articles.Where(x => x.IsDefault && source.ECommercePublished).FirstOrDefault())); + cfg.CreateMap() + .ForMember(d => d.RegionId, o => o.MapFrom(s => s.NationId)); + }); + + [Fact] + public void Should_cache_the_subquery() + { + using (var context = Fixture.CreateContext()) + { + var projection = ProjectTo(context.ProductArticles.OrderBy(p => p.Name)); + var counter = new FirstOrDefaultCounter(); + counter.Visit(projection.Expression); + counter.Count.ShouldBe(24); + var list = projection.ToList(); + + var productArticleAModel = list[0].ShouldBeOfType(); + productArticleAModel.Name.ShouldBe("P1"); + productArticleAModel.A.ShouldBe("a"); + var productModel = productArticleAModel.Product; + productModel.Price.RegionId.ShouldBe((short)1); + productModel.Price.IsDefault.ShouldBeTrue(); + var otherProductModel = productArticleAModel.OtherProduct; + otherProductModel.Price.RegionId.ShouldBe((short)2); + otherProductModel.Price.IsDefault.ShouldBeTrue(); + + var productArticleBModel = list[1].ShouldBeOfType(); + productArticleBModel.Name.ShouldBe("P2"); + productArticleBModel.B.ShouldBe("b"); + productModel = productArticleBModel.Product; + productModel.Price.RegionId.ShouldBe((short)3); + productModel.Price.IsDefault.ShouldBeTrue(); + otherProductModel = productArticleBModel.OtherProduct; + otherProductModel.Price.RegionId.ShouldBe((short)4); + otherProductModel.Price.IsDefault.ShouldBeTrue(); + + var productArticleModel = list[2].ShouldBeOfType(); + productArticleModel.Name.ShouldBe("P3"); + productModel = productArticleModel.Product; + productModel.Price.RegionId.ShouldBe((short)5); + productModel.Price.IsDefault.ShouldBeTrue(); + otherProductModel = productArticleModel.OtherProduct; + otherProductModel.Price.RegionId.ShouldBe((short)6); + otherProductModel.Price.IsDefault.ShouldBeTrue(); + } + } + + public class ProductArticle + { + public int Id { get; set; } + + public string Name { get; set; } + public Product Product { get; set; } + public Product OtherProduct { get; set; } + } + public class ProductArticleA : ProductArticle + { + public string A { get; set; } + } + public class ProductArticleB : ProductArticle + { + public string B { get; set; } + } + + public class ProductArticleModel + { + public int Id { get; set; } + public string Name { get; set; } + public ProductModel Product { get; set; } + public ProductModel OtherProduct { get; set; } + } + public class ProductArticleAModel : ProductArticleModel + { + public string A { get; set; } + } + public class ProductArticleBModel : ProductArticleModel + { + public string B { get; set; } + } + + public partial class Article + { + public int Id { get; set; } + public int ProductId { get; set; } + public bool IsDefault { get; set; } + public short NationId { get; set; } + public virtual Product Product { get; set; } + } + + public partial class Product + { + public int Id { get; set; } + public string Name { get; set; } + public bool ECommercePublished { get; set; } + public virtual ICollection
Articles { get; set; } + } + + public class PriceModel + { + public int Id { get; set; } + public short RegionId { get; set; } + public bool IsDefault { get; set; } + } + + public class ProductModel + { + public int Id { get; set; } + public PriceModel Price { get; set; } + } + + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(ClientContext context) + { + var product1 = context.Products.Add(new Product { ECommercePublished = true, Articles = new[] { new Article { IsDefault = true, NationId = 1, ProductId = 1 } } }); + var product2 = context.Products.Add(new Product { ECommercePublished = true, Articles = new[] { new Article { IsDefault = true, NationId = 2, ProductId = 2 } } }); + context.ProductArticles.Add(new ProductArticleA { A = "a", Name = "P1", Product = product1.Entity, OtherProduct = product2.Entity }); + + var product3 = context.Products.Add(new Product { ECommercePublished = true, Articles = new[] { new Article { IsDefault = true, NationId = 3, ProductId = 1 } } }); + var product4 = context.Products.Add(new Product { ECommercePublished = true, Articles = new[] { new Article { IsDefault = true, NationId = 4, ProductId = 2 } } }); + context.ProductArticles.Add(new ProductArticleB { B = "b", Name = "P2", Product = product3.Entity, OtherProduct = product4.Entity }); + + var product5 = context.Products.Add(new Product { ECommercePublished = true, Articles = new[] { new Article { IsDefault = true, NationId = 5, ProductId = 1 } } }); + var product6 = context.Products.Add(new Product { ECommercePublished = true, Articles = new[] { new Article { IsDefault = true, NationId = 6, ProductId = 2 } } }); + context.ProductArticles.Add(new ProductArticle { Name = "P3", Product = product5.Entity, OtherProduct = product6.Entity }); + } + } + + public class ClientContext : LocalDbContext + { + public DbSet Products { get; set; } + public DbSet ProductArticles { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity(); + modelBuilder.Entity(); + } + } +} +public class MapObjectPropertyFromSubQueryWithCollectionWithInheritance(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) +{ + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap() + .Include() + .Include(); + cfg.CreateMap(); + cfg.CreateMap(); + cfg.CreateMap() + .ForMember(d => d.Price, o => o.MapFrom(source => source.Articles.Where(x => x.IsDefault && x.NationId == 1 && source.ECommercePublished).FirstOrDefault())); + cfg.CreateMap() + .ForMember(d => d.RegionId, o => o.MapFrom(s => s.NationId)); + }); + + [Fact] + public void Should_cache_the_subquery() + { + using (var context = Fixture.CreateContext()) + { + var projection = ProjectTo(context.ProductArticles.OrderBy(p => p.Name)); + var counter = new FirstOrDefaultCounter(); + counter.Visit(projection.Expression); + counter.Count.ShouldBe(12); + var productModel = projection.First().Products.First(); + productModel.Price.RegionId.ShouldBe((short)1); + productModel.Price.IsDefault.ShouldBeTrue(); + productModel.Price.Id.ShouldBe(1); + productModel.Id.ShouldBe(1); + } + } + + class FirstOrDefaultCounter : ExpressionVisitor + { + public int Count; + + protected override Expression VisitMethodCall(MethodCallExpression node) + { + if (node.Method.Name == "FirstOrDefault") + { + Count++; + } + return base.VisitMethodCall(node); + } + } + + public class ProductArticle + { + public int Id { get; set; } + public string Name { get; set; } + public ICollection Products { get; set; } + } + + public class ProductArticleA : ProductArticle + { + public string A { get; set; } + } + + public class ProductArticleB : ProductArticle + { + public string B { get; set; } + } + + public class ProductArticleModel + { + public int Id { get; set; } + public string Name { get; set; } + public ICollection Products { get; set; } + } + + public class ProductArticleAModel : ProductArticleModel + { + public string A { get; set; } + } + public class ProductArticleBModel : ProductArticleModel + { + public string B { get; set; } + } + + public partial class Article + { + public int Id { get; set; } + public int ProductId { get; set; } + public bool IsDefault { get; set; } + public short NationId { get; set; } + public virtual Product Product { get; set; } + } + + public partial class Product + { + public int Id { get; set; } + public string Name { get; set; } + public bool ECommercePublished { get; set; } + public virtual ICollection
Articles { get; set; } + } + + public class PriceModel + { + public int Id { get; set; } + public short RegionId { get; set; } + public bool IsDefault { get; set; } + } + + public class ProductModel + { + public int Id { get; set; } + public PriceModel Price { get; set; } + } + + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(ClientContext context) + { + var product = context.Products.Add(new Product { ECommercePublished = true, Articles = new[] { new Article { IsDefault = true, NationId = 1, ProductId = 1 } } }); + context.ProductArticles.Add(new ProductArticleA { Name = "P1", A = "a", Products = new[] { product.Entity } }); + + product = context.Products.Add(new Product { ECommercePublished = true, Articles = new[] { new Article { IsDefault = true, NationId = 1, ProductId = 1 } } }); + context.ProductArticles.Add(new ProductArticleB { Name = "P2", B = "b", Products = new[] { product.Entity } }); + + product = context.Products.Add(new Product { ECommercePublished = true, Articles = new[] { new Article { IsDefault = true, NationId = 1, ProductId = 1 } } }); + context.ProductArticles.Add(new ProductArticle { Name = "P3", Products = new[] { product.Entity } }); + } + } + + public class ClientContext : LocalDbContext + { + public DbSet Products { get; set; } + public DbSet ProductArticles { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity(); + modelBuilder.Entity(); + } + } +} +public class MapObjectPropertyFromSubQueryWithCollectionSameNameWithInheritance(DatabaseFixture databaseFixture) + : IntegrationTest(databaseFixture) +{ + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap() + .Include() + .Include(); + cfg.CreateMap() + .ForMember(d => d.ECommerceProducts, o => o.MapFrom(source => source.Products.Where(p => p.ECommercePublished))); + cfg.CreateMap(); + cfg.CreateMap() + .ForMember(d => d.ArticlesModel, o => o.MapFrom(s => s)) + .ForMember(d => d.Articles, o => o.MapFrom(source => source.Articles.Where(x => x.IsDefault && x.NationId == 1 && source.ECommercePublished).FirstOrDefault())) + .Include(); + cfg.CreateMap(); + cfg.CreateMap() + .ForMember(d => d.RegionId, o => o.MapFrom(s => s.NationId)); + + cfg.CreateMap(); + }); + + [Fact] + public void Should_cache_the_subquery() + { + using (var context = Fixture.CreateContext()) + { + var projection = ProjectTo(context.ProductArticles.OrderBy(p => p.Name)); + var counter = new FirstOrDefaultCounter(); + counter.Visit(projection.Expression); + counter.Count.ShouldBe(16); + var ecommerce = projection.ToList().OfType().First(); + ecommerce.ECommerceProducts.Count.ShouldBe(1); + ecommerce.Products.Count.ShouldBe(2); + + var productModel = projection.First().Products.First(); + Check(productModel.Articles); + productModel.Id.ShouldBe(1); + productModel.ArticlesCount.ShouldBe(1); + productModel.ArticlesModel.Articles.Count.ShouldBe(1); + Check(productModel.ArticlesModel.Articles.Single()); + } + } + + private static void Check(PriceModel priceModel) + { + priceModel.RegionId.ShouldBe((short)1); + priceModel.IsDefault.ShouldBeTrue(); + priceModel.Id.ShouldBe(1); + } + + class FirstOrDefaultCounter : ExpressionVisitor + { + public int Count; + + protected override Expression VisitMethodCall(MethodCallExpression node) + { + if (node.Method.Name == "FirstOrDefault") + { + Count++; + } + return base.VisitMethodCall(node); + } + } + + public class ProductArticle + { + public int Id { get; set; } + public string Name { get; set; } + public ICollection Products { get; set; } + } + + public class ProductArticleA : ProductArticle + { + public string A { get; set; } + } + + public class ProductArticleB : ProductArticle + { + public string B { get; set; } + } + + public class ProductArticleModel + { + public int Id { get; set; } + public string Name { get; set; } + public ICollection Products { get; set; } + } + + public class ECommerceProductArticleModel : ProductArticleModel + { + public ICollection ECommerceProducts { get; set; } + } + + public class ProductArticleBModel : ProductArticleModel + { + public string B { get; set; } + } + + public partial class Article + { + public int Id { get; set; } + public int ProductId { get; set; } + public bool IsDefault { get; set; } + public short NationId { get; set; } + public virtual Product Product { get; set; } + } + + public partial class Product + { + public int Id { get; set; } + public string Name { get; set; } + public bool ECommercePublished { get; set; } + public virtual ICollection
Articles { get; set; } + } + + public class PriceModel + { + public int Id { get; set; } + public short RegionId { get; set; } + public bool IsDefault { get; set; } + } + + public class ProductModel + { + public int Id { get; set; } + public PriceModel Articles { get; set; } + public int ArticlesCount { get; set; } + public ArticlesModel ArticlesModel { get; set; } + } + + public class ECommerceProductModel : ProductModel + { + } + + public class ArticlesModel + { + public ICollection Articles { get; set; } + } + + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(ClientContext context) + { + var product = context.Products.Add(new Product { ECommercePublished = true, Articles = new[] { new Article { IsDefault = true, NationId = 1, ProductId = 1 } } }); + var product2 = context.Products.Add(new Product { ECommercePublished = false, Articles = new[] { new Article { IsDefault = true, NationId = 1, ProductId = 1 } } }); + context.ProductArticles.Add(new ProductArticleA { Name = "P1", A = "a", Products = new[] { product.Entity, product2.Entity } }); + product = context.Products.Add(new Product { ECommercePublished = true, Articles = new[] { new Article { IsDefault = true, NationId = 1, ProductId = 1 } } }); + context.ProductArticles.Add(new ProductArticleB { Name = "P2", B = "b", Products = new[] { product.Entity } }); + product = context.Products.Add(new Product { ECommercePublished = true, Articles = new[] { new Article { IsDefault = true, NationId = 1, ProductId = 1 } } }); + context.ProductArticles.Add(new ProductArticle { Name = "P3", Products = new[] { product.Entity } }); + } + } + + public class ClientContext : LocalDbContext + { + public DbSet Products { get; set; } + public DbSet ProductArticles { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity(); + modelBuilder.Entity(); + } + } +} +public class SubQueryWithMapFromNullableWithInheritance(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) +{ + // Source Types + public class Cable + { + public int CableId { get; set; } + public string Name { get; set; } + public ICollection Ends { get; set; } = new List(); + } + public class CableA : Cable + { + public string A { get; set; } + } + public class CableB : Cable + { + public string B { get; set; } + } + + public class CableEnd + { + [ForeignKey(nameof(CrossConnectId))] + public virtual Cable CrossConnect { get; set; } + [Column(Order = 0), Key] + public int CrossConnectId { get; set; } + [Column(Order = 1), Key] + public string Name { get; set; } + [ForeignKey(nameof(RackId))] + public virtual Rack Rack { get; set; } + public int? RackId { get; set; } + } + + public class DataHall + { + public int DataHallId { get; set; } + public int DataCentreId { get; set; } + public ICollection Racks { get; set; } = new List(); + } + + public class Rack + { + public int RackId { get; set; } + [ForeignKey(nameof(DataHallId))] + public virtual DataHall DataHall { get; set; } + public int DataHallId { get; set; } + } + + // Dest Types + public class CableListModel + { + public int CableId { get; set; } + public CableEndModel AEnd { get; set; } + public CableEndModel AnotherEnd { get; set; } + } + public class CableListModelA : CableListModel + { + public string A { get; set; } + } + public class CableListModelB : CableListModel + { + public string B { get; set; } + } + + public class CableEndModel + { + public string Name { get; set; } + public int? DataHallId { get; set; } + } + + public class ClientContext : LocalDbContext + { + public DbSet Cables { get; set; } + public DbSet CableEnds { get; set; } + public DbSet DataHalls { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().HasKey(c => new { c.CrossConnectId, c.Name }); + modelBuilder.Entity(); + modelBuilder.Entity(); + } + } + + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(ClientContext context) + { + var rack = new Rack(); + var dh = new DataHall { DataCentreId = 10, Racks = { rack } }; + context.DataHalls.Add(dh); + + context.Cables.Add(new CableA + { + Name = "C1", + A = "a", + Ends = new List() + { + new CableEnd { Name = "A", Rack = rack}, + new CableEnd { Name = "B" }, + } + }); + context.Cables.Add(new CableB + { + Name = "C2", + B = "b", + Ends = new List() + { + new CableEnd { Name = "A", Rack = rack}, + new CableEnd { Name = "B" }, + } + }); + context.Cables.Add(new Cable + { + Name = "C3", + Ends = new List() + { + new CableEnd { Name = "A", Rack = rack}, + new CableEnd { Name = "B" }, + } + }); + } + } + + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap().ForMember(dest => dest.DataHallId, opt => opt.MapFrom(src => src.Rack.DataHall.DataCentreId)); + cfg.CreateMap() + .ForMember(dest => dest.AEnd, opt => opt.MapFrom(src => src.Ends.FirstOrDefault(x => x.Name == "A"))) + .ForMember(dest => dest.AnotherEnd, opt => opt.MapFrom(src => src.Ends.FirstOrDefault(x => x.Name == "B"))) + .Include() + .Include(); + cfg.CreateMap(); + cfg.CreateMap(); + }); + + [Fact] + public void Should_project_ok() + { + using (var context = Fixture.CreateContext()) + { + var projection = ProjectTo(context.Cables.OrderBy(c => c.Name)); + var list = projection.ToList(); + + var resultA = list[0].ShouldBeOfType(); + resultA.AEnd.DataHallId.ShouldBe(10); + resultA.AnotherEnd.DataHallId.ShouldBeNull(); + resultA.A.ShouldBe("a"); + + var resultB = list[1].ShouldBeOfType(); + resultB.AEnd.DataHallId.ShouldBe(10); + resultB.AnotherEnd.DataHallId.ShouldBeNull(); + resultB.B.ShouldBe("b"); + + var result = list[2].ShouldBeOfType(); + result.AEnd.DataHallId.ShouldBe(10); + result.AnotherEnd.DataHallId.ShouldBeNull(); + } + } +} +public class MapObjectPropertyFromSubQueryCustomSourceWithInheritance(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) +{ + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap(); + cfg.CreateMap() + .ForMember(dest => dest.Owner, opt => opt.MapFrom(src => src.Owners.FirstOrDefault())); + cfg.CreateMap() + .ForMember(dest => dest.Brand, opt => opt.MapFrom(src => src.Product.Brand)) + .Include() + .Include(); + cfg.CreateMap(); + cfg.CreateMap(); + }); + + public class Owner + { + public int Id { get; set; } + public string Name { get; set; } + } + public class Brand + { + public int Id { get; set; } + public List Owners { get; set; } = new List(); + } + public class Product + { + public int Id { get; set; } + public Brand Brand { get; set; } + } + public class ProductReview + { + public int Id { get; set; } + public Product Product { get; set; } + } + public class ProductReviewA : ProductReview + { + public string A { get; set; } + } + public class ProductReviewB : ProductReview + { + public string B { get; set; } + } + /* Destination types */ + public class ProductReviewDto + { + public int Id { get; set; } + public BrandDto Brand { get; set; } + } + public class ProductReviewADto : ProductReviewDto + { + public string A { get; set; } + } + public class ProductReviewBDto : ProductReviewDto + { + public string B { get; set; } + } + public class BrandDto + { + public int Id { get; set; } + public OwnerDto Owner { get; set; } + } + public class OwnerDto + { + public int Id { get; set; } + public string Name { get; set; } + } + + public class ClientContext : LocalDbContext + { + public DbSet Owners { get; set; } + public DbSet Products { get; set; } + public DbSet Brands { get; set; } + public DbSet ProductReviews { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(); + modelBuilder.Entity(); + } + } + + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(ClientContext context) + { + context.ProductReviews.Add(new ProductReviewA + { A = "a", Product = new Product { Brand = new Brand { Owners = { new Owner { Name = "Owner" } } } } }); + context.ProductReviews.Add(new ProductReviewB + { B = "b", Product = new Product { Brand = new Brand { Owners = { new Owner() } } } }); + context.ProductReviews.Add(new ProductReview { Product = new Product() }); + } + } + + [Fact] + public void Should_project_ok() + { + using (var context = Fixture.CreateContext()) + { + var projection = ProjectTo(context.ProductReviews); + var results = projection.ToArray(); + results.Any(result => result?.Brand?.Owner?.Name == "Owner").ShouldBeTrue(); + results.Any(result => result?.Brand?.Owner == null).ShouldBeTrue(); + results.Any(result => result?.Brand == null).ShouldBeTrue(); + results.OfType().Any(result => result.A == "a").ShouldBeTrue(); + results.OfType().Any(result => result.B == "b").ShouldBeTrue(); + } + } } \ No newline at end of file diff --git a/src/IntegrationTests/CustomProjection.cs b/src/IntegrationTests/CustomProjection.cs index 2cce4cb637..aad9b84a7c 100644 --- a/src/IntegrationTests/CustomProjection.cs +++ b/src/IntegrationTests/CustomProjection.cs @@ -1,6 +1,6 @@ namespace AutoMapper.IntegrationTests; -public class CustomProjectionStringToString : IntegrationTest +public class CustomProjectionStringToString(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { public class TestContext : LocalDbContext { @@ -24,7 +24,7 @@ protected override void Seed(TestContext testContext) [Fact] public void Direct_assignability_shouldnt_trump_custom_projection() { - using (var context = new TestContext()) + using (var context = Fixture.CreateContext()) { ProjectTo(context.Sources).Single().Greeting.ShouldBe(_niceGreeting); } @@ -52,7 +52,7 @@ class TargetChild public string Greeting { get; set; } } } -public class CustomProjectionCustomClasses : IntegrationTest +public class CustomProjectionCustomClasses(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { public class TestContext : LocalDbContext { @@ -75,7 +75,7 @@ protected override void Seed(TestContext testContext) [Fact] public void Should_work() { - using (var context = new TestContext()) + using (var context = Fixture.CreateContext()) { ProjectTo(context.Sources).Single().Greeting.ShouldBe(_niceGreeting); } @@ -103,7 +103,7 @@ class TargetChild public string Greeting { get; set; } } } -public class CustomProjectionChildClasses : IntegrationTest +public class CustomProjectionChildClasses(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { public class TestContext : LocalDbContext { @@ -126,7 +126,7 @@ protected override void Seed(TestContext testContext) [Fact] public void Should_work() { - using (var context = new TestContext()) + using (var context = Fixture.CreateContext()) { ProjectTo(context.Sources).Single().Child.Greeting.ShouldBe(_niceGreeting); } diff --git a/src/IntegrationTests/ExplicitExpansion/ConstructorExplicitExpansionOverride.cs b/src/IntegrationTests/ExplicitExpansion/ConstructorExplicitExpansionOverride.cs new file mode 100644 index 0000000000..fc11902bb0 --- /dev/null +++ b/src/IntegrationTests/ExplicitExpansion/ConstructorExplicitExpansionOverride.cs @@ -0,0 +1,38 @@ +namespace AutoMapper.IntegrationTests.ExplicitExpansion; + +public class ConstructorExplicitExpansionOverride(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { + public class Entity { + public int Id { get; set; } + public string Name { get; set; } + } + + public class SubEntity : Entity { + } + + record Dto(string Name); + record SubDto(string Name) : Dto(Name) { } + + public class Context : LocalDbContext { + public DbSet Entities { get; set; } + public DbSet SubEntities { get; set; } + } + public class DatabaseInitializer : DropCreateDatabaseAlways { + protected override void Seed(Context context) { + context.Entities.Add(new() { Name = "base" }); + context.SubEntities.Add(new() { Name = "derived" }); + base.Seed(context); + } + } + protected override MapperConfiguration CreateConfiguration() => new(c => { + c.CreateMap().ForCtorParam("Name", o => o.ExplicitExpansion()); + c.CreateMap().IncludeBase().ForCtorParam("Name", o => o.ExplicitExpansion(false)); + }); + [Fact] + public void Should_work() { + using var context = Fixture.CreateContext(); + var dtos = ProjectTo(context.Entities).ToList(); + dtos.Count.ShouldBe(2); + dtos[0].ShouldBeOfType().Name.ShouldBeNull(); + dtos[1].ShouldBeOfType().Name.ShouldBe("derived"); + } +} \ No newline at end of file diff --git a/src/IntegrationTests/ExplicitExpansion/ExpandCollections.cs b/src/IntegrationTests/ExplicitExpansion/ExpandCollections.cs index 715fe25711..56da793d8e 100644 --- a/src/IntegrationTests/ExplicitExpansion/ExpandCollections.cs +++ b/src/IntegrationTests/ExplicitExpansion/ExpandCollections.cs @@ -1,6 +1,6 @@ namespace AutoMapper.IntegrationTests.ExplicitExpansion; -public class ExpandCollections : IntegrationTest +public class ExpandCollections(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { TrainingCourseDto _course; @@ -14,7 +14,7 @@ public class ExpandCollections : IntegrationTest(context.TrainingCourses, null, c => c.Content.Select(co => co.Category)).FirstOrDefault(n => n.CourseName == "Course 1"); } diff --git a/src/IntegrationTests/ExplicitExpansion/ExpandCollectionsOverride.cs b/src/IntegrationTests/ExplicitExpansion/ExpandCollectionsOverride.cs new file mode 100644 index 0000000000..035a3d43e4 --- /dev/null +++ b/src/IntegrationTests/ExplicitExpansion/ExpandCollectionsOverride.cs @@ -0,0 +1,110 @@ +namespace AutoMapper.IntegrationTests.ExplicitExpansion; + +public class ExpandCollectionsOverride(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) +{ + TrainingCourseDto _course; + + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap(); + cfg.CreateMap() + .ForMember(p => p.CourseName, opt => opt.ExplicitExpansion()); + cfg.CreateMap().ForMember(c => c.Category, o => o.ExplicitExpansion()); + cfg.CreateMap() + .IncludeBase() + .ForMember(c => c.CourseName, opt => opt.ExplicitExpansion(false)); + }); + + [Fact] + public void Should_notexpand_courseName() { + using (var context = Fixture.CreateContext()) { + _course = ProjectTo(context.TrainingCourses).FirstOrDefault(); + } + _course.CourseName.ShouldBeNull(); + } + + [Fact] + public void Should_expand_courseName() { + using (var context = Fixture.CreateContext()) { + _course = ProjectTo(context.TrainingCourses).FirstOrDefault(); + } + _course.CourseName.ShouldBe("Course 1"); + } + + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(ClientContext context) + { + var category = new Category { CategoryName = "Category 1" }; + var course = new TrainingCourse { CourseName = "Course 1" }; + context.TrainingCourses.Add(course); + var content = new TrainingContent { ContentName = "Content 1", Category = category }; + context.TrainingContents.Add(content); + course.Content.Add(content); + } + } + + public class ClientContext : LocalDbContext + { + public DbSet Categories { get; set; } + public DbSet TrainingCourses { get; set; } + public DbSet TrainingContents { get; set; } + } + + public class TrainingCourse + { + [Key] + public int CourseId { get; set; } + + public string CourseName { get; set; } + + public virtual IList Content { get; set; } = new List(); + } + + public class TrainingContent + { + [Key] + public int ContentId { get; set; } + + public string ContentName { get; set; } + public string CaptionName { get; set; } + + public Category Category { get; set; } + } + + public class Category + { + public int CategoryId { get; set; } + public string CategoryName { get; set; } + } + + + public class TrainingCourseDto + { + public int CourseId { get; set; } + + public string CourseName { get; set; } + + public virtual IList Content { get; set; } + } + + public class TrainingCourseDetailDto : TrainingCourseDto + { + } + + public class CategoryDto + { + public int CategoryId { get; set; } + public string CategoryName { get; set; } + } + + public class TrainingContentDto + { + public int ContentId { get; set; } + + public string ContentName { get; set; } + + public CategoryDto Category { get; set; } + } + +} \ No newline at end of file diff --git a/src/IntegrationTests/ExplicitExpansion/ExpandCollectionsWithStrings.cs b/src/IntegrationTests/ExplicitExpansion/ExpandCollectionsWithStrings.cs index 059340dda0..8b57dc875e 100644 --- a/src/IntegrationTests/ExplicitExpansion/ExpandCollectionsWithStrings.cs +++ b/src/IntegrationTests/ExplicitExpansion/ExpandCollectionsWithStrings.cs @@ -1,6 +1,6 @@ namespace AutoMapper.IntegrationTests.ExplicitExpansion; -public class ExpandCollectionsWithStrings : IntegrationTest +public class ExpandCollectionsWithStrings(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { TrainingCourseDto _course; @@ -14,7 +14,7 @@ public class ExpandCollectionsWithStrings : IntegrationTest(context.TrainingCourses, null, "Content.Category").FirstOrDefault(n => n.CourseName == "Course 1"); } diff --git a/src/IntegrationTests/ExplicitExpansion/ExpandMembersPath.cs b/src/IntegrationTests/ExplicitExpansion/ExpandMembersPath.cs index 98bdf9715f..cc514950d4 100644 --- a/src/IntegrationTests/ExplicitExpansion/ExpandMembersPath.cs +++ b/src/IntegrationTests/ExplicitExpansion/ExpandMembersPath.cs @@ -1,6 +1,6 @@ namespace AutoMapper.IntegrationTests.ExplicitExpansion; -public class ExpandMembersPath : IntegrationTest +public class ExpandMembersPath(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { protected override MapperConfiguration CreateConfiguration() => new(cfg => { @@ -38,7 +38,7 @@ public class ExpandMembersPath : IntegrationTest(context.Class1Set, null, r => r.Class2DTO.Class3DTO).ToArray(); } @@ -49,7 +49,7 @@ public void Should_expand_all_members_in_path() public void Should_expand_all_members_in_path_with_strings() { Class1DTO[] dtos; - using(TestContext context = new TestContext()) + using(TestContext context = Fixture.CreateContext()) { dtos = ProjectTo(context.Class1Set, null, "Class2DTO.Class3DTO").ToArray(); } diff --git a/src/IntegrationTests/ExplicitExpansion/ExplicitlyExpandCollectionsAndChildReferences.cs b/src/IntegrationTests/ExplicitExpansion/ExplicitlyExpandCollectionsAndChildReferences.cs index 8faa2be65b..dbe8cb16b3 100644 --- a/src/IntegrationTests/ExplicitExpansion/ExplicitlyExpandCollectionsAndChildReferences.cs +++ b/src/IntegrationTests/ExplicitExpansion/ExplicitlyExpandCollectionsAndChildReferences.cs @@ -1,6 +1,6 @@ namespace AutoMapper.IntegrationTests.ExplicitExpansion; -public class ExplicitlyExpandCollectionsAndChildReferences : IntegrationTest +public class ExplicitlyExpandCollectionsAndChildReferences(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { TrainingCourseDto _course; @@ -14,7 +14,7 @@ public class ExplicitlyExpandCollectionsAndChildReferences : IntegrationTest(context.TrainingCourses, null, c => c.Content.Select(co => co.Category)).FirstOrDefault(n => n.CourseName == "Course 1"); } diff --git a/src/IntegrationTests/ExplicitExpansion/MembersToExpandExpressions.cs b/src/IntegrationTests/ExplicitExpansion/MembersToExpandExpressions.cs index 9f2a1e868e..77fec4fb9c 100644 --- a/src/IntegrationTests/ExplicitExpansion/MembersToExpandExpressions.cs +++ b/src/IntegrationTests/ExplicitExpansion/MembersToExpandExpressions.cs @@ -1,6 +1,6 @@ namespace AutoMapper.IntegrationTests.ExplicitExpansion; -public class MembersToExpandExpressions : AutoMapperSpecBase, IAsyncLifetime +public class MembersToExpandExpressions(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { public class SourceDeepInner { @@ -47,7 +47,7 @@ protected override void Seed(TestContext testContext) [Fact] public void Should_project_ok() { - using (var context = new TestContext()) + using (var context = Fixture.CreateContext()) { ProjectTo(context.Sources, null, _ => _.Name).First().Name.ShouldBe(_source.Name); ProjectTo(context.Sources, null, _ => _.Desc).First().Desc.ShouldBe(_source.Desc); @@ -56,12 +56,4 @@ public void Should_project_ok() ProjectTo(context.Sources, null, _ => _.DeepFlattened).First().DeepFlattened.ShouldBe(_source.Inner.Deep.Desc); } } - public async Task InitializeAsync() - { - var initializer = new DatabaseInitializer(); - - await initializer.Migrate(); - } - - public Task DisposeAsync() => Task.CompletedTask; } \ No newline at end of file diff --git a/src/IntegrationTests/ExplicitExpansion/NestedExplicitExpand.cs b/src/IntegrationTests/ExplicitExpansion/NestedExplicitExpand.cs index 91ede3f5e1..4a32afc8ac 100644 --- a/src/IntegrationTests/ExplicitExpansion/NestedExplicitExpand.cs +++ b/src/IntegrationTests/ExplicitExpansion/NestedExplicitExpand.cs @@ -1,6 +1,6 @@ namespace AutoMapper.IntegrationTests.ExplicitExpansion; -public class NestedExplicitExpand : IntegrationTest +public class NestedExplicitExpand(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { protected override MapperConfiguration CreateConfiguration() => new(cfg => { @@ -38,7 +38,7 @@ public class NestedExplicitExpand : IntegrationTest(context.Class1Set, null, r => r.Class2DTO, r => r.Class2DTO.Class3DTO).ToArray(); } @@ -49,7 +49,7 @@ public void Should_handle_nested_explicit_expand_with_expressions() public void Should_handle_nested_explicit_expand_with_strings() { Class1DTO[] dtos; - using(TestContext context = new TestContext()) + using(TestContext context = Fixture.CreateContext()) { dtos = ProjectTo(context.Class1Set, null, "Class2DTO", "Class2DTO.Class3DTO").ToArray(); } diff --git a/src/IntegrationTests/ExplicitExpansion/NestedExplicitExpandWithFields.cs b/src/IntegrationTests/ExplicitExpansion/NestedExplicitExpandWithFields.cs index b237c34f9f..17f69750d1 100644 --- a/src/IntegrationTests/ExplicitExpansion/NestedExplicitExpandWithFields.cs +++ b/src/IntegrationTests/ExplicitExpansion/NestedExplicitExpandWithFields.cs @@ -1,6 +1,6 @@ namespace AutoMapper.IntegrationTests.ExplicitExpansion; -public class NestedExplicitExpandWithFields : IntegrationTest +public class NestedExplicitExpandWithFields(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { protected override MapperConfiguration CreateConfiguration() => new(cfg => { @@ -38,7 +38,7 @@ public class NestedExplicitExpandWithFields : IntegrationTest(context.Class1Set, null, r => r.Class2DTO, r => r.Class2DTO.Class3DTO).ToArray(); } @@ -49,7 +49,7 @@ public void Should_handle_nested_explicit_expand_with_expressions() public void Should_handle_nested_explicit_expand_with_strings() { Class1DTO[] dtos; - using(TestContext context = new TestContext()) + using(TestContext context = Fixture.CreateContext()) { dtos = ProjectTo(context.Class1Set, null, "Class2DTO", "Class2DTO.Class3DTO").ToArray(); } diff --git a/src/IntegrationTests/ExplicitExpansion/ProjectAndAllowNullCollections.cs b/src/IntegrationTests/ExplicitExpansion/ProjectAndAllowNullCollections.cs index a5909cfa69..8e35bf79ae 100644 --- a/src/IntegrationTests/ExplicitExpansion/ProjectAndAllowNullCollections.cs +++ b/src/IntegrationTests/ExplicitExpansion/ProjectAndAllowNullCollections.cs @@ -2,7 +2,7 @@ namespace AutoMapper.IntegrationTests.ExplicitExpansion; -public class ProjectAndAllowNullCollections : IntegrationTest +public class ProjectAndAllowNullCollections(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { public class Foo { @@ -122,7 +122,7 @@ public void Configure(EntityTypeBuilder builder) [Fact] public void Should_work() { - using(var context = new MyContext()) + using(var context = Fixture.CreateContext()) { var foos = ProjectTo(context.Foos.AsNoTracking(), null, m => m.Bars).ToList(); diff --git a/src/IntegrationTests/ExplicitExpansion/ProjectionWithExplicitExpansion.cs b/src/IntegrationTests/ExplicitExpansion/ProjectionWithExplicitExpansion.cs index c5ed97c26a..ce23da5484 100644 --- a/src/IntegrationTests/ExplicitExpansion/ProjectionWithExplicitExpansion.cs +++ b/src/IntegrationTests/ExplicitExpansion/ProjectionWithExplicitExpansion.cs @@ -16,7 +16,7 @@ public static void SqlFromShouldStartWith (this string sqlSelect, string tableN // Example of Value Type mapped to appropriate Nullable -public class ProjectionWithExplicitExpansion : IntegrationTest +public class ProjectionWithExplicitExpansion(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { public class SourceDeepInner { @@ -93,7 +93,7 @@ protected override void Seed(Context context) [Fact] public void NoExplicitExpansion() { - using (var ctx = new Context()) + using (var ctx = Fixture.CreateContext()) { var dto = ProjectTo(ctx.Sources).ToList().First(); var sqlSelect = ctx.GetLastSelectSqlLogEntry(); @@ -109,7 +109,7 @@ public void NoExplicitExpansion() [Fact] public void ProjectReferenceType() { - using (var ctx = new Context()) + using (var ctx = Fixture.CreateContext()) { var dto = ProjectTo(ctx.Sources, null, _ => _.Name).First(); var sqlSelect = ctx.GetLastSelectSqlLogEntry(); @@ -124,7 +124,7 @@ public void ProjectReferenceType() [Fact] public void ProjectValueType() { - using (var ctx = new Context()) + using (var ctx = Fixture.CreateContext()) { var dto = ProjectTo(ctx.Sources, null, _ => _.Desc).First(); @@ -141,7 +141,7 @@ public void ProjectValueType() [Fact] public void ProjectBoth() { - using (var ctx = new Context()) + using (var ctx = Fixture.CreateContext()) { var dto = ProjectTo(ctx.Sources, null, _ => _.Name, _ => _.Desc).First(); @@ -158,7 +158,7 @@ public void ProjectBoth() [Fact] public void ProjectInner() { - using (var ctx = new Context()) + using (var ctx = Fixture.CreateContext()) { var dto = ProjectTo(ctx.Sources, null, _ => _.InnerDescFlattened).ToList().First(); @@ -176,7 +176,7 @@ public void ProjectInner() [Fact] public void ProjectInnerNonKey() { - using (var ctx = new Context()) + using (var ctx = Fixture.CreateContext()) { var dto = ProjectTo(ctx.Sources, null, _ => _.InnerFlattenedNonKey).ToList().First(); @@ -197,7 +197,7 @@ public void ProjectInnerNonKey() [Fact] public void ProjectDeepInner() { - using (var ctx = new Context()) + using (var ctx = Fixture.CreateContext()) { var dto = ProjectTo(ctx.Sources, null, _ => _.DeepFlattened).ToList().First(); var sqlSelect = ctx.GetLastSelectSqlLogEntry(); @@ -214,7 +214,7 @@ public void ProjectDeepInner() } } } -public class ConstructorExplicitExpansion : IntegrationTest +public class ConstructorExplicitExpansion(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { public class Entity { @@ -238,7 +238,7 @@ protected override void Seed(Context context) [Fact] public void Should_work() { - using var context = new Context(); + using var context = Fixture.CreateContext(); var dto = ProjectTo(context.Entities).Single(); dto.Name.ShouldBeNull(); dto = ProjectTo(context.Entities, null, d=>d.Name).Single(); diff --git a/src/IntegrationTests/ICollectionAggregateProjections.cs b/src/IntegrationTests/ICollectionAggregateProjections.cs index eda118b842..6d0a56c4b9 100644 --- a/src/IntegrationTests/ICollectionAggregateProjections.cs +++ b/src/IntegrationTests/ICollectionAggregateProjections.cs @@ -1,5 +1,5 @@ namespace AutoMapper.IntegrationTests; -public class ICollectionAggregateProjections : IntegrationTest +public class ICollectionAggregateProjections(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { public class Customer { @@ -54,7 +54,7 @@ public class CustomerItemCodes [Fact] public void Can_map_with_projection() { - using (var context = new Context()) + using (var context = Fixture.CreateContext()) { var result = ProjectTo(context.Customers.Select(customer => new CustomerItemCodes { diff --git a/src/IntegrationTests/IEnumerableAggregateProjections.cs b/src/IntegrationTests/IEnumerableAggregateProjections.cs index 595e119b3b..558cdf935e 100644 --- a/src/IntegrationTests/IEnumerableAggregateProjections.cs +++ b/src/IntegrationTests/IEnumerableAggregateProjections.cs @@ -1,5 +1,5 @@ namespace AutoMapper.IntegrationTests; -public class IEnumerableAggregateProjections : IntegrationTest +public class IEnumerableAggregateProjections(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { public class Customer { @@ -54,7 +54,7 @@ public class CustomerItemCodes [Fact] public void Can_map_with_projection() { - using (var context = new Context()) + using (var context = Fixture.CreateContext()) { var result = ProjectTo(context.Customers.Select(customer => new CustomerItemCodes { diff --git a/src/IntegrationTests/IEnumerableMemberProjections.cs b/src/IntegrationTests/IEnumerableMemberProjections.cs index e6de82c28a..dc8c81aae1 100644 --- a/src/IntegrationTests/IEnumerableMemberProjections.cs +++ b/src/IntegrationTests/IEnumerableMemberProjections.cs @@ -1,5 +1,5 @@ namespace AutoMapper.IntegrationTests; -public class IEnumerableMemberProjections : IntegrationTest +public class IEnumerableMemberProjections(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { public class Customer { @@ -61,7 +61,7 @@ public class CustomerItemCodes [Fact] public void Can_map_to_ienumerable() { - using (var context = new Context()) + using (var context = Fixture.CreateContext()) { var result = ProjectTo(context.Customers).Single(); diff --git a/src/IntegrationTests/IncludeMembers.cs b/src/IntegrationTests/IncludeMembers.cs index a1474dc9f5..cd58be68f1 100644 --- a/src/IntegrationTests/IncludeMembers.cs +++ b/src/IntegrationTests/IncludeMembers.cs @@ -1,6 +1,6 @@ namespace AutoMapper.IntegrationTests; -public class IncludeMembers : IntegrationTest +public class IncludeMembers(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { public class Source { @@ -53,7 +53,7 @@ protected override void Seed(Context context) [Fact] public void Should_flatten() { - using(var context = new Context()) + using(var context = Fixture.CreateContext()) { var projectTo = ProjectTo(context.Sources); var result = projectTo.Single(); @@ -63,7 +63,7 @@ public void Should_flatten() } } } -public class IncludeMembersExplicitExpansion : IntegrationTest +public class IncludeMembersExplicitExpansion(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { public class Source { @@ -116,7 +116,7 @@ protected override void Seed(Context context) [Fact] public void Should_flatten() { - using (var context = new Context()) + using (var context = Fixture.CreateContext()) { var projectTo = ProjectTo(context.Sources, null, d=>d.Title); var result = projectTo.Single(); @@ -127,7 +127,8 @@ public void Should_flatten() } } -public class IncludeMembersFirstOrDefault : IntegrationTest +public class IncludeMembersFirstOrDefault(DatabaseFixture databaseFixture) + : IntegrationTest(databaseFixture) { public class Source { @@ -178,6 +179,7 @@ protected override void Seed(Context context) base.Seed(context); } } + protected override MapperConfiguration CreateConfiguration() => new(cfg => { cfg.CreateProjection().IncludeMembers(s => s.InnerSources.FirstOrDefault(), s => s.OtherInnerSources.FirstOrDefault()); @@ -187,7 +189,7 @@ protected override void Seed(Context context) [Fact] public void Should_flatten() { - using (var context = new Context()) + using (var context = Fixture.CreateContext()) { var projectTo = ProjectTo(context.Sources); FirstOrDefaultCounter.Assert(projectTo, 2); @@ -201,7 +203,7 @@ public void Should_flatten() } } -public class IncludeMembersFirstOrDefaultWithMapFromExpression : IntegrationTest +public class IncludeMembersFirstOrDefaultWithMapFromExpression(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { public class Source { @@ -261,7 +263,7 @@ protected override void Seed(Context context) [Fact] public void Should_flatten() { - using (var context = new Context()) + using (var context = Fixture.CreateContext()) { var projectTo = ProjectTo(context.Sources); FirstOrDefaultCounter.Assert(projectTo, 2); @@ -274,7 +276,7 @@ public void Should_flatten() } } } -public class IncludeMembersFirstOrDefaultWithSubqueryMapFrom : IntegrationTest +public class IncludeMembersFirstOrDefaultWithSubqueryMapFrom(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { public class Source { @@ -354,7 +356,7 @@ protected override void Seed(Context context) [Fact] public void Should_flatten() { - using (var context = new Context()) + using (var context = Fixture.CreateContext()) { var projectTo = ProjectTo(context.Sources); FirstOrDefaultCounter.Assert(projectTo, 4); @@ -367,7 +369,7 @@ public void Should_flatten() } } } -public class IncludeMembersSelectFirstOrDefaultWithSubqueryMapFrom : IntegrationTest +public class IncludeMembersSelectFirstOrDefaultWithSubqueryMapFrom(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { public class Source { @@ -458,7 +460,7 @@ protected override void Seed(Context context) [Fact] public void Should_flatten() { - using (var context = new Context()) + using (var context = Fixture.CreateContext()) { var projectTo = ProjectTo(context.Sources); FirstOrDefaultCounter.Assert(projectTo, 4); @@ -471,7 +473,7 @@ public void Should_flatten() } } } -public class SubqueryMapFromWithIncludeMembersFirstOrDefault : IntegrationTest +public class SubqueryMapFromWithIncludeMembersFirstOrDefault(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { public class Source { @@ -553,7 +555,7 @@ protected override void Seed(Context context) [Fact] public void Should_flatten() { - using (var context = new Context()) + using (var context = Fixture.CreateContext()) { var projectTo = ProjectTo(context.Sources); FirstOrDefaultCounter.Assert(projectTo, 6); @@ -566,7 +568,7 @@ public void Should_flatten() } } } -public class SubqueryMapFromWithIncludeMembersSelectFirstOrDefault : IntegrationTest +public class SubqueryMapFromWithIncludeMembersSelectFirstOrDefault(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { public class Source { @@ -648,7 +650,7 @@ protected override void Seed(Context context) [Fact] public void Should_flatten() { - using (var context = new Context()) + using (var context = Fixture.CreateContext()) { var projectTo = ProjectTo(context.Sources); FirstOrDefaultCounter.Assert(projectTo, 6); @@ -661,7 +663,7 @@ public void Should_flatten() } } } -public class SubqueryMapFromWithIncludeMembersSelectMemberFirstOrDefault : IntegrationTest +public class SubqueryMapFromWithIncludeMembersSelectMemberFirstOrDefault(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { public class Source { @@ -754,7 +756,7 @@ protected override void Seed(Context context) [Fact] public void Should_flatten() { - using (var context = new Context()) + using (var context = Fixture.CreateContext()) { var projectTo = ProjectTo(context.Sources); FirstOrDefaultCounter.Assert(projectTo, 6); @@ -767,7 +769,7 @@ public void Should_flatten() } } } -public class IncludeMembersWithMapFromExpression : IntegrationTest +public class IncludeMembersWithMapFromExpression(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { public class Source { @@ -819,7 +821,7 @@ protected override void Seed(Context context) [Fact] public void Should_flatten_with_MapFrom() { - using(var context = new Context()) + using(var context = Fixture.CreateContext()) { var result = ProjectTo(context.Sources).Single(); result.Name.ShouldBe("name"); @@ -829,7 +831,7 @@ public void Should_flatten_with_MapFrom() } } -public class IncludeMembersWithNullSubstitute : IntegrationTest +public class IncludeMembersWithNullSubstitute(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { public class Source { @@ -882,7 +884,7 @@ protected override void Seed(Context context) [Fact] public void Should_flatten() { - using(var context = new Context()) + using(var context = Fixture.CreateContext()) { var result = ProjectTo(context.Sources).Single(); result.Name.ShouldBe("name"); @@ -891,7 +893,7 @@ public void Should_flatten() } } } -public class IncludeMembersMembersFirstOrDefaultWithNullSubstitute : IntegrationTest +public class IncludeMembersMembersFirstOrDefaultWithNullSubstitute(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { public class Source { @@ -942,7 +944,7 @@ protected override void Seed(Context context) [Fact] public void Should_flatten() { - using (var context = new Context()) + using (var context = Fixture.CreateContext()) { var projectTo = ProjectTo(context.Sources); FirstOrDefaultCounter.Assert(projectTo, 2); @@ -953,7 +955,7 @@ public void Should_flatten() } } } -public class CascadedIncludeMembers : IntegrationTest +public class CascadedIncludeMembers(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { public class Source { @@ -997,7 +999,7 @@ protected override void Seed(Context context) [Fact] public void Should_flatten() { - using (var context = new Context()) + using (var context = Fixture.CreateContext()) { var projectTo = ProjectTo(context.Sources); var result = projectTo.Single(); @@ -1005,4 +1007,2383 @@ public void Should_flatten() result.TheField.ShouldBe(2); } } +} +public class IncludeMembersWithIheritance(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) +{ + public class Source + { + public int Id { get; set; } + public string Name { get; set; } + public InnerSource InnerSource { get; set; } + public OtherInnerSource OtherInnerSource { get; set; } + } + public class SourceA : Source + { + public InnerSourceA InnerSourceA { get; set; } + } + public class SourceB : Source + { + public string B { get; set; } + } + public class InnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + } + public class InnerSourceA + { + public int Id { get; set; } + public string A { get; set; } + } + public class OtherInnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public string Title { get; set; } + } + public class Destination + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public string Title { get; set; } + } + public class DestinationA : Destination + { + public string A { get; set; } + } + public class DestinationB : Destination + { + public string B { get; set; } + } + public class Context : LocalDbContext + { + public DbSet Sources { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity(); + modelBuilder.Entity(); + } + } + + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(Context context) + { + var sourceA = new SourceA { Name = "name1", InnerSourceA = new InnerSourceA() { A = "a" }, InnerSource = new InnerSource { Description = "description" }, OtherInnerSource = new OtherInnerSource { Title = "title" } }; + context.Sources.Add(sourceA); + var sourceB = new SourceB { Name = "name2", B = "b", InnerSource = new InnerSource { Description = "description" }, OtherInnerSource = new OtherInnerSource { Title = "title" } }; + context.Sources.Add(sourceB); + var sourceC = new Source { Name = "name3", InnerSource = new InnerSource { Description = "description" }, OtherInnerSource = new OtherInnerSource { Title = "title" } }; + context.Sources.Add(sourceC); + base.Seed(context); + } + } + + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap().IncludeMembers(s => s.InnerSource, s => s.OtherInnerSource) + .Include() + .Include(); + cfg.CreateMap().IncludeMembers(s => s.InnerSourceA); + cfg.CreateMap(); + cfg.CreateMap(MemberList.None); + cfg.CreateMap(MemberList.None); + cfg.CreateMap(MemberList.None); + }); + [Fact] + public void Should_flatten() + { + using (var context = Fixture.CreateContext()) + { + var projectTo = ProjectTo(context.Sources.OrderBy(p => p.Name)); + var list = projectTo.ToList(); + + var resultA = list[0].ShouldBeOfType(); + resultA.Name.ShouldBe("name1"); + resultA.Description.ShouldBe("description"); + resultA.Title.ShouldBe("title"); + resultA.A.ShouldBe("a"); + + var resultB = list[1].ShouldBeOfType(); + resultB.Name.ShouldBe("name2"); + resultB.Description.ShouldBe("description"); + resultB.Title.ShouldBe("title"); + resultB.B.ShouldBe("b"); + + var resultC = list[2].ShouldBeOfType(); + resultC.Name.ShouldBe("name3"); + resultC.Description.ShouldBe("description"); + resultC.Title.ShouldBe("title"); + } + } +} +public class IncludeMembersExplicitExpansionWithIheritance(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) +{ + public class Source + { + public int Id { get; set; } + public string Name { get; set; } + public InnerSource InnerSource { get; set; } + public OtherInnerSource OtherInnerSource { get; set; } + } + public class SourceA : Source + { + public InnerSourceA InnerSourceA { get; set; } + } + public class SourceB : Source + { + public string B { get; set; } + } + public class InnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + } + public class InnerSourceA + { + public int Id { get; set; } + public string A { get; set; } + } + public class OtherInnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public string Title { get; set; } + } + public class Destination + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public string Title { get; set; } + } + public class DestinationA : Destination + { + public string A { get; set; } + } + public class DestinationB : Destination + { + public string B { get; set; } + } + public class Context : LocalDbContext + { + public DbSet Sources { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity(); + modelBuilder.Entity(); + } + } + + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(Context context) + { + var sourceA = new SourceA { Name = "name1", InnerSourceA = new InnerSourceA { A = "a" }, InnerSource = new InnerSource { Description = "description" }, OtherInnerSource = new OtherInnerSource { Title = "title" } }; + context.Sources.Add(sourceA); + var sourceB = new SourceB { Name = "name2", B = "b", InnerSource = new InnerSource { Description = "description" }, OtherInnerSource = new OtherInnerSource { Title = "title" } }; + context.Sources.Add(sourceB); + var source = new Source { Name = "name3", InnerSource = new InnerSource { Description = "description" }, OtherInnerSource = new OtherInnerSource { Title = "title" } }; + context.Sources.Add(source); + base.Seed(context); + } + } + + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap().IncludeMembers(s => s.InnerSource, s => s.OtherInnerSource) + .Include() + .Include(); + cfg.CreateMap().IncludeMembers(s => s.InnerSourceA); + cfg.CreateMap(); + cfg.CreateProjection(MemberList.None).ForMember(d => d.Description, o => o.ExplicitExpansion()); + cfg.CreateProjection(MemberList.None).ForMember(d => d.Title, o => o.ExplicitExpansion()); + cfg.CreateProjection(MemberList.None).ForMember(d => d.A, o => o.ExplicitExpansion()); + }); + [Fact] + public void Should_flatten() + { + using (var context = Fixture.CreateContext()) + { + var projectTo = ProjectTo(context.Sources.OrderBy(p => p.Name), null, d => d.Title, d => d.GetType() == typeof(DestinationA) ? ((DestinationA)d).A : null); + var list = projectTo.ToList(); + + var resultA = list[0].ShouldBeOfType(); + resultA.Name.ShouldBe("name1"); + resultA.Description.ShouldBeNull(); + resultA.Title.ShouldBe("title"); + resultA.A.ShouldBe("a"); + + var resultB = list[1].ShouldBeOfType(); + resultB.Name.ShouldBe("name2"); + resultB.Description.ShouldBeNull(); + resultB.Title.ShouldBe("title"); + resultB.B.ShouldBe("b"); + + var result = list[2].ShouldBeOfType(); + result.Name.ShouldBe("name3"); + result.Description.ShouldBeNull(); + result.Title.ShouldBe("title"); + } + } +} +public class IncludeMembersFirstOrDefaultWithIheritance(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) +{ + public class Source + { + public int Id { get; set; } + public string Name { get; set; } + public List InnerSources { get; set; } = new List(); + public List OtherInnerSources { get; set; } = new List(); + } + public class SourceA : Source + { + public List InnerSourcesA { get; set; } = new List(); + } + public class SourceB : Source + { + public string B { get; set; } + } + public class InnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public string Publisher { get; set; } + } + public class InnerSourceA + { + public int Id { get; set; } + public string A { get; set; } + } + public class OtherInnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public string Title { get; set; } + public string Author { get; set; } + } + public class Destination + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public string Title { get; set; } + public string Author { get; set; } + public string Publisher { get; set; } + } + public class DestinationA : Destination + { + public string A { get; set; } + } + public class DestinationB : Destination + { + public string B { get; set; } + } + public class Context : LocalDbContext + { + public DbSet Sources { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity(); + modelBuilder.Entity(); + } + } + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(Context context) + { + var sourceA = new SourceA + { + Name = "name1", + InnerSourcesA = { new InnerSourceA { A = "a" } }, + InnerSources = { new InnerSource { Description = "description", Publisher = "publisher" } }, + OtherInnerSources = { new OtherInnerSource { Title = "title", Author = "author" } } + }; + context.Sources.Add(sourceA); + var sourceB = new SourceB + { + Name = "name2", + B = "b", + InnerSources = { new InnerSource { Description = "description", Publisher = "publisher" } }, + OtherInnerSources = { new OtherInnerSource { Title = "title", Author = "author" } } + }; + context.Sources.Add(sourceB); + var source = new Source + { + Name = "name3", + InnerSources = { new InnerSource { Description = "description", Publisher = "publisher" } }, + OtherInnerSources = { new OtherInnerSource { Title = "title", Author = "author" } } + }; + context.Sources.Add(source); + base.Seed(context); + } + } + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap().IncludeMembers(s => s.InnerSources.FirstOrDefault(), s => s.OtherInnerSources.FirstOrDefault()) + .Include() + .Include(); + cfg.CreateMap().IncludeMembers(s => s.InnerSourcesA.FirstOrDefault()); + cfg.CreateMap(); + cfg.CreateMap(MemberList.None); + cfg.CreateMap(MemberList.None); + cfg.CreateMap(MemberList.None); + }); + [Fact] + public void Should_flatten() + { + using (var context = Fixture.CreateContext()) + { + var projectTo = ProjectTo(context.Sources.OrderBy(p => p.Name)); + FirstOrDefaultCounter.Assert(projectTo, 13); + var list = projectTo.ToList(); + + var resultA = list[0].ShouldBeOfType(); + resultA.Name.ShouldBe("name1"); + resultA.Description.ShouldBe("description"); + resultA.Title.ShouldBe("title"); + resultA.Author.ShouldBe("author"); + resultA.Publisher.ShouldBe("publisher"); + resultA.A.ShouldBe("a"); + + var resultB = list[1].ShouldBeOfType(); + resultB.Name.ShouldBe("name2"); + resultB.Description.ShouldBe("description"); + resultB.Title.ShouldBe("title"); + resultB.Author.ShouldBe("author"); + resultB.Publisher.ShouldBe("publisher"); + resultB.B.ShouldBe("b"); + + var result = list[2].ShouldBeOfType(); + result.Name.ShouldBe("name3"); + result.Description.ShouldBe("description"); + result.Title.ShouldBe("title"); + result.Author.ShouldBe("author"); + result.Publisher.ShouldBe("publisher"); + } + } +} +public class IncludeMembersFirstOrDefaultMixedPolymorhism(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) +{ + public class Source + { + public int Id { get; set; } + public string Name { get; set; } + public List InnerSources { get; set; } = new List(); + public List OtherInnerSources { get; set; } = new List(); + } + public class SourceA : Source + { + public List InnerSourcesA { get; set; } = new List(); + } + public class SourceB : Source + { + public string B { get; set; } + } + public class InnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public string Publisher { get; set; } + } + public class InnerSourceA + { + public int Id { get; set; } + public string A { get; set; } + } + public class OtherInnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public string Title { get; set; } + public string Author { get; set; } + } + public class Destination + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public string Title { get; set; } + public string Author { get; set; } + public string Publisher { get; set; } + } + public class DestinationA : Destination + { + public string A { get; set; } + } + public class DestinationB : Destination + { + public string B { get; set; } + } + public class Context : LocalDbContext + { + public DbSet Sources { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity(); + modelBuilder.Entity(); + } + } + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(Context context) + { + var sourceA = new SourceA + { + Name = "name1", + InnerSourcesA = { new InnerSourceA { A = "a" } }, + InnerSources = { new InnerSource { Description = "description", Publisher = "publisher" } }, + OtherInnerSources = { new OtherInnerSource { Title = "title", Author = "author" } } + }; + context.Sources.Add(sourceA); + var sourceB = new SourceB + { + Name = "name2", + B = "b", + InnerSources = { new InnerSource { Description = "description", Publisher = "publisher" } }, + OtherInnerSources = { new OtherInnerSource { Title = "title", Author = "author" } } + }; + context.Sources.Add(sourceB); + var source = new Source + { + Name = "name3", + InnerSources = { new InnerSource { Description = "description", Publisher = "publisher" } }, + OtherInnerSources = { new OtherInnerSource { Title = "title", Author = "author" } } + }; + context.Sources.Add(source); + base.Seed(context); + } + } + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap().IncludeMembers(s => s.InnerSources.FirstOrDefault(), s => s.OtherInnerSources.FirstOrDefault()) + .Include() + .Include() + .Include() + .Include(); + cfg.CreateMap().IncludeMembers(s => s.InnerSourcesA.FirstOrDefault()); + cfg.CreateMap(); + cfg.CreateMap(); + cfg.CreateMap().ForMember(s => s.B, o => o.MapFrom(s => "b")); + cfg.CreateMap(MemberList.None); + cfg.CreateMap(MemberList.None); + cfg.CreateMap(MemberList.None); + cfg.CreateMap(MemberList.None); + }); + [Fact] + public void Should_flatten() + { + using (var context = Fixture.CreateContext()) + { + var projectTo = ProjectTo(context.Sources.OrderBy(p => p.Name)); + var list = projectTo.ToList(); + + var resultA = list[0].ShouldBeOfType(); + resultA.Name.ShouldBe("name1"); + resultA.Description.ShouldBe("description"); + resultA.Title.ShouldBe("title"); + resultA.Author.ShouldBe("author"); + resultA.Publisher.ShouldBe("publisher"); + resultA.A.ShouldBe("a"); + + var resultB = list[1].ShouldBeOfType(); + resultB.Name.ShouldBe("name2"); + resultB.Description.ShouldBe("description"); + resultB.Title.ShouldBe("title"); + resultB.Author.ShouldBe("author"); + resultB.Publisher.ShouldBe("publisher"); + resultB.B.ShouldBe("b"); + + var result = list[2].ShouldBeOfType(); + result.Name.ShouldBe("name3"); + result.Description.ShouldBe("description"); + result.Title.ShouldBe("title"); + result.Author.ShouldBe("author"); + result.Publisher.ShouldBe("publisher"); + } + } +} +public class IncludeMembersFirstOrDefaultNoPolymorhism(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) +{ + public class Source + { + public int Id { get; set; } + public string Name { get; set; } + public List InnerSources { get; set; } = new List(); + public List OtherInnerSources { get; set; } = new List(); + } + public class SourceA : Source + { + public List InnerSourcesA { get; set; } = new List(); + } + public class SourceB : Source + { + public string B { get; set; } + } + public class InnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public string Publisher { get; set; } + } + public class InnerSourceA + { + public int Id { get; set; } + public string A { get; set; } + } + public class OtherInnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public string Title { get; set; } + public string Author { get; set; } + } + public class Destination + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public string Title { get; set; } + public string Author { get; set; } + public string Publisher { get; set; } + } + public class DestinationA : Destination + { + public string A { get; set; } + } + public class DestinationB : Destination + { + public string B { get; set; } + } + public class Context : LocalDbContext + { + public DbSet Sources { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity(); + modelBuilder.Entity(); + } + } + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(Context context) + { + var sourceA = new SourceA + { + Name = "name1", + InnerSourcesA = { new InnerSourceA { A = "a" } }, + InnerSources = { new InnerSource { Description = "description", Publisher = "publisher" } }, + OtherInnerSources = { new OtherInnerSource { Title = "title", Author = "author" } } + }; + context.Sources.Add(sourceA); + var sourceB = new SourceB + { + Name = "name2", + B = "b", + InnerSources = { new InnerSource { Description = "description", Publisher = "publisher" } }, + OtherInnerSources = { new OtherInnerSource { Title = "title", Author = "author" } } + }; + context.Sources.Add(sourceB); + var source = new Source + { + Name = "name3", + InnerSources = { new InnerSource { Description = "description", Publisher = "publisher" } }, + OtherInnerSources = { new OtherInnerSource { Title = "title", Author = "author" } } + }; + context.Sources.Add(source); + base.Seed(context); + } + } + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap().IncludeMembers(s => s.InnerSources.FirstOrDefault(), s => s.OtherInnerSources.FirstOrDefault()) + .Include() + .Include(); + cfg.CreateMap().ForMember(d=>d.Description, o=>o.MapFrom(s=>"descriptionA")); + cfg.CreateMap().ForMember(s => s.B, o => o.MapFrom(s => "b")); + cfg.CreateMap(MemberList.None); + cfg.CreateMap(MemberList.None); + cfg.CreateMap(MemberList.None); + cfg.CreateMap(MemberList.None); + }); + [Fact] + public void Should_flatten() + { + using (var context = Fixture.CreateContext()) + { + var projectTo = ProjectTo(context.Sources.OrderBy(p => p.Name)); + var list = projectTo.ToList(); + + var resultA = list[0].ShouldBeOfType(); + resultA.Name.ShouldBe("name1"); + resultA.Description.ShouldBe("descriptionA"); + resultA.Title.ShouldBe("title"); + resultA.Author.ShouldBe("author"); + resultA.Publisher.ShouldBe("publisher"); + + var resultB = list[1].ShouldBeOfType(); + resultB.Name.ShouldBe("name2"); + resultB.Description.ShouldBe("description"); + resultB.Title.ShouldBe("title"); + resultB.Author.ShouldBe("author"); + resultB.Publisher.ShouldBe("publisher"); + + var result = list[2].ShouldBeOfType(); + result.Name.ShouldBe("name3"); + result.Description.ShouldBe("description"); + result.Title.ShouldBe("title"); + result.Author.ShouldBe("author"); + result.Publisher.ShouldBe("publisher"); + } + } +} +public class IncludeMembersFirstOrDefaultWithMapFromExpressionWithIheritance(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) +{ + public class Source + { + public int Id { get; set; } + public string Name { get; set; } + public List InnerSources { get; set; } = new List(); + public List OtherInnerSources { get; set; } = new List(); + } + public class SourceA : Source + { + public List InnerSourcesA { get; set; } = new List(); + } + public class SourceB : Source + { + public string B { get; set; } + } + public class InnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public string Description1 { get; set; } + public string Publisher { get; set; } + } + + public class InnerSourceA + { + public int Id { get; set; } + public string A { get; set; } + } + public class OtherInnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public string Title1 { get; set; } + public string Author { get; set; } + } + public class Destination + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public string Title { get; set; } + public string Author { get; set; } + public string Publisher { get; set; } + } + public class DestinationA : Destination + { + public string A { get; set; } + } + public class DestinationB : Destination + { + public string B { get; set; } + } + public class Context : LocalDbContext + { + public DbSet Sources { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity(); + modelBuilder.Entity(); + } + } + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(Context context) + { + var sourceA = new SourceA + { + Name = "name1", + InnerSourcesA = { new InnerSourceA { A = "a" } }, + InnerSources = { new InnerSource { Description1 = "description", Publisher = "publisher" } }, + OtherInnerSources = { new OtherInnerSource { Title1 = "title", Author = "author" } } + }; + context.Sources.Add(sourceA); + var sourceB = new SourceB + { + Name = "name2", + B = "b", + InnerSources = { new InnerSource { Description1 = "description", Publisher = "publisher" } }, + OtherInnerSources = { new OtherInnerSource { Title1 = "title", Author = "author" } } + }; + context.Sources.Add(sourceB); + var source = new Source + { + Name = "name3", + InnerSources = { new InnerSource { Description1 = "description", Publisher = "publisher" } }, + OtherInnerSources = { new OtherInnerSource { Title1 = "title", Author = "author" } } + }; + context.Sources.Add(source); + base.Seed(context); + } + } + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap().IncludeMembers(s => s.InnerSources.FirstOrDefault(), s => s.OtherInnerSources.FirstOrDefault()) + .Include() + .Include(); + cfg.CreateMap().IncludeMembers(s => s.InnerSourcesA.FirstOrDefault()); + cfg.CreateMap(); + cfg.CreateMap(MemberList.None).ForMember(d => d.Description, o => o.MapFrom(s => s.Description1)); + cfg.CreateMap(MemberList.None).ForMember(d => d.Title, o => o.MapFrom(s => s.Title1)); + cfg.CreateMap(MemberList.None).ForMember(d => d.A, o => o.MapFrom(s => s.A)); + }); + [Fact] + public void Should_flatten() + { + using (var context = Fixture.CreateContext()) + { + var projectTo = ProjectTo(context.Sources.OrderBy(p => p.Name)); + FirstOrDefaultCounter.Assert(projectTo, 13); + var list = projectTo.ToList(); + + var resultA = list[0].ShouldBeOfType(); + resultA.Name.ShouldBe("name1"); + resultA.Description.ShouldBe("description"); + resultA.Title.ShouldBe("title"); + resultA.Author.ShouldBe("author"); + resultA.Publisher.ShouldBe("publisher"); + resultA.A.ShouldBe("a"); + + var resultB = list[1].ShouldBeOfType(); + resultB.Name.ShouldBe("name2"); + resultB.Description.ShouldBe("description"); + resultB.Title.ShouldBe("title"); + resultB.Author.ShouldBe("author"); + resultB.Publisher.ShouldBe("publisher"); + resultB.B.ShouldBe("b"); + + var result = list[2].ShouldBeOfType(); + result.Name.ShouldBe("name3"); + result.Description.ShouldBe("description"); + result.Title.ShouldBe("title"); + result.Author.ShouldBe("author"); + result.Publisher.ShouldBe("publisher"); + } + } +} +public class IncludeMembersFirstOrDefaultWithSubqueryMapFromWithIheritance(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) +{ + public class Source + { + public int Id { get; set; } + public string Name { get; set; } + public List InnerSources { get; set; } = new List(); + public List OtherInnerSources { get; set; } = new List(); + } + public class SourceA : Source + { + public List InnerSourcesA { get; set; } = new List(); + } + public class SourceB : Source + { + public string B { get; set; } + } + public class InnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public List InnerSourceDetails { get; } = new List(); + } + public class InnerSourceA + { + public int Id { get; set; } + public List InnerSourceDetails { get; } = new List(); + } + + public class InnerSourceDetails + { + public int Id { get; set; } + public string Description { get; set; } + public string Publisher { get; set; } + } + public class InnerSourceDetailsA + { + public int Id { get; set; } + public string A { get; set; } + } + public class OtherInnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public List OtherInnerSourceDetails { get; } = new List(); + } + public class OtherInnerSourceDetails + { + public int Id { get; set; } + public string Title { get; set; } + public string Author { get; set; } + } + public class Destination + { + public int Id { get; set; } + public string Name { get; set; } + public DestinationDetails Details { get; set; } + public OtherDestinationDetails OtherDetails { get; set; } + } + public class DestinationA : Destination + { + public DestinationADetails DetailsA { get; set; } + } + public class DestinationB : Destination + { + public string B { get; set; } + } + public class DestinationDetails + { + public string Description { get; set; } + public string Publisher { get; set; } + } + public class DestinationADetails + { + public string A { get; set; } + } + public class OtherDestinationDetails + { + public string Title { get; set; } + public string Author { get; set; } + } + public class Context : LocalDbContext + { + public DbSet Sources { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity(); + modelBuilder.Entity(); + } + } + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(Context context) + { + var sourceA = new SourceA + { + Name = "name1", + InnerSourcesA = { new InnerSourceA { InnerSourceDetails = { new InnerSourceDetailsA { A = "a" } } } }, + InnerSources = { new InnerSource { InnerSourceDetails = { new InnerSourceDetails { Description = "description", Publisher = "publisher" } } } }, + OtherInnerSources = { new OtherInnerSource { OtherInnerSourceDetails = { new OtherInnerSourceDetails { Title = "title", Author = "author" } } } } + }; + context.Sources.Add(sourceA); + var sourceB = new SourceB + { + Name = "name2", + B = "b", + InnerSources = { new InnerSource { InnerSourceDetails = { new InnerSourceDetails { Description = "description", Publisher = "publisher" } } } }, + OtherInnerSources = { new OtherInnerSource { OtherInnerSourceDetails = { new OtherInnerSourceDetails { Title = "title", Author = "author" } } } } + }; + context.Sources.Add(sourceB); + var source = new Source + { + Name = "name3", + InnerSources = { new InnerSource { InnerSourceDetails = { new InnerSourceDetails { Description = "description", Publisher = "publisher" } } } }, + OtherInnerSources = { new OtherInnerSource { OtherInnerSourceDetails = { new OtherInnerSourceDetails { Title = "title", Author = "author" } } } } + }; + context.Sources.Add(source); + base.Seed(context); + } + } + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap().IncludeMembers(s => s.InnerSources.FirstOrDefault(), s => s.OtherInnerSources.FirstOrDefault()) + .Include() + .Include(); + cfg.CreateMap().IncludeMembers(s => s.InnerSourcesA.FirstOrDefault()); + cfg.CreateMap(); + cfg.CreateMap(MemberList.None).ForMember(d => d.Details, o => o.MapFrom(s => s.InnerSourceDetails.FirstOrDefault())); + cfg.CreateMap(MemberList.None).ForMember(d => d.OtherDetails, o => o.MapFrom(s => s.OtherInnerSourceDetails.FirstOrDefault())); + cfg.CreateMap(MemberList.None).ForMember(d => d.DetailsA, o => o.MapFrom(s => s.InnerSourceDetails.FirstOrDefault())); + cfg.CreateMap(); + cfg.CreateMap(); + cfg.CreateMap(); + }); + [Fact] + public void Should_flatten() + { + using (var context = Fixture.CreateContext()) + { + var projectTo = ProjectTo(context.Sources.OrderBy(p => p.Name)); + FirstOrDefaultCounter.Assert(projectTo, 40); + var list = projectTo.ToList(); + + var resultA = list[0].ShouldBeOfType(); + resultA.Name.ShouldBe("name1"); + resultA.Details.Description.ShouldBe("description"); + resultA.Details.Publisher.ShouldBe("publisher"); + resultA.OtherDetails.Title.ShouldBe("title"); + resultA.OtherDetails.Author.ShouldBe("author"); + resultA.DetailsA.A.ShouldBe("a"); + + var resultB = list[1].ShouldBeOfType(); + resultB.Name.ShouldBe("name2"); + resultB.Details.Description.ShouldBe("description"); + resultB.Details.Publisher.ShouldBe("publisher"); + resultB.OtherDetails.Title.ShouldBe("title"); + resultB.OtherDetails.Author.ShouldBe("author"); + resultB.B.ShouldBe("b"); + + var result = list[2].ShouldBeOfType(); + result.Name.ShouldBe("name3"); + result.Details.Description.ShouldBe("description"); + result.Details.Publisher.ShouldBe("publisher"); + result.OtherDetails.Title.ShouldBe("title"); + result.OtherDetails.Author.ShouldBe("author"); + } + } +} +public class IncludeMembersSelectFirstOrDefaultWithSubqueryMapFromWithIheritance(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) +{ + public class Source + { + public int Id { get; set; } + public string Name { get; set; } + public List InnerSourceWrappers { get; set; } = new List(); + public List OtherInnerSources { get; set; } = new List(); + } + public class SourceA : Source + { + public List InnerSourceWrappersA { get; set; } = new List(); + } + public class SourceB : Source + { + public string B { get; set; } + } + public class InnerSourceWrapper + { + public int Id { get; set; } + public InnerSource InnerSource { get; set; } + } + public class InnerSourceWrapperA + { + public int Id { get; set; } + public InnerSourceA InnerSource { get; set; } + } + public class InnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public List InnerSourceDetailsWrapper { get; } = new List(); + } + public class InnerSourceA + { + public int Id { get; set; } + public string Name { get; set; } + public List InnerSourceDetailsWrapperA { get; } = new List(); + } + public class InnerSourceDetailsWrapper + { + public int Id { get; set; } + public InnerSourceDetails InnerSourceDetails { get; set; } + } + public class InnerSourceDetailsWrapperA + { + public int Id { get; set; } + public InnerSourceDetailsA InnerSourceDetailsA { get; set; } + } + public class InnerSourceDetails + { + public int Id { get; set; } + public string Description { get; set; } + public string Publisher { get; set; } + } + public class InnerSourceDetailsA + { + public int Id { get; set; } + public string A { get; set; } + } + public class OtherInnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public List OtherInnerSourceDetails { get; } = new List(); + } + public class OtherInnerSourceDetails + { + public int Id { get; set; } + public string Title { get; set; } + public string Author { get; set; } + } + public class Destination + { + public int Id { get; set; } + public string Name { get; set; } + public DestinationDetails Details { get; set; } + public OtherDestinationDetails OtherDetails { get; set; } + } + public class DestinationA : Destination + { + public DestinationDetailsA DetailsA { get; set; } + } + public class DestinationB : Destination + { + public string B { get; set; } + } + public class DestinationDetails + { + public string Description { get; set; } + public string Publisher { get; set; } + } + public class DestinationDetailsA + { + public string A { get; set; } + } + public class OtherDestinationDetails + { + public string Title { get; set; } + public string Author { get; set; } + } + public class Context : LocalDbContext + { + public DbSet Sources { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity(); + modelBuilder.Entity(); + } + } + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(Context context) + { + var sourceA = new SourceA + { + Name = "name1", + InnerSourceWrappersA = { new InnerSourceWrapperA { InnerSource = new InnerSourceA { InnerSourceDetailsWrapperA = { new InnerSourceDetailsWrapperA { InnerSourceDetailsA = new InnerSourceDetailsA { A = "a" } } } } } }, + InnerSourceWrappers = { new InnerSourceWrapper { InnerSource = new InnerSource { InnerSourceDetailsWrapper = { new InnerSourceDetailsWrapper { InnerSourceDetails = new InnerSourceDetails { Description = "description", Publisher = "publisher" } } } } } }, + OtherInnerSources = { new OtherInnerSource { OtherInnerSourceDetails = { new OtherInnerSourceDetails { Title = "title", Author = "author" } } } } + }; + context.Sources.Add(sourceA); + var sourceB = new SourceB + { + Name = "name2", + B = "b", + InnerSourceWrappers = { new InnerSourceWrapper { InnerSource = new InnerSource { InnerSourceDetailsWrapper = { new InnerSourceDetailsWrapper { InnerSourceDetails = new InnerSourceDetails { Description = "description", Publisher = "publisher" } } } } } }, + OtherInnerSources = { new OtherInnerSource { OtherInnerSourceDetails = { new OtherInnerSourceDetails { Title = "title", Author = "author" } } } } + }; + context.Sources.Add(sourceB); + var source = new Source + { + Name = "name3", + InnerSourceWrappers = { new InnerSourceWrapper { InnerSource = new InnerSource { InnerSourceDetailsWrapper = { new InnerSourceDetailsWrapper { InnerSourceDetails = new InnerSourceDetails { Description = "description", Publisher = "publisher" } } } } } }, + OtherInnerSources = { new OtherInnerSource { OtherInnerSourceDetails = { new OtherInnerSourceDetails { Title = "title", Author = "author" } } } } + }; + context.Sources.Add(source); + base.Seed(context); + } + } + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap().IncludeMembers(s => s.InnerSourceWrappers.Select(s => s.InnerSource).FirstOrDefault(), s => s.OtherInnerSources.Select(s => s).FirstOrDefault()) + .Include() + .Include(); + cfg.CreateMap().IncludeMembers(s => s.InnerSourceWrappersA.Select(s => s.InnerSource).FirstOrDefault()); + cfg.CreateMap(); + cfg.CreateMap(MemberList.None).ForMember(d => d.Details, o => o.MapFrom(s => s.InnerSourceDetailsWrapper.Select(s => s.InnerSourceDetails).FirstOrDefault())); + cfg.CreateMap(MemberList.None).ForMember(d => d.OtherDetails, o => o.MapFrom(s => s.OtherInnerSourceDetails.Select(s => s).FirstOrDefault())); + cfg.CreateMap(MemberList.None).ForMember(d => d.DetailsA, o => o.MapFrom(s => s.InnerSourceDetailsWrapperA.Select(s => s.InnerSourceDetailsA).FirstOrDefault())); + cfg.CreateMap(); + cfg.CreateMap(); + cfg.CreateMap(); + }); + [Fact] + public void Should_flatten() + { + using (var context = Fixture.CreateContext()) + { + var projectTo = ProjectTo(context.Sources.OrderBy(p => p.Name)); + FirstOrDefaultCounter.Assert(projectTo, 40); + var list = projectTo.ToList(); + + var resultA = list[0].ShouldBeOfType(); + resultA.Name.ShouldBe("name1"); + resultA.Details.Description.ShouldBe("description"); + resultA.Details.Publisher.ShouldBe("publisher"); + resultA.OtherDetails.Title.ShouldBe("title"); + resultA.OtherDetails.Author.ShouldBe("author"); + resultA.DetailsA.A.ShouldBe("a"); + + var resultB = list[1].ShouldBeOfType(); + resultB.Name.ShouldBe("name2"); + resultB.Details.Description.ShouldBe("description"); + resultB.Details.Publisher.ShouldBe("publisher"); + resultB.OtherDetails.Title.ShouldBe("title"); + resultB.OtherDetails.Author.ShouldBe("author"); + resultB.B.ShouldBe("b"); + + var result = list[2].ShouldBeOfType(); + result.Name.ShouldBe("name3"); + result.Details.Description.ShouldBe("description"); + result.Details.Publisher.ShouldBe("publisher"); + result.OtherDetails.Title.ShouldBe("title"); + result.OtherDetails.Author.ShouldBe("author"); + } + } +} +public class SubqueryMapFromWithIncludeMembersFirstOrDefaultWithIheritance(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) +{ + public class Source + { + public int Id { get; set; } + public string Name { get; set; } + public List InnerSources { get; set; } = new List(); + public List OtherInnerSources { get; set; } = new List(); + } + public class SourceA : Source + { + public List InnerSourcesA { get; set; } = new List(); + } + public class SourceB : Source + { + public string B { get; set; } + } + public class InnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public List InnerSourceDetails { get; } = new List(); + } + public class InnerSourceA + { + public int Id { get; set; } + public string Name { get; set; } + public List InnerSourceDetailsA { get; } = new List(); + } + public class InnerSourceDetails + { + public int Id { get; set; } + public string Description { get; set; } + public string Publisher { get; set; } + } + public class InnerSourceDetailsA + { + public int Id { get; set; } + public string A { get; set; } + } + public class OtherInnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public List OtherInnerSourceDetails { get; } = new List(); + } + public class OtherInnerSourceDetails + { + public int Id { get; set; } + public string Title { get; set; } + public string Author { get; set; } + } + public class Destination + { + public int Id { get; set; } + public string Name { get; set; } + public DestinationDetails Details { get; set; } + public OtherDestinationDetails OtherDetails { get; set; } + } + public class DestinationA : Destination + { + public DestinationDetailsA DetailsA { get; set; } + } + public class DestinationB : Destination + { + public string B { get; set; } + } + public class DestinationDetails + { + public string Description { get; set; } + public string Publisher { get; set; } + } + public class DestinationDetailsA + { + public string A { get; set; } + } + public class OtherDestinationDetails + { + public string Title { get; set; } + public string Author { get; set; } + } + public class Context : LocalDbContext + { + public DbSet Sources { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity(); + modelBuilder.Entity(); + } + } + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(Context context) + { + var sourceA = new SourceA + { + Name = "name1", + InnerSourcesA = { new InnerSourceA { InnerSourceDetailsA = { new InnerSourceDetailsA { A = "a" } } } }, + InnerSources = { new InnerSource { InnerSourceDetails = { new InnerSourceDetails { Description = "description", Publisher = "publisher" } } } }, + OtherInnerSources = { new OtherInnerSource { OtherInnerSourceDetails = { new OtherInnerSourceDetails { Title = "title", Author = "author" } } } } + }; + context.Sources.Add(sourceA); + var sourceB = new SourceB + { + Name = "name2", + B = "b", + InnerSources = { new InnerSource { InnerSourceDetails = { new InnerSourceDetails { Description = "description", Publisher = "publisher" } } } }, + OtherInnerSources = { new OtherInnerSource { OtherInnerSourceDetails = { new OtherInnerSourceDetails { Title = "title", Author = "author" } } } } + }; + context.Sources.Add(sourceB); + var source = new Source + { + Name = "name3", + InnerSources = { new InnerSource { InnerSourceDetails = { new InnerSourceDetails { Description = "description", Publisher = "publisher" } } } }, + OtherInnerSources = { new OtherInnerSource { OtherInnerSourceDetails = { new OtherInnerSourceDetails { Title = "title", Author = "author" } } } } + }; + context.Sources.Add(source); + base.Seed(context); + } + } + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap() + .ForMember(d => d.Details, o => o.MapFrom(s => s.InnerSources.FirstOrDefault())) + .ForMember(d => d.OtherDetails, o => o.MapFrom(s => s.OtherInnerSources.FirstOrDefault())) + .Include() + .Include(); + cfg.CreateMap() + .ForMember(d => d.DetailsA, o => o.MapFrom(s => s.InnerSourcesA.FirstOrDefault())); + cfg.CreateMap(); + cfg.CreateMap().IncludeMembers(s => s.InnerSourceDetails.FirstOrDefault()); + cfg.CreateMap().IncludeMembers(s => s.OtherInnerSourceDetails.FirstOrDefault()); + cfg.CreateMap().IncludeMembers(s => s.InnerSourceDetailsA.FirstOrDefault()); + cfg.CreateMap(); + cfg.CreateMap(); + cfg.CreateMap(); + }); + [Fact] + public void Should_flatten() + { + using (var context = Fixture.CreateContext()) + { + var projectTo = ProjectTo(context.Sources.OrderBy(p => p.Name)); + FirstOrDefaultCounter.Assert(projectTo, 33); + var list = projectTo.ToList(); + + var resultA = list[0].ShouldBeOfType(); + resultA.Name.ShouldBe("name1"); + resultA.Details.Description.ShouldBe("description"); + resultA.Details.Publisher.ShouldBe("publisher"); + resultA.OtherDetails.Title.ShouldBe("title"); + resultA.OtherDetails.Author.ShouldBe("author"); + resultA.DetailsA.A.ShouldBe("a"); + + var resultB = list[1].ShouldBeOfType(); + resultB.Name.ShouldBe("name2"); + resultB.Details.Description.ShouldBe("description"); + resultB.Details.Publisher.ShouldBe("publisher"); + resultB.OtherDetails.Title.ShouldBe("title"); + resultB.OtherDetails.Author.ShouldBe("author"); + resultB.B.ShouldBe("b"); + + var result = list[2].ShouldBeOfType(); + result.Name.ShouldBe("name3"); + result.Details.Description.ShouldBe("description"); + result.Details.Publisher.ShouldBe("publisher"); + result.OtherDetails.Title.ShouldBe("title"); + result.OtherDetails.Author.ShouldBe("author"); + } + } +} +public class SubqueryMapFromWithIncludeMembersSelectFirstOrDefaultWithIheritance(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) +{ + public class Source + { + public int Id { get; set; } + public string Name { get; set; } + public List InnerSources { get; set; } = new List(); + public List OtherInnerSources { get; set; } = new List(); + } + public class SourceA : Source + { + public List InnerSourcesA { get; set; } = new List(); + } + public class SourceB : Source + { + public string B { get; set; } + } + public class InnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public List InnerSourceDetails { get; } = new List(); + } + public class InnerSourceA + { + public int Id { get; set; } + public string Name { get; set; } + public List InnerSourceDetailsA { get; } = new List(); + } + public class InnerSourceDetails + { + public int Id { get; set; } + public string Description { get; set; } + public string Publisher { get; set; } + } + public class InnerSourceDetailsA + { + public int Id { get; set; } + public string A { get; set; } + } + public class OtherInnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public List OtherInnerSourceDetails { get; } = new List(); + } + public class OtherInnerSourceDetails + { + public int Id { get; set; } + public string Title { get; set; } + public string Author { get; set; } + } + public class Destination + { + public int Id { get; set; } + public string Name { get; set; } + public DestinationDetails Details { get; set; } + public OtherDestinationDetails OtherDetails { get; set; } + } + public class DestinationA : Destination + { + public DestinationDetailsA DetailsA { get; set; } + } + public class DestinationB : Destination + { + public string B { get; set; } + } + public class DestinationDetails + { + public string Description { get; set; } + public string Publisher { get; set; } + } + public class DestinationDetailsA + { + public string A { get; set; } + } + public class OtherDestinationDetails + { + public string Title { get; set; } + public string Author { get; set; } + } + public class Context : LocalDbContext + { + public DbSet Sources { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity(); + modelBuilder.Entity(); + } + } + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(Context context) + { + var sourceA = new SourceA + { + Name = "name1", + InnerSourcesA = { new InnerSourceA { InnerSourceDetailsA = { new InnerSourceDetailsA { A = "a" } } } }, + InnerSources = { new InnerSource { InnerSourceDetails = { new InnerSourceDetails { Description = "description", Publisher = "publisher" } } } }, + OtherInnerSources = { new OtherInnerSource { OtherInnerSourceDetails = { new OtherInnerSourceDetails { Title = "title", Author = "author" } } } } + }; + context.Sources.Add(sourceA); + var sourceB = new SourceB + { + Name = "name2", + B = "b", + InnerSources = { new InnerSource { InnerSourceDetails = { new InnerSourceDetails { Description = "description", Publisher = "publisher" } } } }, + OtherInnerSources = { new OtherInnerSource { OtherInnerSourceDetails = { new OtherInnerSourceDetails { Title = "title", Author = "author" } } } } + }; + context.Sources.Add(sourceB); + var source = new Source + { + Name = "name3", + InnerSources = { new InnerSource { InnerSourceDetails = { new InnerSourceDetails { Description = "description", Publisher = "publisher" } } } }, + OtherInnerSources = { new OtherInnerSource { OtherInnerSourceDetails = { new OtherInnerSourceDetails { Title = "title", Author = "author" } } } } + }; + context.Sources.Add(source); + base.Seed(context); + } + } + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap() + .ForMember(d => d.Details, o => o.MapFrom(s => s.InnerSources.Select(s => s).FirstOrDefault())) + .ForMember(d => d.OtherDetails, o => o.MapFrom(s => s.OtherInnerSources.Select(s => s).FirstOrDefault())) + .Include() + .Include(); + cfg.CreateMap() + .ForMember(d => d.DetailsA, o => o.MapFrom(s => s.InnerSourcesA.Select(s => s).FirstOrDefault())); + cfg.CreateMap(); + cfg.CreateMap().IncludeMembers(s => s.InnerSourceDetails.Select(s => s).FirstOrDefault()); + cfg.CreateMap().IncludeMembers(s => s.OtherInnerSourceDetails.Select(s => s).FirstOrDefault()); + cfg.CreateMap().IncludeMembers(s => s.InnerSourceDetailsA.Select(s => s).FirstOrDefault()); + cfg.CreateMap(); + cfg.CreateMap(); + cfg.CreateMap(); + }); + [Fact] + public void Should_flatten() + { + using (var context = Fixture.CreateContext()) + { + var projectTo = ProjectTo(context.Sources.OrderBy(p => p.Name)); + FirstOrDefaultCounter.Assert(projectTo, 33); + var list = projectTo.ToList(); + + var resultA = list[0].ShouldBeOfType(); + resultA.Name.ShouldBe("name1"); + resultA.Details.Description.ShouldBe("description"); + resultA.Details.Publisher.ShouldBe("publisher"); + resultA.OtherDetails.Title.ShouldBe("title"); + resultA.OtherDetails.Author.ShouldBe("author"); + resultA.DetailsA.A.ShouldBe("a"); + + var resultB = list[1].ShouldBeOfType(); + resultB.Name.ShouldBe("name2"); + resultB.Details.Description.ShouldBe("description"); + resultB.Details.Publisher.ShouldBe("publisher"); + resultB.OtherDetails.Title.ShouldBe("title"); + resultB.OtherDetails.Author.ShouldBe("author"); + resultB.B.ShouldBe("b"); + + var result = list[2].ShouldBeOfType(); + result.Name.ShouldBe("name3"); + result.Details.Description.ShouldBe("description"); + result.Details.Publisher.ShouldBe("publisher"); + result.OtherDetails.Title.ShouldBe("title"); + result.OtherDetails.Author.ShouldBe("author"); + } + } +} +public class SubqueryMapFromWithIncludeMembersSelectMemberFirstOrDefaultWithIheritance(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) +{ + public class Source + { + public int Id { get; set; } + public string Name { get; set; } + public List InnerSourceWrappers { get; set; } = new List(); + public List OtherInnerSources { get; set; } = new List(); + } + public class SourceA : Source + { + public List InnerSourceWrappersA { get; set; } = new List(); + } + public class SourceB : Source + { + public string B { get; set; } + } + public class InnerSourceWrapper + { + public int Id { get; set; } + public InnerSource InnerSource { get; set; } + } + public class InnerSourceWrapperA + { + public int Id { get; set; } + public InnerSourceA InnerSourceA { get; set; } + } + public class InnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public List InnerSourceDetailsWrapper { get; } = new List(); + } + public class InnerSourceA + { + public int Id { get; set; } + public string Name { get; set; } + public List InnerSourceDetailsWrapperA { get; } = new List(); + } + public class InnerSourceDetailsWrapper + { + public int Id { get; set; } + public InnerSourceDetails InnerSourceDetails { get; set; } + } + public class InnerSourceDetailsWrapperA + { + public int Id { get; set; } + public InnerSourceDetailsA InnerSourceDetailsA { get; set; } + } + public class InnerSourceDetails + { + public int Id { get; set; } + public string Description { get; set; } + public string Publisher { get; set; } + } + public class InnerSourceDetailsA + { + public int Id { get; set; } + public string A { get; set; } + } + public class OtherInnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public List OtherInnerSourceDetails { get; } = new List(); + } + public class OtherInnerSourceDetails + { + public int Id { get; set; } + public string Title { get; set; } + public string Author { get; set; } + } + public class Destination + { + public int Id { get; set; } + public string Name { get; set; } + public DestinationDetails Details { get; set; } + public OtherDestinationDetails OtherDetails { get; set; } + } + public class DestinationA : Destination + { + public DestinationDetailsA DetailsA { get; set; } + } + public class DestinationB : Destination + { + public string B { get; set; } + } + public class DestinationDetails + { + public string Description { get; set; } + public string Publisher { get; set; } + } + public class DestinationDetailsA + { + public string A { get; set; } + } + public class OtherDestinationDetails + { + public string Title { get; set; } + public string Author { get; set; } + } + public class Context : LocalDbContext + { + public DbSet Sources { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity(); + modelBuilder.Entity(); + } + } + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(Context context) + { + var sourceA = new SourceA + { + Name = "name1", + InnerSourceWrappersA = { new InnerSourceWrapperA { InnerSourceA = new InnerSourceA { InnerSourceDetailsWrapperA = { new InnerSourceDetailsWrapperA { InnerSourceDetailsA = new InnerSourceDetailsA { A = "a" } } } } } }, + InnerSourceWrappers = { new InnerSourceWrapper { InnerSource = new InnerSource { InnerSourceDetailsWrapper = { new InnerSourceDetailsWrapper { InnerSourceDetails = new InnerSourceDetails { Description = "description", Publisher = "publisher" } } } } } }, + OtherInnerSources = { new OtherInnerSource { OtherInnerSourceDetails = { new OtherInnerSourceDetails { Title = "title", Author = "author" } } } } + }; + context.Sources.Add(sourceA); + var sourceB = new SourceB + { + Name = "name2", + B = "b", + InnerSourceWrappers = { new InnerSourceWrapper { InnerSource = new InnerSource { InnerSourceDetailsWrapper = { new InnerSourceDetailsWrapper { InnerSourceDetails = new InnerSourceDetails { Description = "description", Publisher = "publisher" } } } } } }, + OtherInnerSources = { new OtherInnerSource { OtherInnerSourceDetails = { new OtherInnerSourceDetails { Title = "title", Author = "author" } } } } + }; + context.Sources.Add(sourceB); + var source = new Source + { + Name = "name3", + InnerSourceWrappers = { new InnerSourceWrapper { InnerSource = new InnerSource { InnerSourceDetailsWrapper = { new InnerSourceDetailsWrapper { InnerSourceDetails = new InnerSourceDetails { Description = "description", Publisher = "publisher" } } } } } }, + OtherInnerSources = { new OtherInnerSource { OtherInnerSourceDetails = { new OtherInnerSourceDetails { Title = "title", Author = "author" } } } } + }; + context.Sources.Add(source); + base.Seed(context); + } + } + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap() + .ForMember(d => d.Details, o => o.MapFrom(s => s.InnerSourceWrappers.Select(s => s.InnerSource).FirstOrDefault())) + .ForMember(d => d.OtherDetails, o => o.MapFrom(s => s.OtherInnerSources.Select(s => s).FirstOrDefault())) + .Include() + .Include(); + cfg.CreateMap() + .ForMember(d => d.DetailsA, o => o.MapFrom(s => s.InnerSourceWrappersA.Select(s => s.InnerSourceA).FirstOrDefault())); + cfg.CreateMap(); + cfg.CreateMap().IncludeMembers(s => s.InnerSourceDetailsWrapper.Select(s => s.InnerSourceDetails).FirstOrDefault()); + cfg.CreateMap().IncludeMembers(s => s.OtherInnerSourceDetails.Select(s => s).FirstOrDefault()); + cfg.CreateMap().IncludeMembers(s => s.InnerSourceDetailsWrapperA.Select(s => s.InnerSourceDetailsA).FirstOrDefault()); + cfg.CreateMap(); + cfg.CreateMap(); + cfg.CreateMap(); + }); + [Fact] + public void Should_flatten() + { + using (var context = Fixture.CreateContext()) + { + var projectTo = ProjectTo(context.Sources.OrderBy(p => p.Name)); + FirstOrDefaultCounter.Assert(projectTo, 33); + var list = projectTo.ToList(); + + var resultA = list[0].ShouldBeOfType(); + resultA.Name.ShouldBe("name1"); + resultA.Details.Description.ShouldBe("description"); + resultA.Details.Publisher.ShouldBe("publisher"); + resultA.OtherDetails.Title.ShouldBe("title"); + resultA.OtherDetails.Author.ShouldBe("author"); + resultA.DetailsA.A.ShouldBe("a"); + + var resultB = list[1].ShouldBeOfType(); + resultB.Name.ShouldBe("name2"); + resultB.Details.Description.ShouldBe("description"); + resultB.Details.Publisher.ShouldBe("publisher"); + resultB.OtherDetails.Title.ShouldBe("title"); + resultB.OtherDetails.Author.ShouldBe("author"); + resultB.B.ShouldBe("b"); + + var result = list[2].ShouldBeOfType(); + result.Name.ShouldBe("name3"); + result.Details.Description.ShouldBe("description"); + result.Details.Publisher.ShouldBe("publisher"); + result.OtherDetails.Title.ShouldBe("title"); + result.OtherDetails.Author.ShouldBe("author"); + } + } +} +public class IncludeMembersWithMapFromExpressionWithIheritance(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) +{ + public class Source + { + public int Id { get; set; } + public string Name { get; set; } + public InnerSource InnerSource { get; set; } + public OtherInnerSource OtherInnerSource { get; set; } + } + public class SourceA : Source + { + public InnerSourceA InnerSourceA { get; set; } + } + public class SourceB : Source + { + public string B { get; set; } + } + public class InnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public string Description1 { get; set; } + } + public class InnerSourceA + { + public int Id { get; set; } + public string A { get; set; } + } + public class OtherInnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public string Title1 { get; set; } + } + public class Destination + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public string Title { get; set; } + } + public class DestinationA : Destination + { + public string A { get; set; } + } + public class DestinationB : Destination + { + public string B { get; set; } + } + public class Context : LocalDbContext + { + public DbSet Sources { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity(); + modelBuilder.Entity(); + } + } + + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(Context context) + { + var sourceA = new SourceA { Name = "name1", InnerSourceA = new InnerSourceA { A = "a" }, InnerSource = new InnerSource { Description1 = "description" }, OtherInnerSource = new OtherInnerSource { Title1 = "title" } }; + context.Sources.Add(sourceA); + var sourceB = new SourceB { Name = "name2", B = "b", InnerSource = new InnerSource { Description1 = "description" }, OtherInnerSource = new OtherInnerSource { Title1 = "title" } }; + context.Sources.Add(sourceB); + var source = new Source { Name = "name3", InnerSource = new InnerSource { Description1 = "description" }, OtherInnerSource = new OtherInnerSource { Title1 = "title" } }; + context.Sources.Add(source); + base.Seed(context); + } + } + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap().IncludeMembers(s => s.InnerSource, s => s.OtherInnerSource) + .Include() + .Include(); + cfg.CreateMap().IncludeMembers(s => s.InnerSourceA); + cfg.CreateMap(); + cfg.CreateMap(MemberList.None).ForMember(d => d.Description, o => o.MapFrom(s => s.Description1)); + cfg.CreateMap(MemberList.None).ForMember(d => d.Title, o => o.MapFrom(s => s.Title1)); + cfg.CreateMap(MemberList.None).ForMember(d => d.A, o => o.MapFrom(s => s.A)); + }); + [Fact] + public void Should_flatten_with_MapFrom() + { + using (var context = Fixture.CreateContext()) + { + var list = ProjectTo(context.Sources.OrderBy(p => p.Name)).ToList(); + + var resultA = list[0].ShouldBeOfType(); + resultA.Name.ShouldBe("name1"); + resultA.Description.ShouldBe("description"); + resultA.Title.ShouldBe("title"); + resultA.A.ShouldBe("a"); + + var resultB = list[1].ShouldBeOfType(); + resultB.Name.ShouldBe("name2"); + resultB.Description.ShouldBe("description"); + resultB.Title.ShouldBe("title"); + resultB.B.ShouldBe("b"); + + var result = list[2].ShouldBeOfType(); + result.Name.ShouldBe("name3"); + result.Description.ShouldBe("description"); + result.Title.ShouldBe("title"); + } + } +} +public class IncludeMembersWithNullSubstituteWithIheritance(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) +{ + public class Source + { + public int Id { get; set; } + public string Name { get; set; } + public InnerSource InnerSource { get; set; } + public OtherInnerSource OtherInnerSource { get; set; } + } + public class SourceA : Source + { + public InnerSourceA InnerSourceA { get; set; } + } + public class SourceB : Source + { + public string B { get; set; } + } + public class InnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public int? Code { get; set; } + } + public class InnerSourceA + { + public int Id { get; set; } + public string A { get; set; } + } + public class OtherInnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public int? Code { get; set; } + public int? OtherCode { get; set; } + } + public class Destination + { + public int Id { get; set; } + public string Name { get; set; } + public int Code { get; set; } + public int OtherCode { get; set; } + } + public class DestinationA : Destination + { + public string A { get; set; } + } + public class DestinationB : Destination + { + public string B { get; set; } + } + public class Context : LocalDbContext + { + public DbSet Sources { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity(); + modelBuilder.Entity(); + } + } + + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(Context context) + { + var sourceA = new SourceA { Name = "name1" }; + context.Sources.Add(sourceA); + var sourceB = new SourceB { Name = "name2", B = "b", }; + context.Sources.Add(sourceB); + var source = new Source { Name = "name3" }; + context.Sources.Add(source); + base.Seed(context); + } + } + + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap().IncludeMembers(s => s.InnerSource, s => s.OtherInnerSource) + .Include() + .Include(); + cfg.CreateMap().IncludeMembers(s => s.InnerSourceA); + cfg.CreateMap(); + cfg.CreateProjection(MemberList.None).ForMember(d => d.Code, o => o.NullSubstitute(5)); + cfg.CreateProjection(MemberList.None).ForMember(d => d.OtherCode, o => o.NullSubstitute(7)); + cfg.CreateProjection(MemberList.None).ForMember(d => d.A, o => o.NullSubstitute("a")); + }); + [Fact] + public void Should_flatten() + { + using (var context = Fixture.CreateContext()) + { + var list = ProjectTo(context.Sources.OrderBy(p => p.Name)).ToList(); + + var resultA = list[0].ShouldBeOfType(); + resultA.Name.ShouldBe("name1"); + resultA.Code.ShouldBe(5); + resultA.OtherCode.ShouldBe(7); + resultA.A.ShouldBe("a"); + + var resultB = list[1].ShouldBeOfType(); + resultB.Name.ShouldBe("name2"); + resultB.Code.ShouldBe(5); + resultB.OtherCode.ShouldBe(7); + resultB.B.ShouldBe("b"); + + var result = list[2].ShouldBeOfType(); + result.Name.ShouldBe("name3"); + result.Code.ShouldBe(5); + result.OtherCode.ShouldBe(7); + } + } +} +public class IncludeMembersMembersFirstOrDefaultWithNullSubstituteWithIheritance(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) +{ + public class Source + { + public int Id { get; set; } + public string Name { get; set; } + public List InnerSources { get; set; } = new List(); + public List OtherInnerSources { get; set; } = new List(); + } + public class SourceA : Source + { + public List InnerSourcesA { get; set; } = new List(); + } + public class SourceB : Source + { + public string B { get; set; } + } + public class InnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public int? Code { get; set; } + } + public class InnerSourceA + { + public int Id { get; set; } + public string A { get; set; } + } + public class OtherInnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public int? Code { get; set; } + public int? OtherCode { get; set; } + } + public class Destination + { + public int Id { get; set; } + public string Name { get; set; } + public int Code { get; set; } + public int OtherCode { get; set; } + } + public class DestinationA : Destination + { + public string A { get; set; } + } + public class DestinationB : Destination + { + public string B { get; set; } + } + public class Context : LocalDbContext + { + public DbSet Sources { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity(); + modelBuilder.Entity(); + } + } + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(Context context) + { + var sourceA = new SourceA { Name = "name1" }; + context.Sources.Add(sourceA); + var sourceB = new SourceB { Name = "name2", B = "b", }; + context.Sources.Add(sourceB); + var source = new Source { Name = "name3" }; + context.Sources.Add(source); + base.Seed(context); + } + } + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap().IncludeMembers(s => s.InnerSources.FirstOrDefault(), s => s.OtherInnerSources.FirstOrDefault()) + .Include() + .Include(); + cfg.CreateMap().IncludeMembers(s => s.InnerSourcesA.FirstOrDefault()); + cfg.CreateMap(); + cfg.CreateProjection(MemberList.None).ForMember(d => d.Code, o => o.NullSubstitute(5)); + cfg.CreateProjection(MemberList.None).ForMember(d => d.OtherCode, o => o.NullSubstitute(7)); + cfg.CreateProjection(MemberList.None).ForMember(d => d.A, o => o.NullSubstitute("a")); + }); + [Fact] + public void Should_flatten() + { + using (var context = Fixture.CreateContext()) + { + var projectTo = ProjectTo(context.Sources.OrderBy(p => p.Name)); + FirstOrDefaultCounter.Assert(projectTo, 7); + var list = projectTo.ToList(); + + var resultA = list[0].ShouldBeOfType(); + resultA.Name.ShouldBe("name1"); + resultA.Code.ShouldBe(5); + resultA.OtherCode.ShouldBe(7); + resultA.A.ShouldBe("a"); + + var resultB = list[1].ShouldBeOfType(); + resultB.Name.ShouldBe("name2"); + resultB.Code.ShouldBe(5); + resultB.OtherCode.ShouldBe(7); + resultB.B.ShouldBe("b"); + + var result = list[2].ShouldBeOfType(); + result.Name.ShouldBe("name3"); + result.Code.ShouldBe(5); + result.OtherCode.ShouldBe(7); + } + } +} +public class CascadedIncludeMembersWithIheritance(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) +{ + public class Source + { + public int Id { get; set; } + public string Name { get; set; } + public Level1 FieldLevel1 { get; set; } + } + public class SourceA : Source + { + public Level1A FieldLevel1A { get; set; } + } + public class SourceB : Source + { + public string B { get; set; } + } + public class Level1 + { + public int Id { get; set; } + public Level2 FieldLevel2 { get; set; } + } + public class Level2 + { + public int Id { get; set; } + public long TheField { get; set; } + } + + public class Level1A + { + public int Id { get; set; } + public Level2A FieldLevel2A { get; set; } + } + public class Level2A + { + public int Id { get; set; } + public string A { get; set; } + } + public class Destination + { + public int Id { get; set; } + public string Name { get; set; } + public long TheField { get; set; } + } + public class DestinationA : Destination + { + public string A { get; set; } + } + public class DestinationB : Destination + { + public string B { get; set; } + } + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap().IncludeMembers(s => s.FieldLevel1) + .Include() + .Include(); + cfg.CreateMap().IncludeMembers(s => s.FieldLevel1A); + cfg.CreateMap(); + cfg.CreateMap(MemberList.None).IncludeMembers(s => s.FieldLevel2); + cfg.CreateMap(MemberList.None); + cfg.CreateMap(MemberList.None).IncludeMembers(s => s.FieldLevel2A); + cfg.CreateMap(MemberList.None); + }); + public class Context : LocalDbContext + { + public DbSet Sources { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity(); + modelBuilder.Entity(); + } + } + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(Context context) + { + var sourceA = new SourceA { Name = "name1", FieldLevel1A = new Level1A { FieldLevel2A = new Level2A { A = "a" } }, FieldLevel1 = new Level1 { FieldLevel2 = new Level2 { TheField = 2 } } }; + context.Sources.Add(sourceA); + var sourceB = new SourceB { Name = "name2", B = "b", FieldLevel1 = new Level1 { FieldLevel2 = new Level2 { TheField = 2 } } }; + context.Sources.Add(sourceB); + var source = new Source { Name = "name3", FieldLevel1 = new Level1 { FieldLevel2 = new Level2 { TheField = 2 } } }; + context.Sources.Add(source); + base.Seed(context); + } + } + [Fact] + public void Should_flatten() + { + using (var context = Fixture.CreateContext()) + { + var projectTo = ProjectTo(context.Sources.OrderBy(p => p.Name)); + var list = projectTo.ToList(); + + var resultA = list[0].ShouldBeOfType(); + resultA.Name.ShouldBe("name1"); + resultA.TheField.ShouldBe(2); + resultA.A.ShouldBe("a"); + + var resultB = list[1].ShouldBeOfType(); + resultB.Name.ShouldBe("name2"); + resultB.TheField.ShouldBe(2); + resultB.B.ShouldBe("b"); + + var result = list[2].ShouldBeOfType(); + result.Name.ShouldBe("name3"); + result.TheField.ShouldBe(2); + } + } +} +public class IncludeOnlySelectedMembersWithIheritance(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) +{ + public class Source + { + public int Id { get; set; } + public string Name { get; set; } + public InnerSource InnerSource { get; set; } + public OtherInnerSource OtherInnerSource { get; set; } + public int ImNotIncluded { get; set; } + } + public class SourceA : Source + { + public string A { get; set; } + public int ImNotIncludedA { get; set; } + public List InnerSourcesA { get; set; } = new List(); + } + + public class InnerSourceA + { + public int Id { get; set; } + public int ImNotIncludedA { get; set; } + public string IAmIncluded { get; set; } + } + public class SourceB : Source + { + public string B { get; set; } + public int ImNotIncludedB { get; set; } + } + public class InnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + } + public class OtherInnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public string Title { get; set; } + } + public class Destination + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + } + public class DestinationA : Destination + { + public string A { get; set; } + public string IAmIncluded { get; set; } + } + public class DestinationB : Destination + { + public string B { get; set; } + } + public class Context : LocalDbContext + { + public DbSet Sources { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity(); + modelBuilder.Entity(); + } + } + + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(Context context) + { + var sourceA = new SourceA { Name = "name1", A = "a", InnerSource = new InnerSource { Description = "description" }, OtherInnerSource = new OtherInnerSource { Title = "title" }, InnerSourcesA = { new InnerSourceA { IAmIncluded = "a" } } }; + context.Sources.Add(sourceA); + var sourceB = new SourceB { Name = "name2", B = "b", InnerSource = new InnerSource { Description = "description" }, OtherInnerSource = new OtherInnerSource { Title = "title" } }; + context.Sources.Add(sourceB); + var sourceC = new Source { Name = "name3", InnerSource = new InnerSource { Description = "description" }, OtherInnerSource = new OtherInnerSource { Title = "title" } }; + context.Sources.Add(sourceC); + base.Seed(context); + } + } + + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap().IncludeMembers(s => s.InnerSource, s => s.OtherInnerSource) + .Include() + .Include(); + cfg.CreateMap().IncludeMembers(s => s.InnerSourcesA.FirstOrDefault()); + cfg.CreateMap(MemberList.None); + cfg.CreateMap(); + cfg.CreateProjection(MemberList.None); + cfg.CreateProjection(MemberList.None); + }); + [Fact] + public void Should_flatten() + { + using (var context = Fixture.CreateContext()) + { + var projectTo = ProjectTo(context.Sources.OrderBy(p => p.Name)); + var query = projectTo.ToQueryString(); + query.ShouldNotContain(nameof(Source.ImNotIncluded)); + + var list = projectTo.ToList(); + var resultA = list[0].ShouldBeOfType(); + resultA.Name.ShouldBe("name1"); + resultA.Description.ShouldBe("description"); + resultA.A.ShouldBe("a"); + resultA.IAmIncluded.ShouldBe("a"); + + var resultB = list[1].ShouldBeOfType(); + resultB.Name.ShouldBe("name2"); + resultB.Description.ShouldBe("description"); + resultB.B.ShouldBe("b"); + + var resultC = list[2].ShouldBeOfType(); + resultC.Name.ShouldBe("name3"); + resultC.Description.ShouldBe("description"); + } + } +} + +public class IncludeMultipleExpressionsWithIheritance(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) +{ + public class Source + { + public int Id { get; set; } + public string Name { get; set; } + public InnerSource InnerSource { get; set; } + public OtherInnerSource OtherInnerSource { get; set; } + public int ImNotIncluded { get; set; } + public List InnerSourcesA { get; set; } = new List(); + } + public class SourceA : Source + { + public List InnerSourcesAFallback { get; set; } = new List(); + } + + public class InnerSourceA + { + public int Id { get; set; } + public int ImNotIncludedA { get; set; } + public string IAmIncluded { get; set; } + } + public class SourceB : Source + { + public string B { get; set; } + public int ImNotIncludedB { get; set; } + } + public class InnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + } + public class OtherInnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public string Title { get; set; } + } + public class Destination + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + } + public class DestinationA : Destination + { + public string IAmIncluded { get; set; } + } + public class DestinationB : Destination + { + public string B { get; set; } + } + public class Context : LocalDbContext + { + public DbSet Sources { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity(); + modelBuilder.Entity(); + } + } + + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(Context context) + { + var sourceA = new SourceA { Name = "name1", InnerSource = new InnerSource { Description = "description" }, OtherInnerSource = new OtherInnerSource { Title = "title" }, InnerSourcesAFallback = { new InnerSourceA { IAmIncluded = "fallback" } } }; + context.Sources.Add(sourceA); + var sourceB = new SourceB { Name = "name2", B = "b", InnerSource = new InnerSource { Description = "description" }, OtherInnerSource = new OtherInnerSource { Title = "title" } }; + context.Sources.Add(sourceB); + var sourceC = new Source { Name = "name3", InnerSource = new InnerSource { Description = "description" }, OtherInnerSource = new OtherInnerSource { Title = "title" } }; + context.Sources.Add(sourceC); + base.Seed(context); + } + } + + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap().IncludeMembers(s => s.InnerSource, s => s.OtherInnerSource) + .Include() + .Include(); + cfg.CreateMap().IncludeMembers(s => s.InnerSourcesA.FirstOrDefault() ?? s.InnerSourcesAFallback.FirstOrDefault()); + cfg.CreateMap(MemberList.None); + cfg.CreateMap(); + cfg.CreateProjection(MemberList.None); + cfg.CreateProjection(MemberList.None); + }); + [Fact] + public void Should_flatten() + { + using (var context = Fixture.CreateContext()) + { + var projectTo = ProjectTo(context.Sources.OrderBy(p => p.Name)); + var list = projectTo.ToList(); + var resultA = list[0].ShouldBeOfType(); + resultA.Name.ShouldBe("name1"); + resultA.Description.ShouldBe("description"); + resultA.IAmIncluded.ShouldBe("fallback"); + + var resultB = list[1].ShouldBeOfType(); + resultB.Name.ShouldBe("name2"); + resultB.Description.ShouldBe("description"); + resultB.B.ShouldBe("b"); + + var resultC = list[2].ShouldBeOfType(); + resultC.Name.ShouldBe("name3"); + resultC.Description.ShouldBe("description"); + } + } } \ No newline at end of file diff --git a/src/IntegrationTests/Inheritance/DerivedComplexTypes.cs b/src/IntegrationTests/Inheritance/DerivedComplexTypes.cs index 6997e1f153..ce3c2820c3 100644 --- a/src/IntegrationTests/Inheritance/DerivedComplexTypes.cs +++ b/src/IntegrationTests/Inheritance/DerivedComplexTypes.cs @@ -1,6 +1,6 @@ namespace AutoMapper.IntegrationTests.Inheritance; -public class DerivedComplexTypes : IntegrationTest +public class DerivedComplexTypes(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { [ComplexType] public class LocalizedString @@ -67,7 +67,7 @@ protected override void Seed(Context context) [Fact] public void Can_map_with_projection() { - using (var context = new Context()) + using (var context = Fixture.CreateContext()) { var customerVm = ProjectTo(context.Customers).First(); customerVm.Address.ShouldBe("home"); diff --git a/src/IntegrationTests/Inheritance/OverrideDestinationMappingsTest.cs b/src/IntegrationTests/Inheritance/OverrideDestinationMappingsTest.cs index 7c632c7015..0701ae9d95 100644 --- a/src/IntegrationTests/Inheritance/OverrideDestinationMappingsTest.cs +++ b/src/IntegrationTests/Inheritance/OverrideDestinationMappingsTest.cs @@ -1,6 +1,6 @@ namespace AutoMapper.IntegrationTests.Inheritance; -public class OverrideDestinationMappingsTest : IntegrationTest +public class OverrideDestinationMappingsTest(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { public class Context : LocalDbContext { @@ -31,9 +31,9 @@ public void Map_WhenOverrideDestinationTypeAndSourceIsDerived_MustCreateOverridd model.Child.ShouldBeOfType(); } - private static Entity LoadEntity() + private Entity LoadEntity() { - using(var context = new Context()) + using(var context = Fixture.CreateContext()) { return context.Entity.Include(e => e.Child).First(); } diff --git a/src/IntegrationTests/Inheritance/PolymorphismTests.cs b/src/IntegrationTests/Inheritance/PolymorphismTests.cs new file mode 100644 index 0000000000..d6de06a6be --- /dev/null +++ b/src/IntegrationTests/Inheritance/PolymorphismTests.cs @@ -0,0 +1,104 @@ +namespace AutoMapper.IntegrationTests.Inheritance; + +public class PolymorphismTests(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) +{ + public abstract class Vehicle + { + public int Id { get; set; } + public string Name { get; set; } + } + + public class Car : Vehicle + { + public int AmountDoors { get; set; } + } + + public class Motorcycle : Vehicle + { + public bool HasSidecar { get; set; } + } + + public class Bicycle : Vehicle + { + public bool EBike { get; set; } + } + + public class VehicleModel + { + public string Name { get; set; } + } + + public class MotorcycleModel : VehicleModel + { + public bool HasSidecar { get; set; } + } + + public class BicycleModel : VehicleModel + { + public bool EBike { get; set; } + } + + public class Context : LocalDbContext + { + public DbSet Vehicles { get; set; } + + public DbSet Cars { get; set; } + + public DbSet Motorcycles { get; set; } + + public DbSet Bicycles { get; set; } + } + + protected override MapperConfiguration CreateConfiguration() + { + return new MapperConfiguration(cfg => + { + cfg.CreateMap() + .IncludeAllDerived(); + + cfg.CreateMap(); + cfg.CreateMap(); + }); + } + + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(Context context) + { + context.Vehicles.Add(new Car { Name = "Car", AmountDoors = 4 }); + context.Vehicles.Add(new Bicycle { Name = "Bicycle", EBike = true }); + context.Vehicles.Add(new Motorcycle { Name = "Motorcycle", HasSidecar = true }); + + base.Seed(context); + } + } + + [Fact] + public void Should_project_base_queryable_to_derived_models_polymorphic() + { + using var context = Fixture.CreateContext(); + var results = context.Vehicles.ProjectTo(Configuration).ToArray(); + results.Length.ShouldBe(3); + results.ShouldContain(x => x.GetType() == typeof(VehicleModel), 1); + results.ShouldContain(x => x.GetType() == typeof(BicycleModel), 1); + results.ShouldContain(x => x.GetType() == typeof(MotorcycleModel), 1); + } + + [Fact] + public void Should_project_derived_queryable_to_derived_models_if_derived_models_exist() + { + using var context = Fixture.CreateContext(); + var results = context.Motorcycles.ProjectTo(Configuration).ToArray(); + results.Length.ShouldBe(1); + results.ShouldContain(x => x.GetType() == typeof(MotorcycleModel), 1); + } + + [Fact] + public void Should_project_derived_queryable_to_base_models_if_no_derived_models_exist() + { + using var context = Fixture.CreateContext(); + var results = context.Cars.ProjectTo(Configuration).ToArray(); + results.Length.ShouldBe(1); + results.ShouldContain(x => x.GetType() == typeof(VehicleModel), 1); + } +} \ No newline at end of file diff --git a/src/IntegrationTests/Inheritance/ProjectToAbstractType.cs b/src/IntegrationTests/Inheritance/ProjectToAbstractType.cs index ed85ddcee6..e97701f7d5 100644 --- a/src/IntegrationTests/Inheritance/ProjectToAbstractType.cs +++ b/src/IntegrationTests/Inheritance/ProjectToAbstractType.cs @@ -2,7 +2,7 @@ namespace AutoMapper.IntegrationTests.Inheritance; -public class ProjectToAbstractType : IntegrationTest +public class ProjectToAbstractType(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { ITypeA[] _destinations; @@ -51,7 +51,7 @@ public class Context : LocalDbContext [Fact] public void Should_project_to_abstract_type() { - using(var context = new Context()) + using(var context = Fixture.CreateContext()) { _destinations = ProjectTo(context.EntityA).ToArray(); } @@ -60,7 +60,7 @@ public void Should_project_to_abstract_type() } } -public class ProjectToInterface : IntegrationTest +public class ProjectToInterface(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { //Data Objects public class DataLayer @@ -303,7 +303,7 @@ public class Context : LocalDbContext [Fact] public void Should_project_to_abstract_type() { - using(var context = new Context()) + using(var context = Fixture.CreateContext()) { var domainCalendars = ProjectTo(context.Calendars).ToList(); domainCalendars.Count.ShouldBe(2); diff --git a/src/IntegrationTests/Inheritance/ProjectToAbstractTypeWithInheritance.cs b/src/IntegrationTests/Inheritance/ProjectToAbstractTypeWithInheritance.cs new file mode 100644 index 0000000000..935b156bf2 --- /dev/null +++ b/src/IntegrationTests/Inheritance/ProjectToAbstractTypeWithInheritance.cs @@ -0,0 +1,143 @@ +namespace AutoMapper.IntegrationTests.Inheritance; + +public class ProjectToAbstractTypeWithInheritance(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) +{ + public class StepGroup + { + public int Id { get; set; } + public string Name { get; set; } + public virtual List Steps { get; set; } = new(); + } + public abstract class Step + { + public int Id { get; set; } + public string Name { get; set; } + public int StepGroupId { get; set; } + public virtual StepGroup StepGroup { get; set; } + public virtual ICollection StepInputs { get; set; } = new HashSet(); + } + public class CheckingStep : Step { } + public class InstructionStep : Step { } + public abstract class AbstractStep : Step { } + public class StepInput + { + public int Id { get; set; } + public int StepId { get; set; } + public string Input { get; set; } + public virtual Step Step { get; set; } + } + public class StepGroupModel + { + public int Id { get; set; } + public string Name { get; set; } + public List Steps { get; set; } = new(); + } + public abstract class StepModel + { + public int Id { get; set; } + public string Name { get; set; } + public ICollection StepInputs { get; set; } = new HashSet(); + } + public class CheckingStepModel : StepModel { } + public class InstructionStepModel : StepModel { } + public abstract class AbstractStepModel : StepModel { } + public class StepInputModel + { + public int Id { get; set; } + public int StepId { get; set; } + public string Input { get; set; } + public StepModel Step { get; set; } + } + + public class Context : LocalDbContext + { + public DbSet StepGroups { get; set; } + + public DbSet Steps { get; set; } + + public DbSet StepInputs { get; set; } + + public DbSet CheckingSteps { get; set; } + + public DbSet InstructionSteps { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(entity => + { + entity.HasOne(d => d.StepGroup).WithMany(p => p.Steps) + .HasForeignKey(d => d.StepGroupId); + }); + + modelBuilder.Entity(entity => + { + entity.HasOne(d => d.Step).WithMany(p => p.StepInputs) + .HasForeignKey(d => d.StepId); + }); + } + } + + protected override MapperConfiguration CreateConfiguration() + { + return new MapperConfiguration(cfg => + { + cfg.CreateMap(); + cfg.CreateMap(); + cfg.CreateMap() + .IncludeBase(); + cfg.CreateMap() + .IncludeBase(); + cfg.CreateMap() + .IncludeBase(); + cfg.CreateMap(); + }); + } + + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(Context context) + { + context.StepGroups.Add(new StepGroup + { + Name = "StepGroup", + Steps = new List + { + new InstructionStep + { + Name = "InstructionStep", + StepInputs = new List + { + new StepInput + { + Input = "Input" + } + } + }, + new CheckingStep + { + Name = "CheckingStep" + } + } + }); + + base.Seed(context); + } + } + + [Fact] + public void ProjectCollectionWithElementInheritingAbstractClass() + { + using var context = Fixture.CreateContext(); + var steps = ProjectTo(context.StepGroups).Single().Steps; + steps[0].ShouldBeOfType().Name.ShouldBe("CheckingStep"); + steps[1].ShouldBeOfType().Name.ShouldBe("InstructionStep"); + } + + [Fact] + public void ProjectIncludingPolymorphicElement() + { + using var context = Fixture.CreateContext(); + var stepInput = ProjectTo(context.StepInputs).Single(); + stepInput.Step.ShouldBeOfType().Name.ShouldBe("InstructionStep"); + } +} \ No newline at end of file diff --git a/src/IntegrationTests/Inheritance/ProxyTests.cs b/src/IntegrationTests/Inheritance/ProxyTests.cs index 706a60340e..cd1ea89c38 100644 --- a/src/IntegrationTests/Inheritance/ProxyTests.cs +++ b/src/IntegrationTests/Inheritance/ProxyTests.cs @@ -1,6 +1,6 @@ namespace AutoMapper.IntegrationTests.Inheritance; -public class ProxyTests : IAsyncLifetime +public class ProxyTests(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { [Fact] public void Test() @@ -12,13 +12,13 @@ public void Test() }); config.AssertConfigurationIsValid(); - var context = new ClientContext(); + var context = Fixture.CreateContext(); var course = context.TrainingCourses.FirstOrDefault(n => n.CourseName == "Course 1"); var mapper = config.CreateMapper(); var dto = mapper.Map(course); } - class DatabaseInitializer : DropCreateDatabaseAlways + public class DatabaseInitializer : DropCreateDatabaseAlways { protected override void Seed(ClientContext context) { @@ -30,7 +30,7 @@ protected override void Seed(ClientContext context) } } - class ClientContext : LocalDbContext + public class ClientContext : LocalDbContext { public ClientContext() { @@ -102,13 +102,4 @@ public class TrainingContentDto // public int CourseId { get; set; } } - - public async Task InitializeAsync() - { - var initializer = new DatabaseInitializer(); - - await initializer.Migrate(); - } - - public Task DisposeAsync() => Task.CompletedTask; } \ No newline at end of file diff --git a/src/IntegrationTests/Inheritance/QueryableInterfaceInheritanceIssue.cs b/src/IntegrationTests/Inheritance/QueryableInterfaceInheritanceIssue.cs index bc76a5215b..d7384b5af4 100644 --- a/src/IntegrationTests/Inheritance/QueryableInterfaceInheritanceIssue.cs +++ b/src/IntegrationTests/Inheritance/QueryableInterfaceInheritanceIssue.cs @@ -1,6 +1,6 @@ namespace AutoMapper.IntegrationTests.Inheritance; -public class QueryableInterfaceInheritanceIssue : IntegrationTest +public class QueryableInterfaceInheritanceIssue(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { QueryableDto[] _result; @@ -39,7 +39,7 @@ public class ClientContext : LocalDbContext [Fact] public void QueryableShouldMapSpecifiedBaseInterfaceMember() { - using (var context = new ClientContext()) + using (var context = Fixture.CreateContext()) { _result = ProjectTo(context.Entities).ToArray(); } diff --git a/src/IntegrationTests/IntegrationTest.cs b/src/IntegrationTests/IntegrationTest.cs index 2739c3c515..9885c18c26 100644 --- a/src/IntegrationTests/IntegrationTest.cs +++ b/src/IntegrationTests/IntegrationTest.cs @@ -1,26 +1,108 @@ -namespace AutoMapper.IntegrationTests; +using Microsoft.Data.SqlClient; +using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; +using Microsoft.EntityFrameworkCore.Scaffolding.Metadata; +using Testcontainers.MsSql; -public abstract class IntegrationTest : AutoMapperSpecBase, IAsyncLifetime where TInitializer : IInitializer, new() +namespace AutoMapper.IntegrationTests; + +[CollectionDefinition(nameof(DatabaseFixture))] +public class DatabaseCollection : ICollectionFixture { } + +public class DatabaseFixture : IAsyncLifetime { + private MsSqlContainer _msSqlContainer; + + async Task IAsyncLifetime.DisposeAsync() + { + await _msSqlContainer.DisposeAsync(); + } + + public string GetConnectionString() => _msSqlContainer.GetConnectionString(); + + async Task IAsyncLifetime.InitializeAsync() + { + _msSqlContainer = new MsSqlBuilder().Build(); + + await _msSqlContainer.StartAsync(); + } +} + +[Collection(nameof(DatabaseFixture))] +public abstract class IntegrationTest : AutoMapperSpecBase, IAsyncLifetime + where TDbContextFixture : IDbContextFixture, new() +{ + private readonly DatabaseFixture _databaseFixture; + + protected IntegrationTest(DatabaseFixture databaseFixture) + { + _databaseFixture = databaseFixture; + } + + protected TDbContextFixture Fixture { get; private set; } + Task IAsyncLifetime.DisposeAsync() => Task.CompletedTask; - Task IAsyncLifetime.InitializeAsync() => new TInitializer().Migrate(); + + async Task IAsyncLifetime.InitializeAsync() + { + var connectionString = _databaseFixture.GetConnectionString(); + + var builder = new SqlConnectionStringBuilder(connectionString) + { + InitialCatalog = GetType().FullName + }; + + Fixture = new TDbContextFixture + { + ConnectionString = builder.ToString() + }; + + await Fixture.Migrate(); + } } -public interface IInitializer + +public interface IDbContextFixture { Task Migrate(); + string ConnectionString { get; set; } } -public class DropCreateDatabaseAlways : IInitializer where TContext : DbContext, new() + +public class DropCreateDatabaseAlways : IDbContextFixture where TContext : LocalDbContext, new() { + public string ConnectionString { get; set; } + protected virtual void Seed(TContext context){} + public async Task Migrate() { - await using var context = new TContext(); - - await context.Database.EnsureDeletedAsync(); - await context.Database.EnsureCreatedAsync(); + await using var context = new TContext + { + ConnectionString = ConnectionString + }; + + var database = context.Database; + await database.EnsureDeletedAsync(); + var strategy = database.CreateExecutionStrategy(); + await strategy.ExecuteAsync(async () => await database.EnsureCreatedAsync()); Seed(context); await context.SaveChangesAsync(); } + + public TContext CreateContext() + { + return new TContext + { + ConnectionString = ConnectionString + }; + } +} + +public abstract class LocalDbContext : DbContext +{ + public string ConnectionString { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder.UseSqlServer( + ConnectionString, + o => o.EnableRetryOnFailure(maxRetryCount: 10).CommandTimeout(120)); } \ No newline at end of file diff --git a/src/IntegrationTests/LocalDbContext.cs b/src/IntegrationTests/LocalDbContext.cs deleted file mode 100644 index 66fb48d50b..0000000000 --- a/src/IntegrationTests/LocalDbContext.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace AutoMapper.IntegrationTests; - -public abstract class LocalDbContext : DbContext -{ - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder.UseSqlServer( - @$"Data Source=(localdb)\mssqllocaldb;Integrated Security=True;MultipleActiveResultSets=True;Database={GetType()};Connection Timeout=300", - o=>o.EnableRetryOnFailure()); -} \ No newline at end of file diff --git a/src/IntegrationTests/MaxDepth/MaxDepthWithCollections.cs b/src/IntegrationTests/MaxDepth/MaxDepthWithCollections.cs index 33539009d6..a44fa90e61 100644 --- a/src/IntegrationTests/MaxDepth/MaxDepthWithCollections.cs +++ b/src/IntegrationTests/MaxDepth/MaxDepthWithCollections.cs @@ -1,6 +1,6 @@ namespace AutoMapper.IntegrationTests.MaxDepth; -public class MaxDepthWithCollections : IntegrationTest +public class MaxDepthWithCollections(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { TrainingCourseDto _course; @@ -14,7 +14,7 @@ public class MaxDepthWithCollections : IntegrationTest(context.TrainingCourses).FirstOrDefault(n => n.CourseName == "Course 1"); } diff --git a/src/IntegrationTests/MaxDepth/NavigationPropertySO.cs b/src/IntegrationTests/MaxDepth/NavigationPropertySO.cs index b04f534fdd..b248775041 100644 --- a/src/IntegrationTests/MaxDepth/NavigationPropertySO.cs +++ b/src/IntegrationTests/MaxDepth/NavigationPropertySO.cs @@ -1,6 +1,6 @@ namespace AutoMapper.IntegrationTests.MaxDepth; -public class NavigationPropertySO : IntegrationTest +public class NavigationPropertySO(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { CustomerDTO _destination; @@ -79,7 +79,7 @@ protected override void Seed(Context context) [Fact] public void Can_map_with_projection() { - using(var context = new Context()) + using(var context = Fixture.CreateContext()) { _destination = ProjectTo(context.Customers).Single(); _destination.Id.ShouldBe(1); diff --git a/src/IntegrationTests/MaxDepth/NestedDtos.cs b/src/IntegrationTests/MaxDepth/NestedDtos.cs index 09ea40301a..4c98f55aff 100644 --- a/src/IntegrationTests/MaxDepth/NestedDtos.cs +++ b/src/IntegrationTests/MaxDepth/NestedDtos.cs @@ -1,6 +1,6 @@ namespace AutoMapper.IntegrationTests.MaxDepth; -public class NestedDtos : IntegrationTest +public class NestedDtos(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { ArtDto _destination; @@ -66,7 +66,7 @@ protected override void Seed(TestContext context) [Fact] public void Should_project_nested_dto() { - using (var context = new TestContext()) + using (var context = Fixture.CreateContext()) { _destination = ProjectTo(context.Arts).FirstOrDefault(); } diff --git a/src/IntegrationTests/NullCheckCollections.cs b/src/IntegrationTests/NullCheckCollections.cs index 9ca40cabae..9155b9428c 100644 --- a/src/IntegrationTests/NullCheckCollections.cs +++ b/src/IntegrationTests/NullCheckCollections.cs @@ -1,5 +1,5 @@ namespace AutoMapper.IntegrationTests; -public class NullCheckCollectionsFirstOrDefault : IntegrationTest +public class NullCheckCollectionsFirstOrDefault(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { public class SourceType { @@ -29,13 +29,13 @@ public class TestContext : LocalDbContext [Fact] public void Should_project_ok() { - using (var context = new TestContext()) + using (var context = Fixture.CreateContext()) { ProjectTo(context.SourceTypes).Single().Index.ShouldBe(101); } } } -public class NullChildItemTest : IntegrationTest +public class NullChildItemTest(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { protected override MapperConfiguration CreateConfiguration() => new(cfg => cfg.CreateProjection()); public class TestContext : LocalDbContext @@ -53,7 +53,7 @@ protected override void Seed(TestContext testContext) [Fact] public void Should_project_null_value() { - using (var context = new TestContext()) + using (var context = Fixture.CreateContext()) { var query = ProjectTo(context.Parents); var projected = query.Single(); @@ -89,7 +89,7 @@ public class GrandChild public int Value { get; set; } } } -public class NullCheckCollections : IntegrationTest +public class NullCheckCollections(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { public class Student { @@ -143,7 +143,7 @@ protected override void Seed(Context context) [Fact] public void Can_map_with_projection() { - using (var context = new Context()) + using (var context = Fixture.CreateContext()) { ProjectTo(context.Students).Single().Name.ShouldBe("Bob"); } diff --git a/src/IntegrationTests/NullSubstitute.cs b/src/IntegrationTests/NullSubstitute.cs index 747f625d2a..5ac5de61f5 100644 --- a/src/IntegrationTests/NullSubstitute.cs +++ b/src/IntegrationTests/NullSubstitute.cs @@ -1,6 +1,6 @@ namespace AutoMapper.IntegrationTests; -public class NullSubstitute : IntegrationTest +public class NullSubstitute(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { public class Customer { @@ -46,13 +46,13 @@ protected override void Seed(Context context) [Fact] public void Can_map_with_projection() { - using (var context = new Context()) + using (var context = Fixture.CreateContext()) { ProjectTo(context.Customers).First().Value.ShouldBe(5); } } } -public class NullSubstituteWithStrings : IntegrationTest +public class NullSubstituteWithStrings(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { public class Customer { @@ -84,13 +84,13 @@ protected override void Seed(Context context) [Fact] public void Can_map_with_projection() { - using (var context = new Context()) + using (var context = Fixture.CreateContext()) { ProjectTo(context.Customers).First().Value.ShouldBe("5"); } } } -public class NullSubstituteWithEntity : IntegrationTest +public class NullSubstituteWithEntity(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { public class Customer { @@ -133,7 +133,7 @@ protected override void Seed(Context context) [Fact] public void Can_map_with_projection() { - using (var context = new Context()) + using (var context = Fixture.CreateContext()) { ProjectTo(context.Customers).First().Value.ShouldBeNull(); } diff --git a/src/IntegrationTests/ParameterizedQueries.cs b/src/IntegrationTests/ParameterizedQueries.cs index 35d6deffb5..ca5aa3b4b7 100644 --- a/src/IntegrationTests/ParameterizedQueries.cs +++ b/src/IntegrationTests/ParameterizedQueries.cs @@ -1,6 +1,6 @@ namespace AutoMapper.IntegrationTests; -public class ParameterizedQueries : IntegrationTest +public class ParameterizedQueries(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { public class Entity { @@ -45,7 +45,7 @@ public async Task Should_parameterize_value() { List dtos; string username; - using (var db = new ClientContext()) + using (var db = Fixture.CreateContext()) { username = "Mary"; var query = ProjectTo(db.Entities, new { username }); diff --git a/src/IntegrationTests/ProjectionAdvanced.cs b/src/IntegrationTests/ProjectionAdvanced.cs index 1d66f33403..c831f90d45 100644 --- a/src/IntegrationTests/ProjectionAdvanced.cs +++ b/src/IntegrationTests/ProjectionAdvanced.cs @@ -1,11 +1,11 @@ namespace AutoMapper.IntegrationTests; -public class ProjectionAdvanced : IntegrationTest +public class ProjectionAdvanced(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { protected override MapperConfiguration CreateConfiguration() => new(c => c.CreateProjection().Advanced().ForAllMembers(o=>o.Ignore())); [Fact] public void Should_work() { - using var context = new Context(); + using var context = Fixture.CreateContext(); var dto = ProjectTo(context.Entities).Single(); dto.Id.ShouldBe(0); dto.Name.ShouldBeNull(); diff --git a/src/IntegrationTests/ProjectionOrderTest.cs b/src/IntegrationTests/ProjectionOrderTest.cs index 82dd1cb951..7643cbc71e 100644 --- a/src/IntegrationTests/ProjectionOrderTest.cs +++ b/src/IntegrationTests/ProjectionOrderTest.cs @@ -1,6 +1,6 @@ namespace AutoMapper.IntegrationTests; -public class ProjectionOrderTest : IntegrationTest +public class ProjectionOrderTest(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { public class Destination { @@ -56,7 +56,7 @@ public class ClientContext : LocalDbContext [Fact] public void Should_Not_Throw_NotSupportedException_On_Union() { - using (var context = new ClientContext()) + using (var context = Fixture.CreateContext()) { ProjectTo(context.Source1).Union(ProjectTo(context.Source2)).ToString(); } diff --git a/src/IntegrationTests/ValueTransformerTests.cs b/src/IntegrationTests/ValueTransformerTests.cs index 74409fc82a..f1e1d741ca 100644 --- a/src/IntegrationTests/ValueTransformerTests.cs +++ b/src/IntegrationTests/ValueTransformerTests.cs @@ -2,7 +2,7 @@ { namespace ValueTransformerTests { - public class BasicTransforming : IntegrationTest + public class BasicTransforming(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { public class Source { @@ -40,7 +40,7 @@ protected override void Seed(Context context) [Fact] public async Task Should_transform_value() { - using (var context = new Context()) + using (var context = Fixture.CreateContext()) { var dest = await ProjectTo(context.Sources).SingleAsync(); @@ -49,7 +49,7 @@ public async Task Should_transform_value() } } - public class StackingTransformers : IntegrationTest + public class StackingTransformers(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { public class Source { @@ -88,7 +88,7 @@ protected override void Seed(Context context) [Fact] public async Task Should_stack_transformers_in_order() { - using (var context = new Context()) + using (var context = Fixture.CreateContext()) { var dest = await ProjectTo(context.Sources).SingleAsync(); @@ -97,7 +97,7 @@ public async Task Should_stack_transformers_in_order() } } - public class DifferentProfiles : IntegrationTest + public class DifferentProfiles(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { public class Source { @@ -136,7 +136,7 @@ protected override void Seed(Context context) [Fact] public async Task Should_not_apply_other_transform() { - using (var context = new Context()) + using (var context = Fixture.CreateContext()) { var dest = await ProjectTo(context.Sources).SingleAsync(); @@ -145,7 +145,7 @@ public async Task Should_not_apply_other_transform() } } - public class StackingRootConfigAndProfileTransform : IntegrationTest + public class StackingRootConfigAndProfileTransform(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { public class Source { @@ -186,7 +186,7 @@ protected override void Seed(Context context) [Fact] public async Task ShouldApplyProfileFirstThenRoot() { - using (var context = new Context()) + using (var context = Fixture.CreateContext()) { var dest = await ProjectTo(context.Sources).SingleAsync(); @@ -195,7 +195,7 @@ public async Task ShouldApplyProfileFirstThenRoot() } } - public class TransformingValueTypes : IntegrationTest + public class TransformingValueTypes(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { public class Source { @@ -236,7 +236,7 @@ protected override void Seed(Context context) [Fact] public async Task ShouldApplyProfileFirstThenRoot() { - using (var context = new Context()) + using (var context = Fixture.CreateContext()) { var dest = await ProjectTo(context.Sources).SingleAsync(); @@ -245,7 +245,7 @@ public async Task ShouldApplyProfileFirstThenRoot() } } - public class StackingRootAndProfileAndMemberConfig : IntegrationTest + public class StackingRootAndProfileAndMemberConfig(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { public class Source { @@ -288,7 +288,7 @@ protected override void Seed(Context context) [Fact] public async Task ShouldApplyTypeMapThenProfileThenRoot() { - using (var context = new Context()) + using (var context = Fixture.CreateContext()) { var dest = await ProjectTo(context.Sources).SingleAsync(); @@ -297,7 +297,7 @@ public async Task ShouldApplyTypeMapThenProfileThenRoot() } } - public class StackingTypeMapAndRootAndProfileAndMemberConfig : IntegrationTest + public class StackingTypeMapAndRootAndProfileAndMemberConfig(DatabaseFixture databaseFixture) : IntegrationTest(databaseFixture) { public class Source { @@ -340,7 +340,7 @@ protected override void Seed(Context context) [Fact] public async Task ShouldApplyTypeMapThenProfileThenRoot() { - using (var context = new Context()) + using (var context = Fixture.CreateContext()) { var dest = await ProjectTo(context.Sources).SingleAsync(); diff --git a/src/TestApp/Program.cs b/src/TestApp/Program.cs new file mode 100644 index 0000000000..90211c2d43 --- /dev/null +++ b/src/TestApp/Program.cs @@ -0,0 +1,93 @@ +using AutoMapper; +using Microsoft.Extensions.DependencyInjection; + +IServiceCollection services = new ServiceCollection(); +services.AddTransient(sp => new FooService(5)); +services.AddAutoMapper(opt => opt.AddMaps(typeof(Source))); +var provider = services.BuildServiceProvider(); +using (var scope = provider.CreateScope()) +{ + var mapper = scope.ServiceProvider.GetRequiredService(); + + foreach (var typeMap in mapper.ConfigurationProvider.Internal().GetAllTypeMaps()) + { + Console.WriteLine($"{typeMap.SourceType.Name} -> {typeMap.DestinationType.Name}"); + } + + foreach (var service in services) + { + Console.WriteLine(service.ServiceType + " - " + service.ImplementationType); + } + + var dest = mapper.Map(new Source2()); + Console.WriteLine(dest!.ResolvedValue); +} + +Console.ReadKey(); + +public class Source +{ +} + +public class Dest +{ +} + +public class Source2 +{ +} + +public class Dest2 +{ + public int ResolvedValue { get; set; } +} + +public class Profile1 : Profile +{ + public Profile1() + { + CreateMap(); + } +} + +public class Profile2 : Profile +{ + public Profile2() + { + CreateMap() + .ForMember(d => d.ResolvedValue, opt => opt.MapFrom()); + } +} + +public class DependencyResolver : IValueResolver +{ + private readonly ISomeService _service; + + public DependencyResolver(ISomeService service) + { + _service = service; + } + + public int Resolve(object source, object destination, int destMember, ResolutionContext context) + { + return _service.Modify(destMember); + } +} + +public interface ISomeService +{ + int Modify(int value); +} + +public class FooService : ISomeService +{ + private readonly int _value; + + public FooService(int value) + { + _value = value; + } + + public int Modify(int value) => value + _value; +} + diff --git a/src/TestApp/Properties/AssemblyInfo.cs b/src/TestApp/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..fdffe52e63 --- /dev/null +++ b/src/TestApp/Properties/AssemblyInfo.cs @@ -0,0 +1,19 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("TestApp")] +[assembly: AssemblyTrademark("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("de95f633-80b5-4248-a594-7fb357c8dac9")] diff --git a/src/TestApp/TestApp.csproj b/src/TestApp/TestApp.csproj new file mode 100644 index 0000000000..8d7b9ec4b1 --- /dev/null +++ b/src/TestApp/TestApp.csproj @@ -0,0 +1,20 @@ + + + + net10.0 + TestApp + Exe + enable + enable + TestApp + false + false + false + + + + + + + + \ No newline at end of file diff --git a/src/UnitTests/ArraysAndLists.cs b/src/UnitTests/ArraysAndLists.cs index 394a45bba0..eb02f5dd4c 100644 --- a/src/UnitTests/ArraysAndLists.cs +++ b/src/UnitTests/ArraysAndLists.cs @@ -1,9 +1,9 @@ using System.Dynamic; -using AutoMapper.Internal.Mappers; +using AutoMapper.Internal.Mappers; namespace AutoMapper.UnitTests.ArraysAndLists; -public class When_mapping_to_Existing_IEnumerable : AutoMapperSpecBase -{ +public class When_mapping_to_Existing_IEnumerable : AutoMapperSpecBase +{ public class Source { public IEnumerable Items { get; set; } = Enumerable.Empty(); @@ -20,19 +20,19 @@ public class DestinationItem { public string Value { get; set; } } - protected override MapperConfiguration CreateConfiguration() => new(c => + protected override MapperConfiguration CreateConfiguration() => new(c => { c.CreateMap(); c.CreateMap(); - }); - [Fact] - public void Should_overwrite_the_existing_list() - { - var destination = new Destination(); - var existingList = destination.Items; - Mapper.Map(new Source(), destination); - destination.Items.ShouldNotBeSameAs(existingList); - destination.Items.ShouldBeEmpty(); + }); + [Fact] + public void Should_overwrite_the_existing_list() + { + var destination = new Destination(); + var existingList = destination.Items; + Mapper.Map(new Source(), destination); + destination.Items.ShouldNotBeSameAs(existingList); + destination.Items.ShouldBeEmpty(); } } public class When_mapping_to_an_array_as_ICollection_with_MapAtRuntime : AutoMapperSpecBase @@ -116,7 +116,7 @@ public void Should_return_a_copy() var source = new int[] {1, 2, 3, 4}; var dest = new long[4]; Array.Copy(source, dest, 4); - dest[3].ShouldBe(4L); + dest[3].ShouldBe(4L); var plan = Configuration.BuildExecutionPlan(typeof(int[]), typeof(long[])); _destination.ShouldNotBeSameAs(_source); @@ -159,7 +159,10 @@ public Expression MapExpression(IGlobalConfiguration configurationProvider, Prof MemberMap memberMap, Expression sourceExpression, Expression destExpression) => Expression.Multiply(Expression.Convert(sourceExpression, typeof(int)), Expression.Constant(1000)); - } +#if NET471_OR_GREATER + public TypePair? GetAssociatedTypes(TypePair initialTypes) => null; +#endif + } protected override MapperConfiguration CreateConfiguration() => new(c => c.Internal().Mappers.Insert(0, new IntToIntMapper())); @@ -298,8 +301,8 @@ public class Book { public string Name { get; set; } } -} - +} + public class When_mapping_to_an_existing_HashSet_typed_as_IEnumerable : AutoMapperSpecBase { private Destination _destination = new Destination(); @@ -311,7 +314,7 @@ public class Source public class Destination { - public IEnumerable IntCollection { get; set; } = new HashSet { 1, 2, 3, 4, 5 }; + public IEnumerable IntCollection { get; set; } = new HashSet { 1, 2, 3, 4, 5 }; public string Unmapped { get; } } @@ -455,8 +458,8 @@ public void Should_map_from_the_generic_list_of_values_with_formatting() _destination.Values2.ShouldContain("7"); _destination.Values2.ShouldContain("6"); } -} - +} + public class When_mapping_to_a_getter_only_ienumerable : AutoMapperSpecBase { private Destination _destination = new Destination(); @@ -476,13 +479,13 @@ public class Destination }); protected override void Because_of() => _destination = Mapper.Map(new Source { Values = new[] { 1, 2, 3, 4 }, Values2 = new List { 9, 8, 7, 6 } }); [Fact] - public void Should_map_the_list_of_source_items() - { - _destination.Values.ShouldBe(new[] { 1, 2, 3, 4 }); - _destination.Values2.ShouldBe(new[] { "9", "8", "7", "6" }); + public void Should_map_the_list_of_source_items() + { + _destination.Values.ShouldBe(new[] { 1, 2, 3, 4 }); + _destination.Values2.ShouldBe(new[] { "9", "8", "7", "6" }); } -} - +} + public class When_mapping_to_a_getter_only_existing_ienumerable : AutoMapperSpecBase { private Destination _destination = new Destination(); @@ -502,12 +505,12 @@ public class Destination }); protected override void Because_of() => Mapper.Map(new Source { Values = new[] { 1, 2, 3, 4 }, Values2 = new List { 9, 8, 7, 6 } }, _destination); [Fact] - public void Should_map_the_list_of_source_items() - { - _destination.Values.ShouldBe(new[] { 1, 2, 3, 4 }); - _destination.Values2.ShouldBe(new[]{ "9", "8", "7", "6" }); + public void Should_map_the_list_of_source_items() + { + _destination.Values.ShouldBe(new[] { 1, 2, 3, 4 }); + _destination.Values2.ShouldBe(new[]{ "9", "8", "7", "6" }); } -} +} public class When_mapping_to_a_concrete_non_generic_icollection : AutoMapperSpecBase { @@ -799,8 +802,8 @@ public void Should_clear_the_list_before_mapping() { _destination.Values.Count.ShouldBe(2); } -} - +} + public class When_mapping_to_getter_only_list_with_existing_items : AutoMapperSpecBase { public class SourceItem @@ -813,29 +816,29 @@ public class DestItem } public class Source { - public List Values { get; set; } + public List Values { get; set; } public List IValues { get; set; } } public class Destination { - public List Values { get; } = new(); + public List Values { get; } = new(); public IEnumerable IValues { get; } = new List(); } protected override MapperConfiguration CreateConfiguration() => new(cfg => - { + { cfg.CreateMap(); cfg.CreateMap(); - }); + }); [Fact] public void Should_clear_the_list_before_mapping() - { - var destination = new Destination { Values = { new DestItem() } }; + { + var destination = new Destination { Values = { new DestItem() } }; ((List)destination.IValues).Add(new DestItem()); Mapper.Map(new Source(), destination); - destination.Values.ShouldBeEmpty(); + destination.Values.ShouldBeEmpty(); destination.IValues.ShouldBeEmpty(); } -} +} public class When_mapping_to_list_with_existing_items : AutoMapperSpecBase { public class SourceItem @@ -855,24 +858,24 @@ public class Destination public List Values { get; set; } = new(); } protected override MapperConfiguration CreateConfiguration() => new(cfg => - { + { cfg.CreateMap(); cfg.CreateMap(); - }); + }); [Fact] public void Should_clear_the_list_before_mapping() - { + { var destination = new Destination { Values = { new DestItem { } } }; Mapper.Map(new Source { Values = { new SourceItem { Value = 42 } } }, destination); destination.Values.Single().Value.ShouldBe(42); } [Fact] public void Should_clear_the_list_before_mapping_when_the_source_is_null() - { + { var destination = new Destination { Values = { new DestItem { } } }; Mapper.Map(new Source { Values = null }, destination); destination.Values.ShouldBeEmpty(); - } + } } public class When_mapping_a_collection_with_null_members : AutoMapperSpecBase diff --git a/src/UnitTests/AutoMapper.UnitTests.csproj b/src/UnitTests/AutoMapper.UnitTests.csproj index b7b9514f1e..4dc25a1377 100644 --- a/src/UnitTests/AutoMapper.UnitTests.csproj +++ b/src/UnitTests/AutoMapper.UnitTests.csproj @@ -1,16 +1,24 @@  - net7.0 + net10.0 $(NoWarn);649;618 + ..\..\AutoMapper.snk + true + + true + $(TargetFrameworks);net471 + + - - - - + + + + + diff --git a/src/UnitTests/BidirectionalRelationships.cs b/src/UnitTests/BidirectionalRelationships.cs index 90bc34ad22..72a16bbd7e 100644 --- a/src/UnitTests/BidirectionalRelationships.cs +++ b/src/UnitTests/BidirectionalRelationships.cs @@ -432,7 +432,7 @@ public void Should_map_successfully() object.ReferenceEquals(_dtoParent.Children[0].Parents[0], _dtoParent).ShouldBeTrue(); } - public class Parent + public class Parent : IEquatable { public Guid Id { get; private set; } @@ -445,26 +445,9 @@ public Parent() Id = Guid.NewGuid(); Children = new List(); } - - public bool Equals(Parent other) - { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return other.Id.Equals(Id); - } - - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != typeof (Parent)) return false; - return Equals((Parent) obj); - } - - public override int GetHashCode() - { - return Id.GetHashCode(); - } + public bool Equals(Parent other) => throw new NotImplementedException(); + public override bool Equals(object obj) => throw new NotImplementedException(); + public override int GetHashCode() => throw new NotImplementedException(); } public class Child diff --git a/src/UnitTests/Bug/ByrefConstructorParameter.cs b/src/UnitTests/Bug/ByrefConstructorParameter.cs new file mode 100644 index 0000000000..d0feda88c0 --- /dev/null +++ b/src/UnitTests/Bug/ByrefConstructorParameter.cs @@ -0,0 +1,41 @@ +namespace AutoMapper.UnitTests.Bug; + +public class ByrefConstructorParameter : AutoMapperSpecBase +{ + private Destination _destination; + + class Source + { + public TimeSpan X { get; set; } + } + + class Destination + { + public Destination(in TimeSpan x) + { + Y = x; + } + + public TimeSpan Y { get; } + } + + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap(); + }); + + protected override void Because_of() + { + var source = new Source + { + X = TimeSpan.FromSeconds(17) + }; + _destination = Mapper.Map(source); + } + + [Fact] + public void should_just_work() + { + _destination.Y.ShouldBe(TimeSpan.FromSeconds(17)); + } +} \ No newline at end of file diff --git a/src/UnitTests/Bug/ConvertMapperThreading.cs b/src/UnitTests/Bug/ConvertMapperThreading.cs index 4eb344703e..2bf7033a35 100644 --- a/src/UnitTests/Bug/ConvertMapperThreading.cs +++ b/src/UnitTests/Bug/ConvertMapperThreading.cs @@ -13,7 +13,7 @@ class Destination } [Fact] - public void Should_work() + public async Task Should_work() { var tasks = Enumerable.Range(0, 5).Select(i => Task.Factory.StartNew(() => { @@ -21,7 +21,7 @@ public void Should_work() })).ToArray(); try { - Task.WaitAll(tasks); + await Task.WhenAll(tasks); } catch(AggregateException ex) { diff --git a/src/UnitTests/Bug/MultiThreadingIssues.cs b/src/UnitTests/Bug/MultiThreadingIssues.cs index 98dd98bae0..1caa962e30 100644 --- a/src/UnitTests/Bug/MultiThreadingIssues.cs +++ b/src/UnitTests/Bug/MultiThreadingIssues.cs @@ -1,4 +1,6 @@ -namespace AutoMapper.UnitTests.Bug; +using System.Linq; + +namespace AutoMapper.UnitTests.Bug; public class MultiThreadingIssues { public class Type1 @@ -613,7 +615,7 @@ public class Dto } [Fact] - public void Should_work() + public async Task Should_work() { var sourceType = typeof(Entity<>); var destinationType = typeof(Dto<>); @@ -641,12 +643,12 @@ public void Should_work() }; var tasks = types - .Concat(types.Select(t => t.Reverse().ToArray())) + .Concat(types.Select(t => Enumerable.Reverse(t).ToArray())) .Select(t=>(SourceType: sourceType.MakeGenericType(t[0]), DestinationType: destinationType.MakeGenericType(t[1]))) .ToArray() .Select(s => Task.Factory.StartNew(() => c.ResolveTypeMap(s.SourceType, s.DestinationType))) .ToArray(); - Task.WaitAll(tasks); + await Task.WhenAll(tasks); } } @@ -1144,7 +1146,7 @@ public class Dto } [Fact] - public void Should_work() + public async Task Should_work() { var sourceType = typeof(Entity<>); var destinationType = typeof(Dto<>); @@ -1173,11 +1175,11 @@ public void Should_work() }; var tasks = types - .Concat(types.Select(t => t.Reverse().ToArray())) + .Concat(types.Select(t => Enumerable.Reverse(t).ToArray())) .Select(t=>(SourceType: sourceType.MakeGenericType(t[0]), DestinationType: destinationType.MakeGenericType(t[1]))) .ToArray() .Select(s => Task.Factory.StartNew(() => mapper.Map(null, s.SourceType, s.DestinationType))) .ToArray(); - Task.WaitAll(tasks); + await Task.WhenAll(tasks); } } \ No newline at end of file diff --git a/src/UnitTests/Bug/NamingConventions.cs b/src/UnitTests/Bug/NamingConventions.cs index b10629bb0a..1344b0c8d8 100644 --- a/src/UnitTests/Bug/NamingConventions.cs +++ b/src/UnitTests/Bug/NamingConventions.cs @@ -67,7 +67,7 @@ public class Dario public string JaSeZovemImenom { get; set; } } -public class When_mapping_with_lowercae_naming_conventions_two_ways_in_profiles : AutoMapperSpecBase +public class When_mapping_with_lowercase_naming_conventions_two_ways_in_profiles : AutoMapperSpecBase { private Dario _dario; private Neda _neda; @@ -98,6 +98,46 @@ public void Should_map_from_lower_to_pascal() _neda.ja_se_zovem_imenom.ShouldBe("foo"); } + [Fact] + public void Should_map_from_pascal_to_lower() + { + _dario.JaSeZovemImenom.ShouldBe("foo"); + } +} + +public class When_mapping_with_lowercase_naming_conventions_two_ways : AutoMapperSpecBase +{ + private Dario _dario; + private Neda _neda; + + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.SourceMemberNamingConvention = new LowerUnderscoreNamingConvention(); + cfg.DestinationMemberNamingConvention = new LowerUnderscoreNamingConvention(); + cfg.CreateProfile("MyMapperProfile", prf => + { + prf.DestinationMemberNamingConvention = PascalCaseNamingConvention.Instance; + prf.CreateMap(); + }); + cfg.CreateProfile("MyMapperProfile2", prf => + { + prf.SourceMemberNamingConvention = PascalCaseNamingConvention.Instance; + prf.CreateMap(); + }); + }); + + protected override void Because_of() + { + _dario = Mapper.Map(new Neda { ja_se_zovem_imenom = "foo" }); + _neda = Mapper.Map(_dario); + } + + [Fact] + public void Should_map_from_lower_to_pascal() + { + _neda.ja_se_zovem_imenom.ShouldBe("foo"); + } + [Fact] public void Should_map_from_pascal_to_lower() { diff --git a/src/UnitTests/Bug/ReadOnlyCollectionMappingBug.cs b/src/UnitTests/Bug/ReadOnlyCollectionMappingBug.cs index b851d8a832..b680884f84 100644 --- a/src/UnitTests/Bug/ReadOnlyCollectionMappingBug.cs +++ b/src/UnitTests/Bug/ReadOnlyCollectionMappingBug.cs @@ -1,7 +1,7 @@ namespace AutoMapper.UnitTests.Bug; // Bug #511 -// https://github.com/AutoMapper/AutoMapper/issues/511 +// https://github.com/LuckyPennySoftware/AutoMapper/issues/511 public class ReadOnlyCollectionMappingBug { class Source { public int X { get; set; } } diff --git a/src/UnitTests/CollectionMapping.cs b/src/UnitTests/CollectionMapping.cs index 9f2e906957..12e7211e8d 100644 --- a/src/UnitTests/CollectionMapping.cs +++ b/src/UnitTests/CollectionMapping.cs @@ -1,8 +1,63 @@ using System.Collections.Specialized; using System.Collections.Immutable; - namespace AutoMapper.UnitTests; - +public class UnsupportedCollection : AutoMapperSpecBase +{ + class Source + { + public MyList List { get; set; } = new(); + } + class Destination + { + public MyList List { get; set; } + } + class MyList : IEnumerable + { + public IEnumerator GetEnumerator() => new List.Enumerator(); + } + protected override MapperConfiguration CreateConfiguration() => new(c => c.CreateMap()); + [Fact] + public void ThrowsAtMapTime() => new Action(()=>Map(new Source())).ShouldThrow() + .InnerException.ShouldBeOfType().Message.ShouldBe($"Unknown collection. Consider a custom type converter from {typeof(MyList)} to {typeof(MyList)}."); +} +#if NET8_0_OR_GREATER +public class When_mapping_interface_to_interface_readonly_set : AutoMapperSpecBase +{ + public class Source + { + public IReadOnlySet Values { get; set; } + } + public class Destination + { + public IReadOnlySet Values { get; set; } + } + protected override MapperConfiguration CreateConfiguration() => new(config => config.CreateMap()); + [Fact] + public void Should_map_readonly_values() + { + HashSet values = [1, 2, 3, 4]; + Map(new Source { Values = values }).Values.ShouldBe(values); + } +} +public class When_mapping_hashset_to_interface_readonly_set : AutoMapperSpecBase +{ + public class Source + { + public HashSet Values { get; set; } + } + public class Destination + { + public IReadOnlySet Values { get; set; } + } + protected override MapperConfiguration CreateConfiguration() => new(config => config.CreateMap()); + [Fact] + public void Should_map_readonly_values() + { + HashSet values = [1, 2, 3, 4]; + Map(new Source { Values = values }).Values.ShouldBe(values); + } +} +#endif public class NonPublicEnumeratorCurrent : AutoMapperSpecBase { class Source diff --git a/src/UnitTests/ConfigurationRules.cs b/src/UnitTests/ConfigurationRules.cs index b7f390d725..e297f6d2e2 100644 --- a/src/UnitTests/ConfigurationRules.cs +++ b/src/UnitTests/ConfigurationRules.cs @@ -33,19 +33,6 @@ public void Should_throw_for_multiple_create_map_calls() typeof(DuplicateTypeMapConfigurationException).ShouldBeThrownBy(() => config.AssertConfigurationIsValid()); } - [Fact] - public void Should_not_throw_when_allowing_multiple_create_map_calls() - { - var config = new MapperConfiguration(cfg => - { - cfg.CreateMap(); - cfg.CreateMap(); - cfg.Internal().AllowAdditiveTypeMapCreation = true; - }); - - typeof(DuplicateTypeMapConfigurationException).ShouldNotBeThrownBy(() => config.AssertConfigurationIsValid()); - } - [Fact] public void Should_throw_for_multiple_create_map_calls_in_different_profiles() { @@ -58,19 +45,6 @@ public void Should_throw_for_multiple_create_map_calls_in_different_profiles() typeof(DuplicateTypeMapConfigurationException).ShouldBeThrownBy(() => config.AssertConfigurationIsValid()); } - [Fact] - public void Should_not_throw_when_allowing_multiple_create_map_calls_in_different_profiles() - { - var config = new MapperConfiguration(cfg => - { - cfg.AddProfile(); - cfg.AddProfile(); - cfg.Internal().AllowAdditiveTypeMapCreation = true; - }); - - typeof(DuplicateTypeMapConfigurationException).ShouldNotBeThrownBy(() => config.AssertConfigurationIsValid()); - } - [Fact] public void Should_throw_for_multiple_create_map_calls_in_configuration_expression_and_profile() { diff --git a/src/UnitTests/ConfigurationValidation.cs b/src/UnitTests/ConfigurationValidation.cs index babd8e4a93..09cd6250e2 100644 --- a/src/UnitTests/ConfigurationValidation.cs +++ b/src/UnitTests/ConfigurationValidation.cs @@ -1,5 +1,69 @@ namespace AutoMapper.UnitTests.ConfigurationValidation; - +public class When_testing_a_dto_with_mismatched_member_names_and_mismatched_types : AutoMapperSpecBase +{ + public class Source + { + public decimal Foo { get; set; } + } + public class Destination + { + public Type Foo { get; set; } + public string Bar { get; set; } + } + protected override MapperConfiguration CreateConfiguration() => new(cfg => cfg.CreateMap()); + [Fact] + public void Should_throw_unmapped_member_and_mismatched_type_exceptions() + { + new Action(AssertConfigurationIsValid) + .ShouldThrow() + .ShouldSatisfyAllConditions( + aex => aex.InnerExceptions.ShouldBeOfLength(2), + aex => aex.InnerExceptions[0] + .ShouldBeOfType() + .ShouldSatisfyAllConditions( + ex => ex.Errors.ShouldBeOfLength(1), + ex => ex.Errors[0].UnmappedPropertyNames.ShouldContain("Bar")), + aex => aex.InnerExceptions[1] + .ShouldBeOfType() + .ShouldSatisfyAllConditions( + ex => ex.MemberMap.ShouldNotBeNull(), + ex => ex.MemberMap.DestinationName.ShouldBe("Foo")) + ); + } +} +public class When_testing_a_dto_with_mismatches_in_multiple_children : AutoMapperSpecBase +{ + public class Source + { + public Type Foo { get; set; } + public Type Bar { get; set; } + } + public class Destination + { + public int Foo { get; set; } + public int Bar { get; set; } + } + protected override MapperConfiguration CreateConfiguration() => new(cfg => cfg.CreateMap()); + [Fact] + public void Should_throw_for_both_mismatched_children() + { + new Action(AssertConfigurationIsValid) + .ShouldThrow() + .ShouldSatisfyAllConditions( + aex => aex.InnerExceptions.ShouldBeOfLength(2), + aex => aex.InnerExceptions[0] + .ShouldBeOfType() + .ShouldSatisfyAllConditions( + ex => ex.MemberMap.ShouldNotBeNull(), + ex => ex.MemberMap.DestinationName.ShouldBe("Foo")), + aex => aex.InnerExceptions[1] + .ShouldBeOfType() + .ShouldSatisfyAllConditions( + ex => ex.MemberMap.ShouldNotBeNull(), + ex => ex.MemberMap.DestinationName.ShouldBe("Bar")) + ); + } +} public class ConstructorMappingValidation : NonValidatingSpecBase { public class Destination @@ -795,4 +859,26 @@ public class Command } [Fact] public void Validate() => AssertConfigurationIsValid(); +} +public class ObjectPropertyAndNestedTypes : AutoMapperSpecBase +{ + protected override MapperConfiguration CreateConfiguration() => new(cfg => cfg.CreateMap()); + public class RootLevel + { + public object ObjectProperty { get; set; } + public SecondLevel SecondLevel { get; set; } + } + public class RootLevelDto + { + public object ObjectProperty { get; set; } + public SecondLevelDto SecondLevel { get; set; } + } + public class SecondLevel + { + } + public class SecondLevelDto + { + } + [Fact] + public void Should_fail_validation() => new Action(AssertConfigurationIsValid).ShouldThrow().MemberMap.DestinationName.ShouldBe(nameof(RootLevelDto.SecondLevel)); } \ No newline at end of file diff --git a/src/UnitTests/ContextItems.cs b/src/UnitTests/ContextItems.cs index eddba1ed8e..ad488bee3d 100644 --- a/src/UnitTests/ContextItems.cs +++ b/src/UnitTests/ContextItems.cs @@ -1,4 +1,30 @@ namespace AutoMapper.UnitTests; +public class When_mapping_with_context_state +{ + public class Source + { + public int Value { get; set; } + } + public class Dest + { + public int Value { get; set; } + } + public class ContextResolver : IMemberValueResolver + { + public int Resolve(Source src, Dest d, int source, int dest, ResolutionContext context) => source + (int)context.State; + } + [Fact] + public void Should_use_value_passed_in() + { + var config = new MapperConfiguration(cfg => + { + cfg.CreateMap() + .ForMember(d => d.Value, opt => opt.MapFrom(src => src.Value)); + }); + var dest = config.CreateMapper().Map(new Source { Value = 5 }, opt => { opt.State = 10; }); + dest.Value.ShouldBe(15); + } +} public class Context_try_get_items : AutoMapperSpecBase { protected override MapperConfiguration CreateConfiguration() => new(c => c.CreateMap().ConvertUsing((s, _, c) => diff --git a/src/UnitTests/CustomValidations.cs b/src/UnitTests/CustomValidations.cs index a21d424d56..0b5774562e 100644 --- a/src/UnitTests/CustomValidations.cs +++ b/src/UnitTests/CustomValidations.cs @@ -38,7 +38,7 @@ public void Should_call_the_validator() cfg.CreateMap(); }); - config.AssertConfigurationIsValid(); + new Action(config.AssertConfigurationIsValid).ShouldThrow().Message.ShouldBe(nameof(When_using_custom_validation)); _calledForRoot.ShouldBeTrue(); _calledForValues.ShouldBeTrue(); @@ -55,6 +55,7 @@ private void Validator(ValidationContext context) context.Types.DestinationType.ShouldBe(typeof(Dest)); context.ObjectMapper.ShouldBeNull(); context.MemberMap.ShouldBeNull(); + context.Exceptions.Add(new AutoMapperConfigurationException(nameof(When_using_custom_validation))); } else { diff --git a/src/UnitTests/Enumerations.cs b/src/UnitTests/Enumerations.cs index dd0ffaf58e..1d8b75a812 100644 --- a/src/UnitTests/Enumerations.cs +++ b/src/UnitTests/Enumerations.cs @@ -1,8 +1,29 @@ using System.Runtime.Serialization; using AutoMapper.UnitTests; - namespace AutoMapper.Tests; - +public class CreateProjectionEnum : AutoMapperSpecBase +{ + public class Source + { + public string Name { get; set; } + public SourceEnum Value { get; set; } + } + public class Dest + { + public string Name { get; set; } + public DestEnum Value { get; set; } + } + public enum SourceEnum { A, B } + public enum DestEnum { A, B } + protected override MapperConfiguration CreateConfiguration() => new(c => + { + c.CreateProjection().ConvertUsing(src => src == SourceEnum.A ? DestEnum.A : DestEnum.B); + c.CreateProjection(); + c.Internal().ForAllMaps(static (_, _) => { }); + }); + [Fact] + public void Should_work() => ProjectTo(new[] { new Source() }.AsQueryable()).Single().Value.ShouldBe(DestEnum.A); +} public class InvalidStringToEnum : AutoMapperSpecBase { protected override MapperConfiguration CreateConfiguration() => new(_=> { }); diff --git a/src/UnitTests/ExtensionMethods.cs b/src/UnitTests/ExtensionMethods.cs index 5e5591ba39..6d7133a267 100644 --- a/src/UnitTests/ExtensionMethods.cs +++ b/src/UnitTests/ExtensionMethods.cs @@ -64,7 +64,7 @@ public static class BarExtensions public static string GetSimpleName(this When_null_is_passed_to_an_extension_method.Bar source) { if(source == null) - throw new ArgumentNullException("source"); + throw new System.ArgumentNullException("source"); return "SimpleName"; } } diff --git a/src/UnitTests/ForAllMembers.cs b/src/UnitTests/ForAllMembers.cs index e24001f4d0..b8e5890a2e 100644 --- a/src/UnitTests/ForAllMembers.cs +++ b/src/UnitTests/ForAllMembers.cs @@ -76,4 +76,28 @@ public void Should_use_resolver() dest.SomeDate.ShouldBe(source.SomeDate); dest.OtherDate.ShouldBe(source.OtherDate.AddDays(1)); } +} +public class ForAllPropertyMaps_ConvertUsing : AutoMapperSpecBase +{ + public class Well + { + public SpecialTags SpecialTags { get; set; } + } + [Flags] + public enum SpecialTags { None, SendState, NotSendZeroWhenOpen } + public class PostPutWellViewModel + { + public SpecialTags[] SpecialTags { get; set; } = Array.Empty(); + } + class EnumToArray : IValueConverter + { + public object Convert(object sourceMember, ResolutionContext context) => new[] { SpecialTags.SendState }; + } + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap(); + cfg.Internal().ForAllPropertyMaps(pm => pm.SourceType != null, (tm, mapper) => mapper.ConvertUsing(new EnumToArray())); + }); + [Fact] + public void ShouldWork() => Map(new Well()).SpecialTags.Single().ShouldBe(SpecialTags.SendState); } \ No newline at end of file diff --git a/src/UnitTests/ForPath.cs b/src/UnitTests/ForPath.cs index d84f6eea3c..eb45f2b165 100644 --- a/src/UnitTests/ForPath.cs +++ b/src/UnitTests/ForPath.cs @@ -250,11 +250,11 @@ public class SourceModel [Fact] public void Should_throw_exception() { - Assert.Throws(() => + Assert.Throws(() => { var cfg = new MapperConfiguration(config => { - Assert.Throws(() => + Assert.Throws(() => { config.CreateMap() .ForPath(sourceModel => sourceModel.Name, opts => opts.MapFrom(null)); diff --git a/src/UnitTests/IMappingExpression/IncludeMembers.cs b/src/UnitTests/IMappingExpression/IncludeMembers.cs index 072c88ccf4..d01f080d67 100644 --- a/src/UnitTests/IMappingExpression/IncludeMembers.cs +++ b/src/UnitTests/IMappingExpression/IncludeMembers.cs @@ -1776,4 +1776,35 @@ class Destination }); [Fact] public void Should_flatten() => Mapper.Map(new[] { default(Source) })[0].ShouldBeNull(); +} +public class IncludeMembersCascadedNullCheck : AutoMapperSpecBase +{ + public class Grandchild + { + public string C { get; set; } + } + public class Child + { + public string B { get; set; } + public Grandchild Grandchild { get; set; } + } + public class Parent + { + public string A { get; set; } + public Child Child { get; set; } + } + public class Dto + { + public string A { get; set; } + public string B { get; set; } + public string C { get; set; } + } + protected override MapperConfiguration CreateConfiguration() => new(c => + { + c.CreateMap().IncludeMembers(s => s.Child); + c.CreateMap(MemberList.None).IncludeMembers(s => s.Grandchild); + c.CreateMap(MemberList.None); + }); + [Fact] + public void Should_flatten() => Mapper.Map(new Parent { A = "a" }).A.ShouldBe("a"); } \ No newline at end of file diff --git a/src/UnitTests/InterfaceMapping.cs b/src/UnitTests/InterfaceMapping.cs index 34712f0540..6ae2010ef2 100644 --- a/src/UnitTests/InterfaceMapping.cs +++ b/src/UnitTests/InterfaceMapping.cs @@ -1,5 +1,26 @@ namespace AutoMapper.UnitTests.InterfaceMapping; - +public class InterfaceWithObjectProperty : AutoMapperSpecBase +{ + protected override MapperConfiguration CreateConfiguration() => new(cfg => cfg.CreateMap()); + public interface ISourceModel + { + object Id { get; set; } + } + public interface IDestModel + { + object Id { get; set; } + } + public class SourceModel : ISourceModel + { + public object Id { get; set; } + } + public class DestModel : IDestModel + { + public object Id { get; set; } + } + [Fact] + public void Should_work() => Mapper.Map(new SourceModel { Id = 42 }, new DestModel()).Id.ShouldBe(42); +} public class InterfaceInheritance : AutoMapperSpecBase { protected override MapperConfiguration CreateConfiguration() => new(cfg => diff --git a/src/UnitTests/Internal/CreateProxyThreading.cs b/src/UnitTests/Internal/CreateProxyThreading.cs index 84c49e2f83..5b938c16c5 100644 --- a/src/UnitTests/Internal/CreateProxyThreading.cs +++ b/src/UnitTests/Internal/CreateProxyThreading.cs @@ -5,13 +5,13 @@ namespace AutoMapper.UnitTests; public class CreateProxyThreading { [Fact] - public void Should_create_the_proxy_once() + public async Task Should_create_the_proxy_once() { var tasks = Enumerable.Range(0, 5).Select(i => Task.Factory.StartNew(() => { ProxyGenerator.GetProxyType(typeof(ISomeDto)); })).ToArray(); - Task.WaitAll(tasks); + await Task.WhenAll(tasks); } public interface ISomeDto diff --git a/src/UnitTests/Internal/GenerateSimilarType.cs b/src/UnitTests/Internal/GenerateSimilarType.cs index 0a717ff739..1afa729818 100644 --- a/src/UnitTests/Internal/GenerateSimilarType.cs +++ b/src/UnitTests/Internal/GenerateSimilarType.cs @@ -4,13 +4,13 @@ namespace AutoMapper.UnitTests; public class GenerateSimilarType { - public partial class Article + public partial record struct Article { public int Id { get; set; } public int ProductId { get; set; } public bool IsDefault { get; set; } public short NationId { get; set; } - public virtual Product Product { get; set; } + public Product Product { get; set; } } public partial class Product @@ -45,7 +45,7 @@ public void Should_work() instance.ECommercePublished = true; instance.Short = short.MaxValue; instance.Long = long.MaxValue; - var articles = new Article[] { new Article(), null, null }; + var articles = new Article[] { new Article(), default, default }; instance.Articles = articles; instance.Article = articles[0]; diff --git a/src/UnitTests/Internal/TypeMapFactorySpecs.cs b/src/UnitTests/Internal/TypeMapFactorySpecs.cs index 7cf0081e2a..2015642892 100644 --- a/src/UnitTests/Internal/TypeMapFactorySpecs.cs +++ b/src/UnitTests/Internal/TypeMapFactorySpecs.cs @@ -5,7 +5,7 @@ public class StubNamingConvention : INamingConvention { public Regex SplittingExpression { get; set; } public string SeparatorCharacter { get; set; } - public string[] Split(string input) => SplittingExpression.Matches(input).Select(m=>m.Value).ToArray(); + public string[] Split(string input) => SplittingExpression.Matches(input).Cast().Select(m=>m.Value).ToArray(); } public class When_constructing_type_maps_with_matching_property_names : NonValidatingSpecBase diff --git a/src/UnitTests/Licensing/LicenseValidatorTests.cs b/src/UnitTests/Licensing/LicenseValidatorTests.cs new file mode 100644 index 0000000000..95ab06b3e1 --- /dev/null +++ b/src/UnitTests/Licensing/LicenseValidatorTests.cs @@ -0,0 +1,113 @@ +using System.Security.Claims; +using AutoMapper.Licensing; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing; +using License = AutoMapper.Licensing.License; + +namespace AutoMapper.UnitTests.Licensing; + +public class LicenseValidatorTests +{ + [Fact] + public void Should_return_invalid_when_no_claims() + { + var factory = new LoggerFactory(); + var provider = new FakeLoggerProvider(); + factory.AddProvider(provider); + + var licenseValidator = new LicenseValidator(factory); + var license = new License(); + + license.IsConfigured.ShouldBeFalse(); + + licenseValidator.Validate(license); + + var logMessages = provider.Collector.GetSnapshot(); + + logMessages + .ShouldContain(log => log.Level == LogLevel.Warning); + } + + + [Fact] + public void Should_return_valid_when_community() + { + var factory = new LoggerFactory(); + var provider = new FakeLoggerProvider(); + factory.AddProvider(provider); + + var licenseValidator = new LicenseValidator(factory); + var license = new License( + new Claim("account_id", Guid.NewGuid().ToString()), + new Claim("customer_id", Guid.NewGuid().ToString()), + new Claim("sub_id", Guid.NewGuid().ToString()), + new Claim("iat", DateTimeOffset.UtcNow.AddDays(-1).ToUnixTimeSeconds().ToString()), + new Claim("exp", DateTimeOffset.UtcNow.AddDays(1).ToUnixTimeSeconds().ToString()), + new Claim("edition", nameof(Edition.Community)), + new Claim("type", nameof(AutoMapper.Licensing.ProductType.Bundle))); + + license.IsConfigured.ShouldBeTrue(); + + licenseValidator.Validate(license); + + var logMessages = provider.Collector.GetSnapshot(); + + logMessages.ShouldNotContain(log => log.Level == LogLevel.Error + || log.Level == LogLevel.Warning + || log.Level == LogLevel.Critical); + } + + [Fact] + public void Should_return_invalid_when_not_correct_type() + { + var factory = new LoggerFactory(); + var provider = new FakeLoggerProvider(); + factory.AddProvider(provider); + + var licenseValidator = new LicenseValidator(factory); + var license = new License( + new Claim("account_id", Guid.NewGuid().ToString()), + new Claim("customer_id", Guid.NewGuid().ToString()), + new Claim("sub_id", Guid.NewGuid().ToString()), + new Claim("iat", DateTimeOffset.UtcNow.AddDays(-1).ToUnixTimeSeconds().ToString()), + new Claim("exp", DateTimeOffset.UtcNow.AddYears(1).ToUnixTimeSeconds().ToString()), + new Claim("edition", nameof(Edition.Professional)), + new Claim("type", nameof(AutoMapper.Licensing.ProductType.MediatR))); + + license.IsConfigured.ShouldBeTrue(); + + licenseValidator.Validate(license); + + var logMessages = provider.Collector.GetSnapshot(); + + logMessages + .ShouldContain(log => log.Level == LogLevel.Error); + } + + [Fact] + public void Should_return_invalid_when_expired() + { + var factory = new LoggerFactory(); + var provider = new FakeLoggerProvider(); + factory.AddProvider(provider); + + var licenseValidator = new LicenseValidator(factory); + var license = new License( + new Claim("account_id", Guid.NewGuid().ToString()), + new Claim("customer_id", Guid.NewGuid().ToString()), + new Claim("sub_id", Guid.NewGuid().ToString()), + new Claim("iat", DateTimeOffset.UtcNow.AddYears(-1).ToUnixTimeSeconds().ToString()), + new Claim("exp", DateTimeOffset.UtcNow.AddDays(-1).ToUnixTimeSeconds().ToString()), + new Claim("edition", nameof(Edition.Professional)), + new Claim("type", nameof(AutoMapper.Licensing.ProductType.AutoMapper))); + + license.IsConfigured.ShouldBeTrue(); + + licenseValidator.Validate(license); + + var logMessages = provider.Collector.GetSnapshot(); + + logMessages + .ShouldContain(log => log.Level == LogLevel.Error); + } +} \ No newline at end of file diff --git a/src/UnitTests/Mappers/CustomMapperTests.cs b/src/UnitTests/Mappers/CustomMapperTests.cs index 91d49191d7..975b5fe99c 100644 --- a/src/UnitTests/Mappers/CustomMapperTests.cs +++ b/src/UnitTests/Mappers/CustomMapperTests.cs @@ -67,6 +67,9 @@ public Expression MapExpression(IGlobalConfiguration configurationProvider, Prof return expr.Body; } +#if NET471_OR_GREATER + public TypePair? GetAssociatedTypes(TypePair initialTypes) => null; +#endif } public class ClassA diff --git a/src/UnitTests/Mappers/DynamicMapperTests.cs b/src/UnitTests/Mappers/DynamicMapperTests.cs index b16a0fa0a2..36bae7ed1c 100644 --- a/src/UnitTests/Mappers/DynamicMapperTests.cs +++ b/src/UnitTests/Mappers/DynamicMapperTests.cs @@ -67,6 +67,18 @@ public void Should_map_source_properties() Assert.Equal(12, _destination.Baz); ((int[])_destination.Data).SequenceEqual(data).ShouldBeTrue(); } + [Fact] + public void Should_map_to_ExpandoObject() + { + var config = new MapperConfiguration(cfg => { }); + var data = new[] { 1, 2, 3 }; + _destination = config.CreateMapper().Map(new Destination { Foo = "Foo", Bar = "Bar", Data = data, Baz = 12 }); + ((IDictionary)_destination).Count.ShouldBe(4); + Assert.Equal("Foo", _destination.Foo); + Assert.Equal("Bar", _destination.Bar); + Assert.Equal(12, _destination.Baz); + ((int[])_destination.Data).SequenceEqual(data).ShouldBeTrue(); + } } public class When_mapping_from_dynamic diff --git a/src/UnitTests/MappingInheritance/IncludedBaseMappingShouldInheritBaseMappings.cs b/src/UnitTests/MappingInheritance/IncludedBaseMappingShouldInheritBaseMappings.cs index 35579223e7..9244fb7307 100644 --- a/src/UnitTests/MappingInheritance/IncludedBaseMappingShouldInheritBaseMappings.cs +++ b/src/UnitTests/MappingInheritance/IncludedBaseMappingShouldInheritBaseMappings.cs @@ -27,6 +27,66 @@ public class OtherDto public string SubString { get; set; } } + public record class RecordObject(string DifferentBaseString) + { + } + + public record class RecordSubObject(string DifferentBaseString, string SubString) : RecordObject(DifferentBaseString) + { + } + + public record class RecordOtherObject(string BaseString) + { + } + + public record class RecordOtherSubObject(string BaseString, string SubString) : RecordOtherObject(BaseString) + { + } + + public record class RecordOtherSubObjectWithExtraParam(string BaseString, string SubString, string ExtraString) : RecordOtherObject(BaseString) + { + } + + public class ModelObjectWithConstructor + { + public ModelObjectWithConstructor(string onePrime) + { + OnePrime = onePrime; + } + + public string OnePrime { get; } + } + + public class ModelSubObjectWithConstructor : ModelObjectWithConstructor + { + public ModelSubObjectWithConstructor(string onePrime, string two) : base(onePrime) + { + Two = two; + } + + public string Two { get; } + } + + public class DtoObjectWithConstructor + { + public DtoObjectWithConstructor(string one) + { + One = one; + } + + public string One { get; } + } + + public class DtoSubObjectWithConstructorAndWrongType : DtoObjectWithConstructor + { + public DtoSubObjectWithConstructorAndWrongType(int one, string two) : base(one.ToString()) + { + Two = two; + } + + public string Two { get; } + } + [Fact] public void included_mapping_should_inherit_base_mappings_should_not_throw() { @@ -320,6 +380,44 @@ public void include_should_apply_null_substitute() dest.BaseString.ShouldBe("12345"); } + + [Fact] + public void included_mapping_should_inherit_base_constructor_mappings_should_not_throw() + { + var config = new MapperConfiguration(cfg => + { + cfg.ShouldUseConstructor = constructor => constructor.IsPublic; + cfg.CreateMap() + .ForCtorParam(nameof(RecordOtherObject.BaseString), m => m.MapFrom(s => s.DifferentBaseString)) + .Include() + .Include(); + cfg.CreateMap(); + cfg.CreateMap() + .ForCtorParam(nameof(RecordOtherSubObjectWithExtraParam.ExtraString), m => m.MapFrom(s => s.DifferentBaseString + s.SubString)); + }); + config.AssertConfigurationIsValid(); + + var mapper = config.CreateMapper(); + var dest = mapper.Map(new RecordSubObject("base", "sub")); + + dest.BaseString.ShouldBe("base"); + dest.SubString.ShouldBe("sub"); + dest.ExtraString.ShouldBe("basesub"); + } + + [Fact] + public void included_mapping_with_parameter_has_same_name_but_diffent_type_should_throw() + { + var config = new MapperConfiguration(cfg => + { + cfg.CreateMap() + .ForCtorParam("one", m => m.MapFrom(s => s.OnePrime)) + .Include(); + cfg.CreateMap(); + }); + + Assert.Throws(config.AssertConfigurationIsValid).Errors.Single().CanConstruct.ShouldBeFalse(); + } } public class OverrideDifferentMapFrom : AutoMapperSpecBase diff --git a/src/UnitTests/MappingInheritance/IncludedMappingShouldInheritBaseMappings.cs b/src/UnitTests/MappingInheritance/IncludedMappingShouldInheritBaseMappings.cs index 66eb31db63..54dc32de8e 100644 --- a/src/UnitTests/MappingInheritance/IncludedMappingShouldInheritBaseMappings.cs +++ b/src/UnitTests/MappingInheritance/IncludedMappingShouldInheritBaseMappings.cs @@ -1,5 +1,34 @@ namespace AutoMapper.UnitTests; - +public class IncludeBaseIndirectBase : AutoMapperSpecBase +{ + public class FooBaseBase + { + } + public class FooBase : FooBaseBase + { + } + public class Foo : FooBase + { + } + public class FooDtoBaseBase + { + public DateTime Date { get; set; } + } + public class FooDtoBase : FooDtoBaseBase + { + } + public class FooDto : FooDtoBase + { + } + protected override MapperConfiguration CreateConfiguration() => new(c => + { + c.CreateMap().IncludeBase(); + c.CreateMap().IncludeBase(); + c.CreateMap().ForMember(d => d.Date, o => o.MapFrom(s => DateTime.MaxValue)); + }); + [Fact] + public void Should_work() => Map(new Foo()).Date.ShouldBe(DateTime.MaxValue); +} public class ReadonlyCollectionPropertiesOverride : AutoMapperSpecBase { protected override MapperConfiguration CreateConfiguration() => new(cfg => diff --git a/src/UnitTests/NullBehavior.cs b/src/UnitTests/NullBehavior.cs index 7d29dca688..dbf8cc442b 100644 --- a/src/UnitTests/NullBehavior.cs +++ b/src/UnitTests/NullBehavior.cs @@ -1,4 +1,20 @@ namespace AutoMapper.UnitTests.NullBehavior; +public class NullDestinationType : AutoMapperSpecBase +{ + protected override MapperConfiguration CreateConfiguration() => new(c => { }); + [Fact] + public void Should_require_destination_object() + { + Mapper.Map("", "", null, null).ShouldBe(""); + Mapper.Map("", null, null, typeof(string)).ShouldBe(""); + Mapper.Map("", "", null, null, _ => { }).ShouldBe(""); + Mapper.Map("", null, null, typeof(string), _=>{ }).ShouldBe(""); + Mapper.Map("").ShouldBe(""); + Mapper.Map("", default(string)).ShouldBe(""); + Mapper.Map("", _ => { }).ShouldBe(""); + Mapper.Map("", default(string), _ => { }).ShouldBe(""); + } +} public class NullToExistingDestination : AutoMapperSpecBase { protected override MapperConfiguration CreateConfiguration() => new(c => c.CreateMap().DisableCtorValidation()); diff --git a/src/UnitTests/Projection/ConstructorTests.cs b/src/UnitTests/Projection/ConstructorTests.cs index 74156d00c5..aa811a4ead 100644 --- a/src/UnitTests/Projection/ConstructorTests.cs +++ b/src/UnitTests/Projection/ConstructorTests.cs @@ -199,6 +199,508 @@ public void Should_construct_correctly() } } public class NestedConstructors : AutoMapperSpecBase +{ + public class A + { + public int Id { get; set; } + public B B { get; set; } + } + public class B + { + public int Id { get; set; } + } + public class DtoA + { + public DtoB B { get; } + public DtoA(DtoB b) => B = b; + } + public class DtoB + { + public int Id { get; } + public DtoB(int id) => Id = id; + } + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateProjection(); + cfg.CreateProjection(); + }); + [Fact] + public void Should_project_ok() => + ProjectTo(new[] { new A { B = new B { Id = 3 } } }.AsQueryable()).FirstOrDefault().B.Id.ShouldBe(3); +} + +public class ConstructorLetClauseWithIheritance : AutoMapperSpecBase +{ + class Source + { + public IList Items { get; set; } + } + class SourceA : Source + { + public string A { get; set; } + } + class SourceB : Source + { + public string B { get; set; } + } + + class SourceItem + { + public IList Values { get; set; } + } + class SourceValue + { + public int Value1 { get; set; } + public int Value2 { get; set; } + } + class Destination + { + public Destination(DestinationItem item) => Item = item; + public DestinationItem Item { get; } + } + class DestinationA : Destination + { + public DestinationA(DestinationItem item, string a) : base(item) => A = a; + public string A { get; } + } + class DestinationB : Destination + { + public DestinationB(DestinationItem item, string b) : base(item) => B = b; + public string B { get; } + } + + class DestinationValue + { + public DestinationValue(int value1, int value2) + { + Value1 = value1; + Value2 = value2; + } + public int Value1 { get; } + public int Value2 { get; } + } + class DestinationItem + { + public DestinationItem(DestinationValue destinationValue) + { + Value1 = destinationValue.Value1; + Value2 = destinationValue.Value2; + } + public int Value1 { get; } + public int Value2 { get; } + public IList Values { get; set; } + } + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap() + .ForCtorParam("item", o => o.MapFrom(s => s.Items.FirstOrDefault())) + .Include() + .Include(); + cfg.CreateMap() + .ForCtorParam("item", o => o.MapFrom(s => s.Items.FirstOrDefault())) + .ForCtorParam("a", o => o.MapFrom(s => s.A)); + cfg.CreateMap() + .ForCtorParam("item", o => o.MapFrom(s => s.Items.FirstOrDefault())) + .ForCtorParam("b", o => o.MapFrom(s => s.B)); + + cfg.CreateMap().ForCtorParam("destinationValue", o => o.MapFrom(s => s.Values.FirstOrDefault())); + cfg.CreateMap(); + }); + [Fact] + public void Should_construct_correctly() + { + var query = new[] { + new SourceA + { + A = "a", + Items = new[] + { + new SourceItem { Values = new[] { new SourceValue { Value1 = 1, Value2 = 2 } } } + } + }, + new SourceB + { + B = "b", + Items = new[] + { + new SourceItem { Values = new[] { new SourceValue { Value1 = 1, Value2 = 2 } } } + } + }, + new Source + { + Items = new[] + { + new SourceItem { Values = new[] { new SourceValue { Value1 = 1, Value2 = 2 } } } + } + } + }.AsQueryable().ProjectTo(Configuration); + + var list = query.ToList(); + var first = list.First(); + first.Item.Value1.ShouldBe(1); + first.Item.Value2.ShouldBe(2); + var firstValue = first.Item.Values.Single(); + firstValue.Value1.ShouldBe(1); + firstValue.Value2.ShouldBe(2); + + list.OfType().Any(a => a.A == "a").ShouldBeTrue(); + list.OfType().Any(a => a.B == "b").ShouldBeTrue(); + } +} +public class ConstructorToStringWithIheritance : AutoMapperSpecBase +{ + class Source + { + public int Value { get; set; } + } + class SourceA : Source + { + public string A { get; set; } + } + class SourceB : Source + { + public string B { get; set; } + } + class Destination + { + public Destination(string value) => Value = value; + public string Value { get; } + } + class DestinationA : Destination + { + public DestinationA(string value, string a) : base(value) => A = a; + public string A { get; } + } + class DestinationB : Destination + { + public DestinationB(string value, string b) : base(value) => B = b; + public string B { get; } + } + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap() + .Include(); + cfg.CreateMap(); + cfg.CreateMap(); + }); + [Fact] + public void Should_construct_correctly() + { + var list = new[] + { + new Source { Value = 5 }, + new SourceA { Value = 5, A = "a" }, + new SourceB { Value = 5, B = "b" } + }.AsQueryable().ProjectTo(Configuration); + + list.ShouldAllBe(p => p.Value == "5"); + list.OfType().Any(p => p.A == "a"); + list.OfType().Any(p => p.B == "b"); + } +} +public class ConstructorMapFromWithIheritance : AutoMapperSpecBase +{ + class Source + { + public int Value { get; set; } + } + class SourceA : Source + { + public string A { get; set; } + } + class SourceB : Source + { + public string B { get; set; } + } + record Destination(bool Value) + { + } + record DestinationA(bool Value, bool HasA) : Destination(Value) + { + } + record DestinationB(bool Value, bool HasB) : Destination(Value) + { + } + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap() + .ForCtorParam(nameof(Destination.Value), o => o.MapFrom(s => s.Value == 5)) + .Include() + .Include(); + cfg.CreateMap() + .ForCtorParam(nameof(Destination.Value), o => o.MapFrom(s => s.Value == 5)) + .ForCtorParam(nameof(DestinationA.HasA), o => o.MapFrom(s => s.A == "a")); + cfg.CreateMap() + .ForCtorParam(nameof(Destination.Value), o => o.MapFrom(s => s.Value == 5)) + .ForCtorParam(nameof(DestinationB.HasB), o => o.MapFrom(s => s.B == "b")); + }); + [Fact] + public void Should_construct_correctly() + { + var list = new[] + { + new Source { Value = 5 }, + new SourceA { Value = 5, A = "a" }, + new SourceB { Value = 5, B = "b" } + }.AsQueryable().ProjectTo(Configuration); + + list.All(p => p.Value).ShouldBeTrue(); + list.OfType().Any(p => p.HasA).ShouldBeTrue(); + list.OfType().Any(p => p.HasB).ShouldBeTrue(); + } +} +public class ConstructorIncludeMembersWithIheritance : AutoMapperSpecBase +{ + class SourceWrapper + { + public Source Source { get; set; } + } + class SourceWrapperA : SourceWrapper + { + public SourceA SourceA { get; set; } + } + class SourceWrapperB : SourceWrapper + { + public SourceB SourceB { get; set; } + } + class Source + { + public int Value { get; set; } + } + class SourceA + { + public string A { get; set; } + } + class SourceB + { + public string B { get; set; } + } + class Destination + { + public Destination(string value) => Value = value; + public string Value { get; } + } + + class DestinationA : Destination + { + public DestinationA(string value, string a) : base(value) => A = a; + public string A { get; } + } + class DestinationB : Destination + { + public DestinationB(string value, string b) : base(value) => B = b; + public string B { get; } + } + + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap() + .IncludeMembers(s => s.Source) + .Include() + .Include(); + cfg.CreateMap() + .IncludeMembers(s => s.Source, s => s.SourceA); + cfg.CreateMap() + .IncludeMembers(s => s.Source, s => s.SourceB); + cfg.CreateMap(); + cfg.CreateMap(MemberList.None); + cfg.CreateMap(MemberList.None); + cfg.CreateMap(MemberList.None); + cfg.CreateMap(MemberList.None); + }); + [Fact] + public void Should_construct_correctly() + { + var list = new[] + { + new SourceWrapper { Source = new Source { Value = 5 } }, + new SourceWrapperA { Source = new Source { Value = 5 }, SourceA = new SourceA() { A = "a" } }, + new SourceWrapperB { Source = new Source { Value = 5 }, SourceB = new SourceB() { B = "b" } } + }.AsQueryable().ProjectTo(Configuration); + + list.All(p => p.Value == "5").ShouldBeTrue(); + list.OfType().Any(p => p.A == "a").ShouldBeTrue(); + list.OfType().Any(p => p.B == "b").ShouldBeTrue(); + } +} +public class ConstructorsWithCollectionsWithIheritance : AutoMapperSpecBase +{ + class Addresses + { + public int Id { get; set; } + public string Address { get; set; } + public ICollection Users { get; set; } + } + class Users + { + public int Id { get; set; } + public Addresses FkAddress { get; set; } + } + class UsersA : Users + { + public string A { get; set; } + } + class UsersB : Users + { + public string B { get; set; } + } + class AddressDto + { + public int Id { get; } + public string Address { get; } + public AddressDto(int id, string address) + { + Id = id; + Address = address; + } + } + class UserDto + { + public int Id { get; set; } + public AddressDto AddressDto { get; set; } + } + class UserADto : UserDto + { + public string A { get; set; } + } + class UserBDto : UserDto + { + public string B { get; set; } + } + + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap() + .ForMember(d => d.AddressDto, e => e.MapFrom(s => s.FkAddress)) + .Include() + .Include(); + + cfg.CreateMap(); + cfg.CreateMap(); + + cfg.CreateMap().ConstructUsing(a => new AddressDto(a.Id, a.Address)); + }); + [Fact] + public void Should_work() + { + var list = ProjectTo(new[] + { + new Users { FkAddress = new Addresses { Address = "address" } }, + new UsersA { A = "a", FkAddress = new Addresses { Address = "address" } }, + new UsersB { B = "b", FkAddress = new Addresses { Address = "address" } } + }.AsQueryable()).ToList(); + + list.All(p => p.AddressDto.Address == "address").ShouldBeTrue(); + list.OfType().Any(p => p.A == "a").ShouldBeTrue(); + list.OfType().Any(p => p.B == "b").ShouldBeTrue(); + } +} +public class ConstructorTestsWithIheritance : AutoMapperSpecBase +{ + private Dest[] _dest; + + public class Source + { + public int Value { get; set; } + } + public class SourceA : Source + { + public string A { get; set; } + } + public class SourceB : Source + { + public string B { get; set; } + } + + public class Dest + { + public Dest() + { + + } + public Dest(int other) + { + Other = other; + } + + public int Value { get; set; } + [IgnoreMap] + public int Other { get; set; } + } + public class DestA : Dest + { + public DestA() : base() + { + + } + public DestA(int other, string otherA) : base(other) + { + OtherA = otherA; + } + [IgnoreMap] + public string OtherA { get; set; } + } + public class DestB : Dest + { + public DestB() : base() + { + + } + public DestB(int other, string otherB) : base(other) + { + OtherB = otherB; + } + [IgnoreMap] + public string OtherB { get; set; } + } + + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.AddIgnoreMapAttribute(); + cfg.CreateMap() + .ConstructUsing(src => new Dest(src.Value + 10)) + .Include() + .Include(); + + cfg.CreateMap() + .ConstructUsing(src => new DestA(src.Value + 10, src.A + "a")); + + cfg.CreateMap() + .ConstructUsing(src => new DestB(src.Value + 10, src.B + "b")); + }); + + protected override void Because_of() + { + var values = new[] + { + new Source() + { + Value = 5 + }, + new SourceA() + { + Value = 5, + A = "a" + }, + new SourceB() + { + Value = 5, + B = "b" + } + }.AsQueryable(); + + _dest = values.ProjectTo(Configuration).ToArray(); + } + + [Fact] + public void Should_construct_correctly() + { + _dest.All(p => p.Other == 15).ShouldBeTrue(); + _dest.OfType().Any(p => p.OtherA == "aa").ShouldBeTrue(); + _dest.OfType().Any(p => p.OtherB == "bb").ShouldBeTrue(); + } +} +public class NestedConstructorsWithIheritance : AutoMapperSpecBase { public class A { diff --git a/src/UnitTests/Projection/MapFromTest.cs b/src/UnitTests/Projection/MapFromTest.cs index be0ae0591e..7b618ef88f 100644 --- a/src/UnitTests/Projection/MapFromTest.cs +++ b/src/UnitTests/Projection/MapFromTest.cs @@ -11,7 +11,7 @@ public void Should_not_fail() .ForMember(dto => dto.FullName, opt => opt.MapFrom(src => src.LastName + " " + src.FirstName)); }); - typeof(NullReferenceException).ShouldNotBeThrownBy(() => config.Internal().ProjectionBuilder.GetMapExpression()); //null reference exception here + typeof(System.NullReferenceException).ShouldNotBeThrownBy(() => config.Internal().ProjectionBuilder.GetMapExpression()); //null reference exception here } [Fact] @@ -82,7 +82,7 @@ class Model } class InnerModel { - public InnerModel(string value) => Value = value ?? throw new ArgumentNullException(nameof(value)); + public InnerModel(string value) => Value = value ?? throw new System.ArgumentNullException(nameof(value)); private string Value { get; set; } } class Dto @@ -101,7 +101,7 @@ class Model } class InnerModel { - public InnerModel(string value) => SomeValue = value ?? throw new ArgumentNullException(nameof(value)); + public InnerModel(string value) => SomeValue = value ?? throw new System.ArgumentNullException(nameof(value)); private string SomeValue { get; set; } private string GetSomeValue() => SomeValue; } diff --git a/src/UnitTests/Projection/ProjectCollectionListTest.cs b/src/UnitTests/Projection/ProjectCollectionListTest.cs index c6521aa244..c9264d709d 100644 --- a/src/UnitTests/Projection/ProjectCollectionListTest.cs +++ b/src/UnitTests/Projection/ProjectCollectionListTest.cs @@ -87,3 +87,26 @@ public override bool Equals(object obj) } } } +public class MapProjection : AutoMapperSpecBase +{ + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateProjection(); + cfg.CreateMap(); + }); + [Fact] + public void ShouldNotMap() => new Action(() => Map(new Customer())).ShouldThrow().Message.ShouldBe("CreateProjection works with ProjectTo, not with Map."); + public class Customer + { + public IList
Addresses { get; set; } + } + public record class Address(string Street); + public class CustomerDto + { + public IList Addresses { get; set; } + } + public class AddressDto + { + public string Street { get; set; } + } +} \ No newline at end of file diff --git a/src/UnitTests/ReverseMapping.cs b/src/UnitTests/ReverseMapping.cs index 5eb946c964..5ef53f8e37 100644 --- a/src/UnitTests/ReverseMapping.cs +++ b/src/UnitTests/ReverseMapping.cs @@ -378,7 +378,7 @@ public class UnderscoreNamingConvention : INamingConvention public Regex SplittingExpression { get; } = new Regex(@"\p{Lu}[a-z0-9]*(?=_?)"); public string SeparatorCharacter => "_"; - public string[] Split(string input) => SplittingExpression.Matches(input).Select(m => m.Value).ToArray(); + public string[] Split(string input) => SplittingExpression.Matches(input).Cast().Select(m => m.Value).ToArray(); } protected override MapperConfiguration CreateConfiguration() => new(cfg => @@ -442,6 +442,13 @@ public void Should_create_a_map_with_the_reverse_items() { _source.Value.ShouldBe(10); } + + [Fact] + public void Should_not_initialize_details_on_initial_mapping() + { + var map = FindTypeMapFor(); + map.HasDetails.ShouldBeFalse(); + } } public class When_validating_only_against_source_members_and_source_matches : AutoMapperSpecBase @@ -673,4 +680,31 @@ public void Should_reverse_map_ok() source.Value.ShouldBe(1337); source.StringValue.ShouldBe("StringValue2"); } +} + +public class When_validating_reverse_mapping_classes_with_missing_properties : AutoMapperSpecBase +{ + public class Source + { + public int SomeValue { get; set; } + public int SomeValue2 { get; set; } + } + + public class Destination + { + public int SomeValue { get; set; } + } + + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap(MemberList.Destination) + .ReverseMap() + .ValidateMemberList(MemberList.Destination); + }); + + [Fact] + public void Should_throw_a_configuration_validation_error() + { + typeof(AutoMapperConfigurationException).ShouldBeThrownBy(AssertConfigurationIsValid); + } } \ No newline at end of file diff --git a/src/UnitTests/SeparateConfiguration.cs b/src/UnitTests/SeparateConfiguration.cs index f71e8d6783..08485c41e7 100644 --- a/src/UnitTests/SeparateConfiguration.cs +++ b/src/UnitTests/SeparateConfiguration.cs @@ -1,4 +1,6 @@ -namespace AutoMapper.UnitTests; +using Microsoft.Extensions.Logging.Abstractions; + +namespace AutoMapper.UnitTests; public class SeparateConfiguration : NonValidatingSpecBase { public class Source @@ -15,7 +17,7 @@ protected override MapperConfiguration CreateConfiguration() expr.CreateMap(); - return new MapperConfiguration(expr); + return new MapperConfiguration(expr, new NullLoggerFactory()); } [Fact] diff --git a/src/UnitTests/TypeConverters.cs b/src/UnitTests/TypeConverters.cs index d05f334c8c..c18e51895e 100644 --- a/src/UnitTests/TypeConverters.cs +++ b/src/UnitTests/TypeConverters.cs @@ -1,5 +1,26 @@ namespace AutoMapper.UnitTests.CustomMapping; - +public class StringToEnumConverter : AutoMapperSpecBase +{ + class Source + { + public string Enum { get; set; } + } + class Destination + { + public ConsoleColor Enum { get; set; } + } + protected override MapperConfiguration CreateConfiguration() => new(c => + { + c.CreateMap().ConvertUsing(s => ConsoleColor.DarkCyan); + c.CreateMap(); + }); + [Fact] + public void Should_work() + { + Map("").ShouldBe(ConsoleColor.DarkCyan); + Map(new Source()).Enum.ShouldBe(ConsoleColor.DarkCyan); + } +} public class NullableConverter : AutoMapperSpecBase { public enum GreekLetters diff --git a/src/UnitTests/ValueTypes.cs b/src/UnitTests/ValueTypes.cs index 83a6db6e41..54890c7eae 100644 --- a/src/UnitTests/ValueTypes.cs +++ b/src/UnitTests/ValueTypes.cs @@ -197,4 +197,12 @@ public void Should_still_map_value_type() { _destination.Value1.ShouldBe(10); } +} +public class ValueTypeDestinationPreserveReferences : AutoMapperSpecBase +{ + record Source(List List); + record struct Destination(List List); + protected override MapperConfiguration CreateConfiguration() => new(cfg => cfg.CreateMap()); + [Fact] + public void ShouldWork() => Map(new Source(new() { new Source(null) })).List.Single().List.ShouldBeEmpty(); } \ No newline at end of file