diff --git a/.github/prompts/csharp-docs.prompt.md b/.github/prompts/csharp-docs.prompt.md new file mode 100644 index 00000000..23687706 --- /dev/null +++ b/.github/prompts/csharp-docs.prompt.md @@ -0,0 +1,63 @@ +--- +agent: 'agent' +tools: ['changes', 'search/codebase', 'edit/editFiles', 'problems'] +description: 'Ensure that C# types are documented with XML comments and follow best practices for documentation.' +--- + +# C# Documentation Best Practices + +- Public members should be documented with XML comments. +- It is encouraged to document internal members as well, especially if they are complex or not self-explanatory. + +## Guidance for all APIs + +- Use `` to provide a brief, one sentence, description of what the type or member does. Start the summary with a present-tense, third-person verb. +- Use `` for additional information, which can include implementation details, usage notes, or any other relevant context. +- Use `` for language-specific keywords like `null`, `true`, `false`, `int`, `bool`, etc. +- Use `` for inline code snippets. +- Use `` for usage examples on how to use the member. + - Use `` for code blocks. `` tags should be placed within an `` tag. Add the language of the code example using the `language` attribute, for example, ``. +- Use `` to reference other types or members inline (in a sentence). +- Use `` for standalone (not in a sentence) references to other types or members in the "See also" section of the online docs. +- Use `` to inherit documentation from base classes or interfaces. + - Unless there is major behavior change, in which case you should document the differences. + +## Methods + +- Use `` to describe method parameters. + - The description should be a noun phrase that doesn't specify the data type. + - Begin with an introductory article. + - If the parameter is a flag enum, start the description with "A bitwise combination of the enumeration values that specifies...". + - If the parameter is a non-flag enum, start the description with "One of the enumeration values that specifies...". + - If the parameter is a Boolean, the wording should be of the form "`` to ...; otherwise, ``.". + - If the parameter is an "out" parameter, the wording should be of the form "When this method returns, contains .... This parameter is treated as uninitialized.". +- Use `` to reference parameter names in documentation. +- Use `` to describe type parameters in generic types or methods. +- Use `` to reference type parameters in documentation. +- Use `` to describe what the method returns. + - The description should be a noun phrase that doesn't specify the data type. + - Begin with an introductory article. + - If the return type is Boolean, the wording should be of the form "`` if ...; otherwise, ``.". + +## Constructors + +- The summary wording should be "Initializes a new instance of the class [or struct].". + +## Properties + +- The `` should start with: + - "Gets or sets..." for a read-write property. + - "Gets..." for a read-only property. + - "Gets [or sets] a value that indicates whether..." for properties that return a Boolean value. +- Use `` to describe the value of the property. + - The description should be a noun phrase that doesn't specify the data type. + - If the property has a default value, add it in a separate sentence, for example, "The default is ``". + - If the value type is Boolean, the wording should be of the form "`` if ...; otherwise, ``. The default is ...". + +## Exceptions + +- Use `` to document exceptions thrown by constructors, properties, indexers, methods, operators, and events. +- Document all exceptions thrown directly by the member. +- For exceptions thrown by nested members, document only the exceptions users are most likely to encounter. +- The description of the exception describes the condition under which it's thrown. + - Omit "Thrown if ..." or "If ..." at the beginning of the sentence. Just state the condition directly, for example "An error occurred when accessing a Message Queuing API." diff --git a/.github/workflows/build-deploy-docs.yml b/.github/workflows/build-deploy-docs.yml new file mode 100644 index 00000000..84bd2a3d --- /dev/null +++ b/.github/workflows/build-deploy-docs.yml @@ -0,0 +1,77 @@ +name: Build and Deploy DocFX + +on: + push: + branches: [ 'master' ] + paths: + - 'docs/**' + - 'README.md' + # Uncomment this if API docs changes should trigger a rebuild of the documentation + # - 'src/**' + - '.github/workflows/build-deploy-docs.yml' + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build-docs: + if: ${{ github.ref == 'refs/heads/master' }} + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 8.x + 9.x + # - name: Show dotnet version + # run: | + # dotnet --list-sdks + # dotnet --list-runtimes + + # - name: Restore dependencies before docs deployment as recommended by docfx + # run: dotnet restore ./src/Mapster.sln + + # - name: Build with dotnet + # run: dotnet build ./src/Mapster.sln + + # - name: Run tests on .NET 9.0 + # run: dotnet test --verbosity normal ./src/Mapster.sln + + - name: Install DocFX as .NET tool + run: | + dotnet tool update -g docfx + + - name: Build docfx site + working-directory: docs + run: docfx docfx.json + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + # Upload entire repository + path: 'docs/_site' + + deploy-docs: + needs: build-docs + if: ${{ needs.build-docs.result == 'success' && github.ref == 'refs/heads/master' }} + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/conventional-commits.yml b/.github/workflows/conventional-commits.yml new file mode 100644 index 00000000..77a0a8c8 --- /dev/null +++ b/.github/workflows/conventional-commits.yml @@ -0,0 +1,21 @@ +name: Conventional Commits + +on: + pull_request: + branches: + - master + - development + - release/* + +env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + +jobs: + commitsar: + name: Validate for conventional commits + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - name: Run commitsar + uses: aevea/commitsar@v0.20.2 diff --git a/.github/workflows/dotnet-buildandtest.yml b/.github/workflows/dotnet-buildandtest.yml index efad3f06..32648a71 100644 --- a/.github/workflows/dotnet-buildandtest.yml +++ b/.github/workflows/dotnet-buildandtest.yml @@ -12,16 +12,19 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup .NET - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: - dotnet-version: 7.0.x + dotnet-version: | + 8.x + 9.x + 10.x - name: Show dotnet version run: | dotnet --list-sdks dotnet --list-runtimes - name: Build with dotnet run: dotnet build ./src/Mapster.sln - - name: Run tests on .NET 7.0 + - name: Run tests on .NET 10.0 run: dotnet test --verbosity normal ./src/Mapster.sln diff --git a/.gitignore b/.gitignore index 83a5664f..434416f4 100644 --- a/.gitignore +++ b/.gitignore @@ -131,6 +131,9 @@ packages/ # This line needs to be after the ignore of the build folder (and the packages folder if the line above has been uncommented) !packages/build/ +# Not ignore packages folder in docs folder +!docs/articles/packages/ + # Windows Azure Build Output csx/ *.build.csdef @@ -181,3 +184,7 @@ project.lock.json /src/.vs /.vs src/.idea + +# VS Code settings +.vscode/launch.json +.vscode/tasks.json diff --git a/.markdownlint.jsonc b/.markdownlint.jsonc new file mode 100644 index 00000000..17ee9dd2 --- /dev/null +++ b/.markdownlint.jsonc @@ -0,0 +1,16 @@ +{ + "$schema": "https://raw.githubusercontent.com/DavidAnson/markdownlint/v0.38.0/schema/markdownlint-config-schema.json", + "default": true, + "line-length": false, + "commands-show-output": false, + "no-bare-urls": false, + "no-inline-html": false, + "no-duplicate-heading": false, + "no-emphasis-as-heading": false, + // Headers must start at the beginning of the line - false positive in some cases where it makes sense. + "MD023": false, + // First line in a file should be a top-level heading - false positive for include files. + "MD041": false, + // Link fragments should be valid - false positive for docfx tabs + "MD051": false +} \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000..a22d4740 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,8 @@ +{ + "recommendations": [ + "DavidAnson.vscode-markdownlint", + "streetsidesoftware.code-spell-checker-cspell-bundled-dictionaries", + "ms-vscode.powershell", + "joshbolduc.commitlint" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..781f0a8d --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "files.associations": { + "*.agent.md": "chatagent", + "*.instructions.md": "instructions", + "*.prompt.md": "prompt", + "docfx.json": "jsonc" + } +} \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..b8db01f2 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,36 @@ +![Mapster Icon](https://raw.githubusercontent.com/MapsterMapper/Mapster/master/docs/images/mapster-logo.svg) + +# Contributing to Mapster + +Thank you for your interest in contributing! We welcome contributions from the community. + +## How to Contribute + +1. **Fork the repository** and create your branch from [`development`](https://github.com/MapsterMapper/Mapster/tree/development) +2. **Make your changes** following the existing code style +3. **Write tests** using [MSTest](https://docs.microsoft.com/en-us/dotnet/core/testing/unit-testing-with-mstest) and [xUnit](https://xunit.net/) to ensure your changes work correctly +4. **Document your code** with XML comments and update [docs/articles/](./docs/articles/) if needed following the [DocFX](https://dotnet.github.io/docfx/) guidelines +5. **Commit with clear messages** following [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) (e.g., `feat:`, `fix:`, `docs:`) +6. **Submit a pull request** with a description of your changes + +## Reporting Issues + +Found a bug or have a feature request? Please [open an issue](https://github.com/MapsterMapper/Mapster/issues) with: + +- Clear description of the problem or request +- Steps to reproduce (for bugs) +- Code samples if applicable +- Environment details (Mapster version, .NET version) + +For questions, use [GitHub Discussions](https://github.com/MapsterMapper/Mapster/discussions). + +## Development Guidelines + +- Follow existing code conventions +- Add XML documentation for public APIs +- Write unit tests for new features and bug fixes +- Keep code clean, well-documented, and tested + +## License + +By contributing, you agree that your contributions will be licensed under the [MIT License](LICENSE). diff --git a/README.md b/README.md index 9be17bb0..208e2074 100644 --- a/README.md +++ b/README.md @@ -1,42 +1,77 @@ -![Mapster Icon](https://cloud.githubusercontent.com/assets/5763993/26522718/d16f3e42-4330-11e7-9b78-f8c7402624e7.png) +![Mapster Icon](https://raw.githubusercontent.com/MapsterMapper/Mapster/master/docs/images/mapster-logo.svg) + +# Mapster - The Mapper of Your Domain -## Mapster - The Mapper of Your Domain Writing mapping methods is a machine job. Do not waste your time, let Mapster do it. -[![NuGet](https://img.shields.io/nuget/v/Mapster.svg)](https://www.nuget.org/packages/Mapster) +## NuGet Sources + +### NuGet Packages + +| Package | Stable | Pre-release | +|---------|--------|-------------| +| **Mapster** | [![Mapster](https://img.shields.io/nuget/v/Mapster.svg?label=Mapster&color=informational)](https://www.nuget.org/packages/Mapster/latest) | [![Mapster](https://img.shields.io/nuget/vpre/Mapster.svg?label=Mapster&color=orange)](https://www.nuget.org/packages/Mapster) | +| **Mapster.Core** | [![Mapster.Core](https://img.shields.io/nuget/v/Mapster.Core.svg?label=Mapster.Core&color=informational)](https://www.nuget.org/packages/Mapster.Core/latest) | [![Mapster.Core](https://img.shields.io/nuget/vpre/Mapster.Core.svg?label=Mapster.Core&color=orange)](https://www.nuget.org/packages/Mapster.Core) | +| **Mapster.DependencyInjection** | [![Mapster.DependencyInjection](https://img.shields.io/nuget/v/Mapster.DependencyInjection.svg?label=Mapster.DependencyInjection&color=informational)](https://www.nuget.org/packages/Mapster.DependencyInjection/latest) | [![Mapster.DependencyInjection](https://img.shields.io/nuget/vpre/Mapster.DependencyInjection.svg?label=Mapster.DependencyInjection&color=orange)](https://www.nuget.org/packages/Mapster.DependencyInjection) | +| **Mapster.EFCore** | [![Mapster.EFCore](https://img.shields.io/nuget/v/Mapster.EFCore.svg?label=Mapster.EFCore&color=informational)](https://www.nuget.org/packages/Mapster.EFCore/latest) | [![Mapster.EFCore](https://img.shields.io/nuget/vpre/Mapster.EFCore.svg?label=Mapster.EFCore&color=orange)](https://www.nuget.org/packages/Mapster.EFCore) | +| **Mapster.EF6** | [![Mapster.EF6](https://img.shields.io/nuget/v/Mapster.EF6.svg?label=Mapster.EF6&color=informational)](https://www.nuget.org/packages/Mapster.EF6/latest) | [![Mapster.EF6](https://img.shields.io/nuget/vpre/Mapster.EF6.svg?label=Mapster.EF6&color=orange)](https://www.nuget.org/packages/Mapster.EF6) | +| **Mapster.JsonNet** | [![Mapster.JsonNet](https://img.shields.io/nuget/v/Mapster.JsonNet.svg?label=Mapster.JsonNet&color=informational)](https://www.nuget.org/packages/Mapster.JsonNet/latest) | [![Mapster.JsonNet](https://img.shields.io/nuget/vpre/Mapster.JsonNet.svg?label=Mapster.JsonNet&color=orange)](https://www.nuget.org/packages/Mapster.JsonNet) | +| **Mapster.Immutable** | [![Mapster.Immutable](https://img.shields.io/nuget/v/Mapster.Immutable.svg?label=Mapster.Immutable&color=informational)](https://www.nuget.org/packages/Mapster.Immutable/latest) | [![Mapster.Immutable](https://img.shields.io/nuget/vpre/Mapster.Immutable.svg?label=Mapster.Immutable&color=orange)](https://www.nuget.org/packages/Mapster.Immutable) | +| **Mapster.Diagnostics** | [![Mapster.Diagnostics](https://img.shields.io/nuget/v/Mapster.Diagnostics.svg?label=Mapster.Diagnostics&color=informational)](https://www.nuget.org/packages/Mapster.Diagnostics/latest) | | +| **ExpressionDebugger** | [![ExpressionDebugger](https://img.shields.io/nuget/v/ExpressionDebugger.svg?label=ExpressionDebugger&color=informational)](https://www.nuget.org/packages/ExpressionDebugger/latest) | | + +### DotNet Tools + +| Tool | Stable | Pre-release | +|------|--------|-------------| +| **Mapster.Tool** | [![Mapster.Tool](https://img.shields.io/nuget/v/Mapster.Tool.svg?label=Mapster.Tool&color=informational)](https://www.nuget.org/packages/Mapster.Tool/latest) | [![Mapster.Tool](https://img.shields.io/nuget/vpre/Mapster.Tool.svg?label=Mapster.Tool&color=orange)](https://www.nuget.org/packages/Mapster.Tool) | + +_Badges zeigen die jeweils aktuellste Stable-Version und die aktuellste Pre-Release-Version._ + +## Installation -### Installation Install Mapster with the NuGet CLI: -``` + +```Powershell Install-Package Mapster ``` Or use the .NET core CLI to install Mapster: + +```bash +dotnet add package Mapster --project ``` -dotnet add package Mapster -``` -### Basic usage -#### Mapping to a new object +## Basic usage + +### Mapping to a new object + Mapster creates the destination object and maps values to it. ```csharp var destObject = sourceObject.Adapt(); ``` -#### Mapping to an existing object +### Mapping to an existing object + You create the object, Mapster maps to the object. ```csharp sourceObject.Adapt(destObject); ``` -#### You can get IMapper instance via dependency injection so you do not have to change code when migrating to mapster from automapper -Add Mapster to service collection +### Use Mapster with Dependency Injection + +You can get your `IMapper` instance via dependency injection, so you do not have to change code, when migrating to mapster from automapper! + +**Just add Mapster to service collection:** + ```csharp services.AddMapster(); ``` -And use it with DI + +**And use it with DI in your Project:** + ```csharp public class Test { @@ -47,7 +82,8 @@ public class Test } ``` -#### Queryable Extensions +### Queryable Extensions + Mapster also provides extensions to map queryables. ```csharp @@ -67,7 +103,8 @@ using (MyDbContext context = new MyDbContext()) } ``` -#### Generating models & mappers +### Generating models & mappers + No need to write your own DTO classes. Mapster provides [Mapster.Tool](https://github.com/MapsterMapper/Mapster/wiki/Mapster.Tool) to help you generating models. And if you would like to have explicit mapping, Mapster also generates mapper class for you. ```csharp @@ -77,7 +114,8 @@ public class Student { } ``` -Then Mapster will generate: +Then Mapster will generate for you: + ```csharp public class StudentDto { ... @@ -89,7 +127,8 @@ public static class StudentMapper { } ``` -### What's new +## What's new + - [Fluent API for code generation](https://github.com/MapsterMapper/Mapster/wiki/Fluent-API-Code-generation) - [Automatically generate mapping code on build](https://github.com/MapsterMapper/Mapster/wiki/Mapster.Tool) - [Define setting to nested mapping](https://github.com/MapsterMapper/Mapster/wiki/Config-for-nested-mapping) @@ -99,10 +138,13 @@ public static class StudentMapper { - New plugins - [Immutable collection support](https://github.com/MapsterMapper/Mapster/wiki/Immutable) -### Why Mapster? -#### Performance & Memory efficient +## Why Mapster? + +### Performance & Memory efficient + Mapster was designed to be efficient on both speed and memory. You could gain a 4x performance improvement whilst using only 1/3 of memory. -And you could gain up to 12x faster performance with +And you could gain up to 12x faster performance with: + - [Roslyn Compiler](https://github.com/MapsterMapper/Mapster/wiki/Debugging) - [FEC](https://github.com/MapsterMapper/Mapster/wiki/FastExpressionCompiler) - Code generation @@ -116,35 +158,38 @@ And you could gain up to 12x faster performance with | 'ExpressMapper 1.9.1' | 205.78 ms | 5.357 ms | 8.098 ms | 59000.0000 | - | - | 236.51 MB | | 'AutoMapper 10.0.0' | 420.97 ms | 23.266 ms | 35.174 ms | 87000.0000 | - | - | 350.95 MB | +### Step into debugging +[Step-into debugging](https://mapstermapper.github.io/docs/packages/Diagnostics.html) lets you debug your mapping and inspect values just like your code. -#### Step into debugging +![inspect-generated-source-code-while-debugging](https://cloud.githubusercontent.com/assets/5763993/26521773/180427b6-431b-11e7-9188-10c01fa5ba5c.png) -[Step-into debugging](https://github.com/MapsterMapper/Mapster/wiki/Debugging) lets you debug your mapping and inspect values just like your code. -![image](https://cloud.githubusercontent.com/assets/5763993/26521773/180427b6-431b-11e7-9188-10c01fa5ba5c.png) +### Code Generation -#### Code Generation Code generation allows you to + - Validate mapping at compile time - Getting raw performance - Seeing your mapping code & debugging - Finding usage of your models' properties There are currently two tools which you can choose based on your preferences. -* [Mapster.Tool](https://github.com/MapsterMapper/Mapster/wiki/Mapster.Tool) NEW! -* [TextTemplate](https://github.com/MapsterMapper/Mapster/wiki/TextTemplate) -### Change logs +- [Mapster.Tool](https://github.com/MapsterMapper/Mapster/wiki/Mapster.Tool) _**NEW!**_ +- [TextTemplate](https://github.com/MapsterMapper/Mapster/wiki/TextTemplate) + +## Change logs + https://github.com/MapsterMapper/Mapster/releases -### Usages -* [English](https://github.com/MapsterMapper/Mapster/wiki) -* [中文文档](https://github.com/rivenfx/Mapster-docs) (sp thx to [@staneee](https://github.com/staneee)) +## Usage Guides with Translations + +- [English](https://github.com/MapsterMapper/Mapster/wiki) +- [中文文档](https://github.com/rivenfx/Mapster-docs) (sp thx to [@staneee](https://github.com/staneee)) -### Acknowledgements +## Acknowledgements [JetBrains](https://www.jetbrains.com/?from=Mapster) kindly provides Mapster with a free open-source licence for their Resharper and Rider. + - **Resharper** makes Visual Studio a much better IDE - **Rider** is fast & powerful cross platform .NET IDE - -![image](https://upload.wikimedia.org/wikipedia/commons/thumb/1/1a/JetBrains_Logo_2016.svg/121px-JetBrains_Logo_2016.svg.png) diff --git a/cSpell.json b/cSpell.json new file mode 100644 index 00000000..23a431b2 --- /dev/null +++ b/cSpell.json @@ -0,0 +1,26 @@ +{ + "version": "0.2", + "language": "en", + "words": [ + "alertblocks", + "appsettings", + "definitionlists", + "docfx", + "dotnet", + "github", + "gridtables", + "markdig", + "medialinks", + "MVVM" + ], + "dictionaries": [ + "csharp", + "companies" + ], + "ignoreWords": [], + "ignorePaths": [ + "src/*", + "**.gitignore", + "LICENSE" + ] +} diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 00000000..c91848d7 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,2 @@ +_site/* +!docs/articles/**.{md,yml} diff --git a/docs/.nojekyll b/docs/.nojekyll new file mode 100644 index 00000000..e69de29b diff --git a/docs/Build-Docs.ps1 b/docs/Build-Docs.ps1 new file mode 100644 index 00000000..e6bc559c --- /dev/null +++ b/docs/Build-Docs.ps1 @@ -0,0 +1,45 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS + Builds DocFX documentation with clean API regeneration. + +.DESCRIPTION + 1. Cleans obsolete API documentation files + 2. Regenerates metadata from current source code + 3. Builds the documentation site + +.EXAMPLE + .\Build-Docs.ps1 + .\Build-Docs.ps1 -Verbose + .\Build-Docs.ps1 -serve $true -openBrowser $true +#> + +[CmdletBinding()] +param( + [Parameter()] + [ValidateSet('Quiet', 'Info', 'Warning', 'Error', 'Verbose')] + [string]$LogLevel = 'Warning', + [bool]$serve = $false, + [bool]$openBrowser = $false +) + +Set-Location $PSScriptRoot + +# Step 1: Clean obsolete API docs +Write-Information "Cleaning obsolete API documentation..." -InformationAction Continue +& .\Clean-ApiDocs.ps1 + +# Step 2: Run DocFX (metadata + build + pdf in one command) +Write-Information "Running DocFX..." -InformationAction Continue +docfx docfx.json --logLevel $LogLevel --serve:$serve --open-browser:$openBrowser + +if ($LASTEXITCODE -ne 0) { + Write-Error "DocFX build failed" + exit $LASTEXITCODE +} + +Write-Host "✓ Documentation built successfully!" -ForegroundColor Green +Write-Debug "Output: $PSScriptRoot\_site" + +# Step 3: Reset to original location +Pop-Location \ No newline at end of file diff --git a/docs/Clean-ApiDocs.ps1 b/docs/Clean-ApiDocs.ps1 new file mode 100644 index 00000000..a60065cc --- /dev/null +++ b/docs/Clean-ApiDocs.ps1 @@ -0,0 +1,45 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS + Cleans obsolete API documentation files before regenerating with DocFX. + +.DESCRIPTION + Removes all generated YAML files from the api/ directory and the _site/ + output folder to ensure only current types are documented and built. + Preserves any manually-created markdown files like api/index.md. + +.EXAMPLE + .\Clean-ApiDocs.ps1 + .\Clean-ApiDocs.ps1 -Verbose +#> + +[CmdletBinding()] +param() + +$apiPath = Join-Path $PSScriptRoot "api" +$sitePath = Join-Path $PSScriptRoot "_site" +$isVerboseOutput = $PSBoundParameters.ContainsKey('Verbose').Equals($true) + +# Step 1: Clean API folder +if (Test-Path $apiPath) { + Write-Information "Cleaning API documentation folder..." -InformationAction Continue + + # Remove all .yml files (generated API docs) + Get-ChildItem -Path $apiPath -Filter "*.yml" -File | Remove-Item -Force -Verbose:$isVerboseOutput + + Write-Host "✓ Cleaned obsolete API documentation files" -ForegroundColor Green +} else { + Write-Debug "API folder does not exist yet: $apiPath" +} + +# Step 2: Clean _site folder +if (Test-Path $sitePath) { + Write-Information "Cleaning output site folder..." -InformationAction Continue + Remove-Item -Path $sitePath -Recurse -Force -Verbose:$isVerboseOutput + Write-Host "✓ Cleaned output site folder" -ForegroundColor Green +} else { + Write-Debug "Output site folder does not exist yet: $sitePath" +} + +# Step 3: Reset to original location +Pop-Location \ No newline at end of file diff --git a/docs/Clean-and-Build-Docs.ps1 b/docs/Clean-and-Build-Docs.ps1 new file mode 100644 index 00000000..53db82ff --- /dev/null +++ b/docs/Clean-and-Build-Docs.ps1 @@ -0,0 +1,44 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS + Full documentation build: Clean, regenerate, and build. + +.DESCRIPTION + 1. Cleans obsolete API documentation files (Clean-ApiDocs.ps1) + 2. Builds documentation using Build-Docs.ps1 + +.EXAMPLE + .\Build-Docs-Full.ps1 + .\Build-Docs-Full.ps1 -Verbose + .\Build-Docs-Full.ps1 -serve $true -openBrowser $true +#> + +[CmdletBinding()] +param( + [Parameter()] + [ValidateSet('Quiet', 'Info', 'Warning', 'Error', 'Verbose')] + [string]$LogLevel = 'Warning' +) + +# Step 1: Set the current location +Set-Location $PSScriptRoot + +# Step 2: Clean obsolete API docs +Write-Information "Cleaning obsolete API documentation..." -InformationAction Continue +& .\Clean-ApiDocs.ps1 + +# Step 3: Build documentation +Write-Information "Building documentation..." -InformationAction Continue + +& .\Build-Docs.ps1 -LogLevel $LogLevel -serve $true -open-browser $true + +if ($LASTEXITCODE -ne 0) { + Write-Error "Documentation build failed" + exit $LASTEXITCODE +} + +Write-Information "✓ Full documentation build completed successfully!" -InformationAction Continue +Write-Debug "Output: $PSScriptRoot\_site" + +# Step 4: Reset to original location +Pop-Location \ No newline at end of file diff --git a/docs/api/.gitignore b/docs/api/.gitignore new file mode 100644 index 00000000..13609968 --- /dev/null +++ b/docs/api/.gitignore @@ -0,0 +1,2 @@ +*.yml +.manifest diff --git a/docs/api/Reference.md b/docs/api/Reference.md new file mode 100644 index 00000000..9626ce24 --- /dev/null +++ b/docs/api/Reference.md @@ -0,0 +1,139 @@ +--- +uid: Mapster.References +--- + +# References + +## Basic + +| Method | Description | Link | +| -------------|-----------------------| ----- | +| `src.Adapt()` | Mapping to new type | [basic](xref:Mapster.Mapping.BasicUsages) | +| `src.Adapt(dest)` | Mapping to existing object | [basic](xref:Mapster.Mapping.BasicUsages) | +| `query.ProjectToType()` | Mapping from queryable | [basic](xref:Mapster.Mapping.BasicUsages) | +| | Convention & Data type support | [data types](xref:Mapster.Mapping.DataTypes) | + +### Mapper instance (for dependency injection) + +| Method | Description | Link | +| -------------|----------------------| ----- | +| `IMapper mapper = new Mapper()`| Create mapper instance | [mappers](xref:Mapster.Mapping.Mappers) | +| `mapper.Map(src)` | Mapping to new type | | +| `mapper.Map(src, dest)` | Mapping to existing object | | + +### Builder (for complex mapping) + +| Method | Description | Link | +| ------------- |-----------------------| ----- | +| `src.BuildAdapter()`
`mapper.From(src)` | Create builder | [mappers](xref:Mapster.Mapping.Mappers) | +| `.ForkConfig(config => ...)` | Inline configuration | [config location](xref:Mapster.Configuration.Location) | +| `.AddParameters(name, value)` | Passing runtime value | [setting values](xref:Mapster.Settings.SettingValues) | +| `.AdaptToType()` | Mapping to new type | | +| `.AdaptTo(dest)` | Mapping to existing object | | +| `.CreateMapExpression()` | Get mapping expression | | +| `.CreateMapToTargetExpression()` | Get mapping to existing object expression | | +| `.CreateProjectionExpression()` | Get mapping from queryable expression | | + +## Config + +| Method | Description | Link | +|----------------------------------------------|-----------------------------------------| ----- | +| `TypeAdapterConfig.GlobalSettings` | Global config | [config](xref:Mapster.Configuration.Overview) | +| `var config = new TypeAdapterConfig()` | Create new config instance | [config instance](xref:Mapster.Configuration.Instance) | +| `src.Adapt(config)` | Passing config to mapping | | +| `new Mapper(config)` | Passing config to mapper instance | | +| `src.BuildAdapter(config)` | Passing config to builder | | +| `config.RequireDestinationMemberSource` | Validate all properties are mapped | [config validation](xref:Mapster.Configuration.ValidationAndCompilation) | +| `config.RequireExplicitMapping` | Validate all type pairs are defined | [config validation](xref:Mapster.Configuration.ValidationAndCompilation) | +| `config.AllowImplicitDestinationInheritance` | Use config from destination based class | [inheritance](xref:Mapster.Configuration.Inheritance) | +| `config.AllowImplicitSourceInheritance` | Use config from source based class | [inheritance](xref:Mapster.Configuration.Inheritance) | +| `config.SelfContainedCodeGeneration` | Generate all nested mapping in 1 method | [TextTemplate](xref:Mapster.Tools.TextTemplate) | +| `config.Compile()` | Validate mapping instruction & cache | [config validation](xref:Mapster.Configuration.ValidationAndCompilation) | +| `config.CompileProjection()` | Validate mapping instruction & cache for queryable | | +| `config.Clone()` | Copy config | [config instance](xref:Mapster.Configuration.Instance) | +| `config.Fork(forked => ...)` | Inline configuration | [config location](xref:Mapster.Configuration.Location) | + +### Config scanning + +| Method | Description | Link | +|------------------------------|-------------------------------| ----- | +| `IRegister` | Interface for config scanning | [config location](xref:Mapster.Configuration.Location) | +| `config.Scan(...assemblies)` | Scan for config in assemblies | [config location](xref:Mapster.Configuration.Location) | +| `config.Apply(...registers)` | Apply registers directly | [config location](xref:Mapster.Configuration.Location) | + + +### Declare settings + +| Method | Description | Link | +| ------------- |-----------------------| ----- | +| `config.Default` | Get setting applied to all type pairs | [config](xref:Mapster.Configuration.Overview) | +| `TypeAdapterConfig.NewConfig()`
`config.NewConfig()` | Create setting applied to specific type pairs | [config](xref:Mapster.Configuration.Overview) | +| `TypeAdapterConfig.ForType()`
`config.ForType()` | Get setting applied to specific type pairs | [config](xref:Mapster.Configuration.Overview) | +| `config.ForType(typeof(GenericPoco<>),typeof(GenericDto<>))` | Get setting applied to generic type pairs | [config](xref:Mapster.Configuration.Overview) | +| `config.When((src, dest, mapType) => ...)` | Get setting that applied conditionally | [config](xref:Mapster.Configuration.Overview) | +| `config.ForDestinationType()` | Get setting that applied to specific destination type | [config](xref:Mapster.Configuration.Overview) | +| | Configuration for nested mapping | [nested mapping](xref:Mapster.Configuration.NestedMapping) | + +## Settings + +| Method | Description | Apply to queryable | Link | +| ------------- |-----------------------| ------------ | ----- | +| `AddDestinationTransform` | Clean up data for a specific type | x | [setting values](xref:Mapster.Settings.SettingValues) | +| `AfterMapping` | Add steps after mapping done | | [before-after](xref:Mapster.Settings.BeforeAfterMapping) | +| `AvoidInlineMapping` | Skip inline process for large type mapping | | [object reference](xref:Mapster.Settings.ObjectReferences) | +| `BeforeMapping` | Add steps before mapping start | | [before-after](xref:Mapster.Settings.BeforeAfterMapping) | +| `ConstructUsing` | Define how to create object | x | [constructor](xref:Mapster.Settings.ConstructorMapping) | +| `EnableNonPublicMembers` | Mapping non-public properties | | [non-public](xref:Mapster.Settings.Custom.NonPublicMembers) | +| `EnumMappingStrategy` | Choose whether mapping enum by value or by name | | [data types](xref:Mapster.Mapping.DataTypes) | +| `Fork` | Add new settings without side effect on main config | x | [nested mapping](xref:Mapster.Configuration.NestedMapping) | +| `GetMemberName` | Define how to resolve property name | x | [custom naming](xref:Mapster.Settings.Custom.NamingConvention) | +| `Ignore` | Ignore specific properties | x | [ignore](xref:Mapster.Settings.Custom.IgnoringMembers) | +| `IgnoreAttribute` | Ignore specific attributes annotated on properties | x | [attribute](xref:Mapster.Settings.Custom.Attributes) | +| `IgnoreIf` | Ignore conditionally | x | [ignore](xref:Mapster.Settings.Custom.IgnoringMembers) | +| `IgnoreMember` | Setup rules to ignore | x | [rule based](xref:Mapster.Settings.Custom.RuleBasedMapping) | +| `IgnoreNonMapped` | Ignore all properties not defined in `Map` | x | [ignore](xref:Mapster.Settings.Custom.IgnoringMembers) | +| `IgnoreNullValues` | Not map if src property is null | | [shallow & merge](xref:Mapster.Settings.ShallowMerge) | +| `Include` | Include derived types on mapping | | [inheritance](xref:Mapster.Configuration.Inheritance) | +| `IncludeAttribute` | Include specific attributes annotated on properties | x | [attribute](xref:Mapster.Settings.Custom.Attributes) | +| `IncludeMember` | Setup rules to include | x | [rule based](xref:Mapster.Settings.Custom.RuleBasedMapping) | +| `Inherits` | Copy setting from based type | x | [inheritance](xref:Mapster.Configuration.Inheritance) | +| `Map` | Define property pairs | x | [custom mapping](xref:Mapster.Settings.Custom.Mapping) | +| `MapToConstructor` | Mapping to constructor | x | [constructor](xref:Mapster.Settings.ConstructorMapping) | +| `MapToTargetWith` | Define how to map to existing object between type pair | | [custom conversion](xref:Mapster.Settings.CustomConversionLogic) | +| `MapWith` | Define how to map between type pair | x | [custom conversion](xref:Mapster.Settings.CustomConversionLogic) | +| `MaxDepth` | Limit depth of nested mapping | x | [object reference](xref:Mapster.Settings.ObjectReferences) | +| `NameMatchingStrategy` | Define how to resolve property's name | x | [custom naming](xref:Mapster.Settings.Custom.NamingConvention) | +| `PreserveReference` | Tracking reference when mapping | | [object reference](xref:Mapster.Settings.ObjectReferences) | +| `ShallowCopyForSameType` | Direct assign rather than deep clone if type pairs are the same | | [shallow & merge](xref:Mapster.Settings.ShallowMerge) | +| `TwoWays` | Define type mapping are 2 ways | x | [2-ways & unflattening](xref:Mapster.Settings.Custom.TwoWaysMapping) | +| `Unflattening` | Allow unflatten mapping | x |[2-ways & unflattening](xref:Mapster.Settings.Custom.TwoWaysMapping) | +| `UseDestinationValue` | Use existing property object to map data | |[readonly-prop](xref:Mapster.Settings.Custom.ReadonlyProperty) | + +## Attributes + +| Annotation | Description | Link | +| ------------- |-----------------------| ----- | +| `[AdaptMember(name)]` | Mapping property to different name | [attribute](xref:Mapster.Settings.Custom.Attributes) | +| `[AdaptIgnore(side)]` | Ignore property from mapping | [attribute](xref:Mapster.Settings.Custom.Attributes) | +| `[UseDestinationValue]` | Use existing property object to map data | [attribute](xref:Mapster.Settings.Custom.Attributes) | +| `[AdaptTo]` `[AdaptFrom]` `[AdaptTwoWays]` | Add setting on POCO class | [location](xref:Mapster.Configuration.Location#attributes) | +| `[Mapper]` `[GenerateMapper]` `[PropertyType]` | Define setting for code generation | [Mapster.Tool](xref:Mapster.Tools.MapsterTool.Overview) | + +## Packages + +| Packages | Method | Description | +| ------ | ------------- |-----------------------| +| [Async](xref:Mapster.Packages.Async) | `setting.AfterMappingAsync`
`builder.AdaptToTypeAsync` | perform async operation on mapping | +| [Debugging](xref:Mapster.Packages.ExpressionDebugging) | `config.Compiler = exp => exp.CompileWithDebugInfo()` | compile to allow step into debugging | +| [Dependency Injection](xref:Mapster.Packages.DependencyInjection) | `MapContext.Current.GetService()` | Inject service into mapping logic | +| [EF 6 & EF Core](xref:Mapster.Packages.EntityFramework) | `builder.EntityFromContext` | Copy data to tracked EF entity | +| [FEC](xref:Mapster.Packages.FastExpressionCompiler) | `config.Compiler = exp => exp.CompileFast()` | compile using FastExpressionCompiler | +| [Immutable](xref:Mapster.Packages.Immutable) | `config.EnableImmutableMapping()` | mapping to immutable collection | +| [Json.net](xref:Mapster.Packages.JsonNet) | `config.EnableJsonMapping()` | map json from/to poco and string | + +## Code Generation Tools + +| Plugin | Tool | Description | +| ------ | ------------- |-----------------------| +| [Mapster.Tool](xref:Mapster.Tools.MapsterTool.Overview) | `dotnet mapster` | generate DTOs and mapping codes on build | +| [TextTemplate](xref:Mapster.Tools.TextTemplate) | `t4` | generate mapping codes using t4 | diff --git a/docs/api/index.md b/docs/api/index.md new file mode 100644 index 00000000..96322154 --- /dev/null +++ b/docs/api/index.md @@ -0,0 +1,5 @@ +# MapsterMapper Mapster API Documentation + +Welcome in the Mapster API documentation! + +Use the table of contents to browse API documentation for the `MapsterMapper.Mapster` Package. diff --git a/docs/articles/.assets/step-into-debugging.png b/docs/articles/.assets/step-into-debugging.png new file mode 100644 index 00000000..5183ce54 Binary files /dev/null and b/docs/articles/.assets/step-into-debugging.png differ diff --git a/docs/articles/_Sidebar.md b/docs/articles/_Sidebar.md new file mode 100644 index 00000000..22c58675 --- /dev/null +++ b/docs/articles/_Sidebar.md @@ -0,0 +1,56 @@ +# Getting Started with Mapster + +## [References](https://github.com/MapsterMapper/Mapster/wiki) + +## Mapping + +* [Basic usages](https://github.com/MapsterMapper/Mapster/wiki/Basic-usages) +* [Mappers](https://github.com/MapsterMapper/Mapster/wiki/Mappers) +* [Data types](https://github.com/MapsterMapper/Mapster/wiki/Data-types) +* [Mapping with interface](https://github.com/MapsterMapper/Mapster/wiki/Mapping-Configuration-With-IMapFrom-Interface) + +## Configuration + +* [Configuration](https://github.com/MapsterMapper/Mapster/wiki/Configuration) +* [Config inheritance](https://github.com/MapsterMapper/Mapster/wiki/Config-inheritance) +* [Config instance](https://github.com/MapsterMapper/Mapster/wiki/Config-instance) +* [Config location](https://github.com/MapsterMapper/Mapster/wiki/Config-location) +* [Config validation & compilation](https://github.com/MapsterMapper/Mapster/wiki/Config-validation-&-compilation) +* [Config for nested mapping](https://github.com/MapsterMapper/Mapster/wiki/Config-for-nested-mapping) + +## Settings + +* [Constructor mapping](https://github.com/MapsterMapper/Mapster/wiki/Constructor-mapping) +* [Before & after mapping](https://github.com/MapsterMapper/Mapster/wiki/Before-after-mapping) +* [Setting values](https://github.com/MapsterMapper/Mapster/wiki/Setting-values) +* [Shallow & merge mapping](https://github.com/MapsterMapper/Mapster/wiki/Shallow-merge) +* [Recursive & object references](https://github.com/MapsterMapper/Mapster/wiki/Object-references) +* [Custom conversion logic](https://github.com/MapsterMapper/Mapster/wiki/Custom-conversion-logic) +* [Inheritance](https://github.com/MapsterMapper/Mapster/wiki/Config-inheritance) +* **Custom member matching logic** + * [Custom mapping](https://github.com/MapsterMapper/Mapster/wiki/Custom-mapping) + * [Custom naming convention](https://github.com/MapsterMapper/Mapster/wiki/Naming-convention) + * [Setting by attributes](https://github.com/MapsterMapper/Mapster/wiki/Setting-by-attributes) + * [Ignoring members](https://github.com/MapsterMapper/Mapster/wiki/Ignoring-members) + * [Rule-based member matching](https://github.com/MapsterMapper/Mapster/wiki/Rule-based-member-mapping) + * [Mapping readonly prop](https://github.com/MapsterMapper/Mapster/wiki/Mapping-readonly-prop) + * [Mapping non-public members](https://github.com/MapsterMapper/Mapster/wiki/Mapping-non-public-members) + * [Two ways & unflattening mapping](https://github.com/MapsterMapper/Mapster/wiki/Two-ways) + +## Plugins + +* [Async Support](https://github.com/MapsterMapper/Mapster/wiki/Async) +* [Debugging](https://github.com/MapsterMapper/Mapster/wiki/Debugging) +* [Dependency Injection](https://github.com/MapsterMapper/Mapster/wiki/Dependency-Injection) +* [EF 6 & EF Core](https://github.com/MapsterMapper/Mapster/wiki/EF-6-&-EF-Core) +* [FastExpressionCompiler](https://github.com/MapsterMapper/Mapster/wiki/FastExpressionCompiler) +* [Immutable](https://github.com/MapsterMapper/Mapster/wiki/Immutable) +* [Json.net](https://github.com/MapsterMapper/Mapster/wiki/Json.net) + +## Tools + +* [TextTemplate](https://github.com/MapsterMapper/Mapster/wiki/TextTemplate) +* [Mapster.Tool](https://github.com/MapsterMapper/Mapster/wiki/Mapster.Tool) + * [Fluent API](https://github.com/MapsterMapper/Mapster/wiki/Fluent-API-Code-generation) + * [Attributes](https://github.com/MapsterMapper/Mapster/wiki/Attribute-base-Code-generation) + * [Interfaces](https://github.com/MapsterMapper/Mapster/wiki/Interface-base-Code-generation) diff --git a/docs/articles/configuration/Config-for-nested-mapping.md b/docs/articles/configuration/Config-for-nested-mapping.md new file mode 100644 index 00000000..9416b503 --- /dev/null +++ b/docs/articles/configuration/Config-for-nested-mapping.md @@ -0,0 +1,72 @@ +--- +uid: Mapster.Configuration.NestedMapping +title: "Configuration - Config for nested mapping" +--- + +For example if you have parent and child classes. + +```csharp +class ParentPoco +{ + public string Id { get; set; } + public List Children { get; set; } + public string Name { get; set; } +} + +class ChildPoco +{ + public string Id { get; set; } + public List GrandChildren { get; set; } +} + +class GrandChildPoco +{ + public string Id { get; set; } +} +``` + +And if you have setting on parent type. + +```csharp +TypeAdapterConfig.NewConfig() + .PreserveReference(true); +``` + +By default, children types will not get effect from `PreserveReference`. + +To do so, you must specify all type pairs inside `ParentPoco`. + +```csharp +TypeAdapterConfig.NewConfig() + .PreserveReference(true); +TypeAdapterConfig.NewConfig() + .PreserveReference(true); +TypeAdapterConfig.NewConfig() + .PreserveReference(true); +``` + +Or you can set `PreserveReference` in global setting. + +```csharp +TypeAdapterConfig.GlobalSettings.Default.PreserveReference(true); +``` + +## Fork + +You can use `Fork` command to define config that applies only specified mapping down to nested mapping without polluting global setting. + +```csharp +TypeAdapterConfig.NewConfig() + .Fork(config => config.Default.PreserveReference(true)); +``` + +**Ignore if string is null or empty** + +Another example, Mapster only can ignore null value ([IgnoreNullValues](xref:Mapster.Settings.ShallowMerge#copy-vs-merge)), however, you use `Fork` to ignore null or empty. + +```csharp +TypeAdapterConfig.NewConfig() + .Fork(config => config.ForType() + .MapToTargetWith((src, dest) => string.IsNullOrEmpty(src) ? dest : src) + ); +``` diff --git a/docs/articles/configuration/Config-inheritance.md b/docs/articles/configuration/Config-inheritance.md new file mode 100644 index 00000000..edcc93f4 --- /dev/null +++ b/docs/articles/configuration/Config-inheritance.md @@ -0,0 +1,56 @@ +--- +uid: Mapster.Configuration.Inheritance +title: "Configuration - Inheritance" +--- + +## Implicit inheritance + +Type mappings will automatically inherit for source types. Ie. if you set up following config. + +```csharp +TypeAdapterConfig.NewConfig() + .Map(dest => dest.Name, src => src.Name + "_Suffix"); +``` + +A derived type of `SimplePoco` will automatically apply the base mapping config. + +```csharp +var dest = TypeAdapter.Adapt(src); +//dest.Name = src.Name + "_Suffix" +``` + +If you don't wish for a derived type to use the base mapping, you can turn it off by using `AllowImplicitSourceInheritance` + +```csharp +TypeAdapterConfig.GlobalSettings.AllowImplicitSourceInheritance = false; +``` + +And by default, Mapster will not inherit destination type mappings. You can turn it on by `AllowImplicitDestinationInheritance`. + +```csharp +TypeAdapterConfig.GlobalSettings.AllowImplicitDestinationInheritance = true; +``` + +## Explicit inheritance + +You can copy setting from based type explicitly. + +```csharp +TypeAdapterConfig.NewConfig() + .Inherits(); +``` + +## Include derived types + +You can also include derived type to the based type declaration. For example: + +```csharp +TypeAdapterConfig.NewConfig() + .Include(); + +Vehicle vehicle = new Car { Id = 1, Name = "Car", Make = "Toyota" }; +var dto = vehicle.Adapt(); + +dto.ShouldBeOfType(); +((CarDto)dto).Make.ShouldBe("Toyota"); //The 'Make' property doesn't exist in Vehicle +``` diff --git a/docs/articles/configuration/Config-instance.md b/docs/articles/configuration/Config-instance.md new file mode 100644 index 00000000..c82a1860 --- /dev/null +++ b/docs/articles/configuration/Config-instance.md @@ -0,0 +1,72 @@ +--- +uid: Mapster.Configuration.Instance +title: "Configuration - Config Instance" +--- + +## Creating `Config` Instance with `TypeAdapterConfig` + +You may wish to have different settings in different scenarios. +If you would not like to apply setting at a static level, Mapster also provides setting instance configurations. + +```csharp +var config = new TypeAdapterConfig(); +config.Default.Ignore("Id"); +``` + +### `ForType` Configuration Methods + +For instance configurations, you can use the same `NewConfig` and `ForType` methods that are used at the global level with +the same behavior: `NewConfig` drops any existing configuration and `ForType` creates or enhances a configuration. + +```csharp +config.NewConfig() + .Map(dest => dest.FullName, + src => string.Format("{0} {1}", src.FirstName, src.LastName)); + +config.ForType() + .Map(dest => dest.FullName, + src => string.Format("{0} {1}", src.FirstName, src.LastName)); +``` + +### `Adapt` Configuration Methods + +You can apply a specific config instance by passing it to the `Adapt` method. (NOTE: please reuse your config instance to prevent recompilation) + +```csharp +var result = src.Adapt(config); +``` + +### `Map` Configuration Methods + +Or to a Mapper instance. + +```csharp +var mapper = new Mapper(config); +var result = mapper.Map(src); +``` + +## Cloning `Config` Instance + +If you would like to create configuration instance from existing configuration, you can use `Clone` method. For example, if you would like to clone from global setting. + +```csharp +var newConfig = TypeAdapterConfig.GlobalSettings.Clone(); +``` + +Or clone from existing configuration instance + +```csharp +var newConfig = oldConfig.Clone(); +``` + +### `Fork` Configuration + +`Fork` is similar to `Clone`, but `Fork` will allow you to keep configuration and mapping in the same location. See [Config Location](xref:Mapster.Configuration.Location) for more info. + +```csharp +var forked = mainConfig.Fork(config => + config.ForType() + .Map(dest => dest.code, src => src.Id)); + +var dto = poco.Adapt(forked); +``` diff --git a/docs/articles/configuration/Config-location.md b/docs/articles/configuration/Config-location.md new file mode 100644 index 00000000..4ac27893 --- /dev/null +++ b/docs/articles/configuration/Config-location.md @@ -0,0 +1,117 @@ +--- +uid: Mapster.Configuration.Location +title: "Configuration - Location" +--- + +## Entry point for configuration + +Configuration should be set only once and reuse for mapping. Therefore, we should not keep configuration and mapping in the same location. For example: + +```csharp +config.ForType().Ignore("Id"); +var dto1 = poco1.Adapt(config); + +config.ForType().Ignore("Id"); //<--- Exception occurred here, because config was already compiled +var dto2 = poco2.Adapt(config); +``` + +Therefore, you should separate configuration and mapping. Configuration should keep in entry point such as `Main` function or `Global.asax.cs` or `Program.cs` / `Startup.cs`. + +```csharp +// Application_Start in Global.asax.cs +config.ForType().Ignore("Id"); +``` + +```csharp +// in Controller class +var dto1 = poco1.Adapt(config); +var dto2 = poco2.Adapt(config); +``` + +### Keep together with mapping + +A potential problem with separating configuration and mapping is that the code will be separated into 2 locations. You might remove or alter mapping, and you can forget to update the configuration. `Fork` method allow you to keep config and mapping inline. + +```csharp +var dto = poco.Adapt(config.Fork(forked => forked.ForType().Ignore("Id")); +``` + +Don't worry about performance, forked config will be compiled only once. When mapping occurs for the second time, `Fork` function will return config from cache. + +#### Using Fork in generic class or method + +`Fork` method uses filename and line number and the key. But if you use `Fork` method inside generic class or method, you must specify your own key (with all type names) to prevent `Fork` to return invalid config from different type arguments. + +```csharp +IQueryable GetItems() +{ + var forked = config.Fork( + f => f.ForType().Ignore("Id"), + $"MyKey|{typeof(TPoco).FullName}|{typeof(TDto).FullName}"); + return db.Set().ProjectToType(forked); +} +``` + +### In separated assemblies + +It's relatively common to have mapping configurations spread across a number of different assemblies. +Perhaps your domain assembly has some rules to map to domain objects and your web api has some specific rules to map to your api contracts. + +#### Scan method + +It can be helpful to allow assemblies to be scanned for these rules so you have some basic method of organizing your rules and not forgetting to have the registration code called. In some cases, it may even be necessary to register the assemblies in a particular order, so that some rules override others. Assembly scanning helps with this. + +Assembly scanning is simple, just create any number of `IRegister` implementations in your assembly, then call `Scan` from your `TypeAdapterConfig` class: + +```csharp +public class MyRegister : IRegister +{ + public void Register(TypeAdapterConfig config) + { + config.NewConfig(); + + //OR to create or enhance an existing configuration + config.ForType(); + } +} +``` + +To scan and register at the Global level: + +```csharp +TypeAdapterConfig.GlobalSettings.Scan(assembly1, assembly2, assemblyN) +``` + +For a specific config instance: + +```csharp +var config = new TypeAdapterConfig(); +config.Scan(assembly1, assembly2, assemblyN); +``` + +#### Apply method + +If you use other assembly scanning library such as MEF, you can easily apply registration with `Apply` method. + +```csharp +var registers = container.GetExports(); +config.Apply(registers); +``` + +`Apply` method also allows you to selectively pick from one or more `IRegister` rather than every `IRegister` in assembly. + +```csharp +var register = new MockingRegister(); +config.Apply(register); +``` + +### Attributes + +You can also set config together with your POCO classes. For example: + +```csharp +[AdaptTo(typeof(StudentDto), PreserveReference = true)] +public class Student { + ... +} +``` diff --git a/docs/articles/configuration/Config-validation-and-compilation.md b/docs/articles/configuration/Config-validation-and-compilation.md new file mode 100644 index 00000000..f3a06210 --- /dev/null +++ b/docs/articles/configuration/Config-validation-and-compilation.md @@ -0,0 +1,64 @@ +--- +uid: Mapster.Configuration.ValidationAndCompilation +title: "Configuration - Validation and Compilation" +--- + +To validate your mapping in unit tests and in order to help with "Fail Fast" situations, the following strict mapping modes have been added. + +## Explicit Mapping + +Forcing all classes to be explicitly mapped: + +```csharp +//Default is "false" +TypeAdapterConfig.GlobalSettings.RequireExplicitMapping = true; +//This means you have to have an explicit configuration for each class, even if it's just: +TypeAdapterConfig.NewConfig(); +``` + +## Checking Destination Member + +Forcing all destination properties to have a corresponding source member or explicit mapping/ignore: + +```csharp +//Default is "false" +TypeAdapterConfig.GlobalSettings.RequireDestinationMemberSource = true; +``` + +## Validating Mappings + +Both a specific TypeAdapterConfig or all current configurations can be validated. In addition, if Explicit Mappings (above) are enabled, it will also include errors for classes that are not registered at all with the mapper. + +```csharp +//Validate a specific config +var config = TypeAdapterConfig.NewConfig(); +config.Compile(); + +//Validate globally +TypeAdapterConfig.NewConfig(); +TypeAdapterConfig.NewConfig(); +TypeAdapterConfig.GlobalSettings.Compile(); +``` + +## Config Compilation + +Mapster will automatically compile mapping for first time usage. + +```csharp +var result = poco.Adapt(); +``` + +However, you can explicitly compile mapping by `Compile` method. + +```csharp +//Global config +TypeAdapterConfig.GlobalSettings.Compile(); + +//Config instance +var config = new TypeAdapterConfig(); +config.Compile(); +``` + +Calling `Compile` method on start up helps you validate mapping and detect problem on start-up time, not on run-time. + +NOTE: After compile, when you change setting in config, it will generate errors. Therefore, make sure you finish configuration before calling `Compile`. diff --git a/docs/articles/configuration/Configuration.md b/docs/articles/configuration/Configuration.md new file mode 100644 index 00000000..34a30549 --- /dev/null +++ b/docs/articles/configuration/Configuration.md @@ -0,0 +1,82 @@ +--- +uid: Mapster.Configuration.Overview +title: "Configuration - Overview" +--- + + +## Setting per type pair + +You can easily create settings for a type mapping by using: `TypeAdapterConfig.NewConfig()`. +When `NewConfig` is called, any previous configuration for this particular TSource => TDestination mapping is dropped. + +```csharp +TypeAdapterConfig + .NewConfig() + .Ignore(dest => dest.Age) + .Map(dest => dest.FullName, + src => string.Format("{0} {1}", src.FirstName, src.LastName)); +``` + +As an alternative to `NewConfig`, you can use `ForType` in the same way: + +```csharp +TypeAdapterConfig + .ForType() + .Ignore(dest => dest.Age) + .Map(dest => dest.FullName, + src => string.Format("{0} {1}", src.FirstName, src.LastName)); +``` + +`ForType` differs in that it will create a new mapping if one doesn't exist, but if the specified TSource => TDestination +mapping does already exist, it will enhance the existing mapping instead of dropping and replacing it. + +## Global setting + +Use global settings to apply policies to all mappings. + +```csharp +TypeAdapterConfig.GlobalSettings.Default.PreserveReference(true); +``` + +Then for individual type mappings, you can easily override the global setting(s). + +```csharp +TypeAdapterConfig.NewConfig().PreserveReference(false); +``` + +## Rule based settings + +To set the setting at a more granular level. You can use the `When` method in global settings. +In the example below, when any source type and destination type are the same, we will not the copy the `Id` property. + +```csharp +TypeAdapterConfig.GlobalSettings.When((srcType, destType, mapType) => srcType == destType) + .Ignore("Id"); +``` + +In this example, the config would only apply to Query Expressions (projections). + +```csharp +TypeAdapterConfig.GlobalSettings.When((srcType, destType, mapType) => mapType == MapType.Projection) + .IgnoreAttribute(typeof(NotMapAttribute)); +``` + +## Destination type only + +A setting can also be created without knowing the source type, by using `ForDestinationType`. For example, you can do `AfterMapping` setting to validate after mapping. + +```csharp +TypeAdapterConfig.GlobalSettings.ForDestinationType() + .AfterMapping(dest => dest.Validate()); +``` + +NOTE: `ForDestinationType` above will always apply to all types assignable to `IValidator`. If destination class implements `IValidator`, it will also apply the `AfterMapping` config. + +## Open generics + +If the mapping type is generic, you can create a setting by passing generic type definition to `ForType`. + +```csharp +TypeAdapterConfig.GlobalSettings.ForType(typeof(GenericPoco<>), typeof(GenericDto<>)) + .Map("value", "Value"); +``` diff --git a/docs/articles/configuration/toc.yml b/docs/articles/configuration/toc.yml new file mode 100644 index 00000000..3979af6c --- /dev/null +++ b/docs/articles/configuration/toc.yml @@ -0,0 +1,20 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/dotnet/docfx/main/schemas/toc.schema.json +# Side navigation for Configuration articles +- name: Configuration Overview + uid: Mapster.Configuration.Overview + href: Configuration.md +- name: Config inheritance + uid: Mapster.Configuration.Inheritance + href: Config-inheritance.md +- name: Config Instance + uid: Mapster.Configuration.Instance + href: Config-instance.md +- name: Config location + uid: Mapster.Configuration.Location + href: Config-location.md +- name: Config validation & compilation + uid: Mapster.Configuration.ValidationAndCompilation + href: Config-validation-and-compilation.md +- name: Config for nested mapping + uid: Mapster.Configuration.NestedMapping + href: Config-for-nested-mapping.md diff --git a/docs/articles/mapping/Basic-usages.md b/docs/articles/mapping/Basic-usages.md new file mode 100644 index 00000000..21d0fd12 --- /dev/null +++ b/docs/articles/mapping/Basic-usages.md @@ -0,0 +1,44 @@ +--- +uid: Mapster.Mapping.BasicUsages +title: "Mapping - Basic Usages" +--- + +## Mapping to a new object + +Mapster creates the destination object and maps values to it. + +```csharp +var destObject = sourceObject.Adapt(); +``` + +### Mapping to an existing object + +You make the object, Mapster maps to the object. + +```csharp +sourceObject.Adapt(destObject); +``` + +## Queryable Extensions + +Mapster also provides extensions to map queryables. + +> [!IMPORTANT] +> Avoid calling ProjectToType() before materializing queries from Entity Framework. This is known to cause issues. Instead, call ToList() or ToListAsync() before calling ProjectToType. + +```csharp +using (MyDbContext context = new MyDbContext()) +{ + // Build a Select Expression from DTO + var destinations = context.Sources.ProjectToType().ToList(); + + // Versus creating by hand: + var destinations = context.Sources.Select(c => new Destination { + Id = p.Id, + Name = p.Name, + Surname = p.Surname, + .... + }) + .ToList(); +} +``` diff --git a/docs/articles/mapping/Data-types.md b/docs/articles/mapping/Data-types.md new file mode 100644 index 00000000..f4951ed4 --- /dev/null +++ b/docs/articles/mapping/Data-types.md @@ -0,0 +1,114 @@ +--- +uid: Mapster.Mapping.DataTypes +title: "Mapping - Data Types" +--- + +## Primitives + +Converting between primitive types (ie. int, bool, double, decimal) is supported, including when those types are nullable. For all other types, if you can cast types in c#, you can also cast in Mapster. + +```csharp +decimal i = 123.Adapt(); //equal to (decimal)123; +``` + +## Enums + +Mapster maps enums to numerics automatically, but it also maps strings to and from enums automatically in a fast manner. +The default Enum.ToString() in .NET is quite slow. The implementation in Mapster is double the speed. Likewise, a fast conversion from strings to enums is also included. If the string is null or empty, the enum will initialize to the first enum value. + +In Mapster, flagged enums are also supported. + +```csharp +var e = "Read, Write, Delete".Adapt(); +//FileShare.Read | FileShare.Write | FileShare.Delete +``` + +For enum to enum with different type, by default, Mapster will map enum by value. You can override to map enum by name by: + +```csharp +TypeAdapterConfig.GlobalSettings.Default + .EnumMappingStrategy(EnumMappingStrategy.ByName); +``` + +## Strings + +When Mapster maps other types to string, Mapster will use `ToString` method. And whenever Mapster maps string to the other types, Mapster will use `Parse` method. + +```csharp +var s = 123.Adapt(); //equal to 123.ToString(); +var i = "123".Adapt(); //equal to int.Parse("123"); +``` + +## Collections + +This includes mapping among lists, arrays, collections, dictionary including various interfaces: `IList`, `ICollection`, `IEnumerable`, `ISet`, `IDictionary` etc... + +```csharp +var list = db.Pocos.ToList(); +var target = list.Adapt>(); +``` + +## Mappable Objects + +Mapster can map two different objects using the following rules: + +- Source and destination property names are the same. Ex: `dest.Name = src.Name` +- Source has get method. Ex: `dest.Name = src.GetName()` +- Source property has child object which can flatten to destination. Ex: `dest.ContactName = src.Contact.Name` or `dest.Contact_Name = src.Contact.Name` + +Example: + +```csharp +class Staff { + public string Name { get; set; } + public int GetAge() { + return (DateTime.Now - this.BirthDate).TotalDays / 365.25; + } + public Staff Supervisor { get; set; } + ... +} + +struct StaffDto { + public string Name { get; set; } + public int Age { get; set; } + public string SupervisorName { get; set; } +} + +var dto = staff.Adapt(); +//dto.Name = staff.Name, dto.Age = staff.GetAge(), dto.SupervisorName = staff.Supervisor.Name +``` + +**Mappable Object types are included:** + +- POCO classes +- POCO structs +- POCO interfaces +- Dictionary type implement `IDictionary` +- Record types (either class, struct, and interface) + +Example for object to dictionary: + +```csharp +var point = new { X = 2, Y = 3 }; +var dict = point.Adapt>(); +dict["Y"].ShouldBe(3); +``` + +Example for record types: + +```csharp +class Person { + public string Name { get; } + public int Age { get; } + + public Person(string name, int age) { + this.Name = name; + this.Age = age; + } +} + +var src = new { Name = "Mapster", Age = 3 }; +var target = src.Adapt(); +``` + +There are limitations to map Record type automatically. Record type must not have a setter and have only one non-empty constructor, and all parameter names must match with properties. Otherwise you need to add [`MapToConstructor` configuration](xref:Mapster.Settings.ConstructorMapping#map-to-constructor). diff --git a/docs/articles/mapping/Mappers.md b/docs/articles/mapping/Mappers.md new file mode 100644 index 00000000..b3ac783a --- /dev/null +++ b/docs/articles/mapping/Mappers.md @@ -0,0 +1,57 @@ +--- +uid: Mapster.Mapping.Mappers +title: "Mapping - Mappers" +--- + +## Extension method + +You can simply call `Adapt` method from anywhere. + +```csharp +var dest = src.Adapt(); +``` + +or just + +```csharp +var dest = src.Adapt(); +``` + +2 extension methods are doing the same thing. `src.Adapt` will cast `src` to object. Therefore, if you map value type, please use `src.Adapt` to avoid boxing and unboxing. + +## Mapper instance + +In some cases, you need an instance of a mapper (or a factory function) to pass into a DI container. Mapster has +the `IMapper` and `Mapper` to fill this need: + +```csharp +IMapper mapper = new Mapper(); +``` + +And usage `Map` method to perform mapping. + +```csharp +var result = mapper.Map(source); +``` + +## Builder + +In most case `Adapt` method is enough, but sometimes we need builder to support fancy scenario. Basic example, is to pass run-time value. + +```csharp +var dto = poco.BuildAdapter() + .AddParameters("user", this.User.Identity.Name) + .AdaptToType(); +``` + +Or if you use mapper instance, you can create builder by method `From`. + +```csharp +var dto = mapper.From(poco) + .AddParameters("user", this.User.Identity.Name) + .AdaptToType(); +``` + +## Code generation + +See [Mapster.Tool](xref:Mapster.Tools.MapsterTool.Overview) for generating your specific mapper class, rather than using the provided mappers. diff --git a/docs/articles/mapping/Mapping-Configuration-With-IMapFrom-Interface.md b/docs/articles/mapping/Mapping-Configuration-With-IMapFrom-Interface.md new file mode 100644 index 00000000..1d8ecf50 --- /dev/null +++ b/docs/articles/mapping/Mapping-Configuration-With-IMapFrom-Interface.md @@ -0,0 +1,39 @@ +--- +uid: Mapster.Mapping.IMapFromInterface +title: "Mapping - IMapFrom Interface" +--- + +Before using this feature you have to add this line: + +```csharp +TypeAdapterConfig.GlobalSettings.ScanInheritedTypes(Assembly.GetExecutingAssembly()); +``` + +With adding above line to your Startup.cs or Program.cs or any other way to run at startup, you can write mapping configs in the destination class that implements IMapFrom interface + +Example: + +```csharp +public class InheritedDestinationModel : IMapFrom +{ + public string Type { get; set; } + public int Value { get; set; } + + public void ConfigureMapping(TypeAdapterConfig config) + { + config.NewConfig() + .Map(dest => dest.Value, source => int.Parse(source.Value)); + } +} +``` + +Even if your destination model doesn't have a specific configuration (you don't want to customize anything), you can just inherit from IMapFrom interface + +Example: + +```csharp +public class DestinationModel : IMapFrom +{ + public string Type { get; set; } +} +``` diff --git a/docs/articles/mapping/toc.yml b/docs/articles/mapping/toc.yml new file mode 100644 index 00000000..ab7e7755 --- /dev/null +++ b/docs/articles/mapping/toc.yml @@ -0,0 +1,13 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/dotnet/docfx/main/schemas/toc.schema.json +- name: Basic usages + uid: Mapster.Mapping.BasicUsages + href: Basic-usages.md +- name: Mappers + uid: Mapster.Mapping.Mappers + href: Mappers.md +- name: Data types + uid: Mapster.Mapping.DataTypes + href: Data-types.md +- name: Mapping with interface + uid: Mapster.Mapping.IMapFromInterface + href: Mapping-Configuration-With-IMapFrom-Interface.md \ No newline at end of file diff --git a/docs/articles/packages/Async.md b/docs/articles/packages/Async.md new file mode 100644 index 00000000..563cb65c --- /dev/null +++ b/docs/articles/packages/Async.md @@ -0,0 +1,40 @@ +--- +uid: Mapster.Packages.Async +title: "Packages - Mapster Async Support" +--- + +This Package allows you to perform async operation for mapping. + +```nuget + PM> Install-Package Mapster.Async +``` + +## Setup + +Use `AfterMappingAsync` to setup async operation: + +```csharp +config.NewConfig() + .AfterMappingAsync(async (poco, dto) => + { + var userManager = MapContext.Current.GetService(); + var user = await userManager.FindByIdAsync(poco.UserId); + dto.UserName = user.Name; + }); +``` + +## Mapping + +Then map asynchronously with `AdaptToTypeAsync`. + +```csharp +var dto = await poco.BuildAdapter() + .AdaptToTypeAsync(); +``` + +Or like this, if you use mapper instance: + +```csharp +var dto = await _mapper.From(poco) + .AdaptToTypeAsync(); +``` diff --git a/docs/articles/packages/Dependency-Injection.md b/docs/articles/packages/Dependency-Injection.md new file mode 100644 index 00000000..358b256d --- /dev/null +++ b/docs/articles/packages/Dependency-Injection.md @@ -0,0 +1,59 @@ +--- +uid: Mapster.Packages.DependencyInjection +title: "Packages - Dependency Injection Support" +--- + +## Installation + +This package allows you to inject service into mapping configuration. + +```nuget + PM> Install-Package Mapster.DependencyInjection +``` + +## Usage + +On startup, register `TypeAdapterConfig`, and `ServiceMapper`. + +```csharp +public void ConfigureServices(IServiceCollection services) +{ + ... + var config = new TypeAdapterConfig(); + // Or + // var config = TypeAdapterConfig.GlobalSettings; + services.AddSingleton(config); + services.AddScoped(); + ... +} +``` + +NOTE: lifetime of `ServiceMapper` is up to services you would like to inject. It can be singleton, if you inject only singleton services. Or it can be transient, if any injected services is transient. + +## Mapping configuration + +You can get service by `MapContext.Current.GetService<>()`, for example + +```csharp +config.NewConfig() + .Map(dest => dest.Name, src => MapContext.Current.GetService().Format(src.Name)); +``` + +## Mapping + +If you setup service injection, you need to use mapper instance to map object. + +```csharp +public class FooService { + private readonly IMapper _mapper; + + public FooService(IMapper mapper) { + _mapper = mapper; + } + + public void DoSomething(Poco poco) { + var dto = _mapper.Map(poco); + ... + } +} +``` diff --git a/docs/articles/packages/EF-6-and-EF-Core.md b/docs/articles/packages/EF-6-and-EF-Core.md new file mode 100644 index 00000000..f255d6e9 --- /dev/null +++ b/docs/articles/packages/EF-6-and-EF-Core.md @@ -0,0 +1,61 @@ +--- +uid: Mapster.Packages.EntityFramework +title: "Packages - EF 6 and EF Core Support" +--- + +## [EntityFramework 6 support](#tab/ef6) + +To install the package for EF 6: + +```nuget + PM> Install-Package Mapster.EF6 +``` + +## [EntityFramework Core support](#tab/efcore) + +To install the package for EF Core: + +```nuget + PM> Install-Package Mapster.EFCore +``` + +In EF, objects are tracked, when you copy data from dto to entity containing navigation properties, this package will help finding entity object in navigation properties automatically. + +--- + +## Compatibility + +- use Mapster.EFCore version 5.x for EFCore 5.x +- use Mapster.EFCore version 3.x for EFCore 3.x +- use Mapster.EFCore version 1.x for EFCore 2.x + +## Usage + +Use `EntityFromContext` method to define data context. + +```csharp +var poco = db.DomainPoco.Include("Children") + .Where(item => item.Id == dto.Id).FirstOrDefault(); + +dto.BuildAdapter() + .EntityFromContext(db) + .AdaptTo(poco); +``` + +Or like this, if you use mapper instance + +```csharp +_mapper.From(dto) + .EntityFromContext(db) + .AdaptTo(poco); +``` + +### EF Core `ProjectToType` + +`Mapster.EFCore` also allows you to perform projection from `IQueryable` source via mapper instance and `ProjectToType` directly to your DTO type. + +```csharp +var query = db.Customers.Where(...); +_mapper.From(query) + .ProjectToType(); +``` diff --git a/docs/articles/packages/ExpressionDebugging.md b/docs/articles/packages/ExpressionDebugging.md new file mode 100644 index 00000000..e32f41dd --- /dev/null +++ b/docs/articles/packages/ExpressionDebugging.md @@ -0,0 +1,61 @@ +--- +uid: Mapster.Packages.ExpressionDebugging +title: "Packages - Expression Debugging" +--- + +This Package allows you to perform step-into debugging using Roslyn! + +```nuget + PM> Install-Package ExpressionDebugger +``` + +## Usage + +Then add following code on start up (or anywhere before mapping is compiled) + +```csharp +TypeAdapterConfig.GlobalSettings.Compiler = exp => exp.CompileWithDebugInfo(); +``` + +Now in your mapping code (only in `DEBUG` mode). + +```csharp +var dto = poco.Adapt(); //<--- you can step-into this function!! +``` + +![step-into-debugging-screenshot](../.assets/step-into-debugging.png) + +## Using internal classes or members + +`private`, `protected` and `internal` aren't allowed in debug mode. + +### Get mapping script + +We can also see how Mapster generates mapping logic with `ToScript` method. + +```csharp +var script = poco.BuildAdapter() + .CreateMapExpression() + .ToScript(); +``` + +## Specifics for Visual Studio on Mac + +To step-into debugging, you might need to emit file + +```csharp +var opt = new ExpressionCompilationOptions { EmitFile = true }; +TypeAdapterConfig.GlobalSettings.Compiler = exp => exp.CompileWithDebugInfo(opt); +... +var dto = poco.Adapt(); //<-- you can step-into this function!! +``` + +### Do not worry about performance + +In `RELEASE` mode, Roslyn compiler is actually faster than default dynamic compilation by 2x. +Here is the result: + +| Method | Mean | StdDev | Error | Gen 0 | Gen 1 | Gen 2 | Allocated | +|-------------------------- |---------------:|-------------:|-------------:|------------:|------:|------:|-----------:| +| 'Mapster 4.1.1' | 115.31 ms | 0.849 ms | 1.426 ms | 31000.0000 | - | - | 124.36 MB | +| 'Mapster 4.1.1 (Roslyn)' | 53.55 ms | 0.342 ms | 0.654 ms | 31100.0000 | - | - | 124.36 MB | diff --git a/docs/articles/packages/FastExpressionCompiler.md b/docs/articles/packages/FastExpressionCompiler.md new file mode 100644 index 00000000..a20df828 --- /dev/null +++ b/docs/articles/packages/FastExpressionCompiler.md @@ -0,0 +1,27 @@ +--- +uid: Mapster.Packages.FastExpressionCompiler +title: "Packages - Fast Expression Compiler Support" +--- + +Need more speed? Let's compile with [FastExpressionCompiler](https://github.com/dadhi/FastExpressionCompiler). + +## Installation + +Getting the package: + +```nuget + PM> Install-Package FastExpressionCompiler +``` + +Then add following code on start up + +```csharp +TypeAdapterConfig.GlobalSettings.Compiler = exp => exp.CompileFast(); +``` + +That's it. Now your code will enjoy performance boost. Here is result. + +| Method | Mean | StdDev | Error | Gen 0 | Gen 1 | Gen 2 | Allocated | +|-------------------------- |---------------:|-------------:|-------------:|------------:|------:|------:|-----------:| +| 'Mapster 4.1.1' | 115.31 ms | 0.849 ms | 1.426 ms | 31000.0000 | - | - | 124.36 MB | +| 'Mapster 4.1.1 (FEC)' | 54.70 ms | 1.023 ms | 1.546 ms | 29600.0000 | - | - | 118.26 MB | diff --git a/docs/articles/packages/Immutable.md b/docs/articles/packages/Immutable.md new file mode 100644 index 00000000..3b8bd5ef --- /dev/null +++ b/docs/articles/packages/Immutable.md @@ -0,0 +1,42 @@ +--- +uid: Mapster.Packages.Immutable +title: "Packages - Immutable Support" +--- + +This Package enables Immutable collection support in Mapster. + +### Installation + +```nuget + PM> Install-Package Mapster.Immutable +``` + +### Usage + +Call `EnableImmutableMapping` from your `TypeAdapterConfig` to enable Immutable collection. + +```csharp +TypeAdapterConfig.GlobalSettings.EnableImmutableMapping(); +``` + +or: + +```csharp +config.EnableImmutableMapping(); +``` + +This will allow mapping to: + +- `IImmutableDictionary<,>` +- `IImmutableList<>` +- `IImmutableQueue<>` +- `IImmutableSet<>` +- `IImmutableStack<>` +- `ImmutableArray<>` +- `ImmutableDictionary<,>` +- `ImmutableHashSet<>` +- `ImmutableList<>` +- `ImmutableQueue<>` +- `ImmutableSortedDictionary<,>` +- `ImmutableSortedSet<>` +- `ImmutableStack<>` diff --git a/docs/articles/packages/Json.net.md b/docs/articles/packages/Json.net.md new file mode 100644 index 00000000..2cc8d3b3 --- /dev/null +++ b/docs/articles/packages/Json.net.md @@ -0,0 +1,31 @@ +--- +uid: Mapster.Packages.JsonNet +title: "Packages - Json.net Support" +--- + +The `Json.net` Package adds conversion supports for Json.Net types. + +## Installation + +```nuget + PM> Install-Package Mapster.JsonNet +``` + +## Usage + +Call `EnableJsonMapping` from your `TypeAdapterConfig` to enable Json.Net mapping. + +```csharp +TypeAdapterConfig.GlobalSettings.EnableJsonMapping(); +``` + +or: + +```csharp +config.EnableJsonMapping(); +``` + +This will allow: + +- Mapping between Json.Net types (`JToken`, `JArray`, `JObject`) from/to POCO types +- Serialize and deserialize Json.Net types from/to string diff --git a/docs/articles/packages/toc.yml b/docs/articles/packages/toc.yml new file mode 100644 index 00000000..7558022c --- /dev/null +++ b/docs/articles/packages/toc.yml @@ -0,0 +1,23 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/dotnet/docfx/main/schemas/toc.schema.json +# NOTE: Use quotes for names with special characters like "&" and don't use them in filenames +- name: Async Support + uid: Mapster.Packages.Async + href: Async.md +- name: "Packages - Expression Debugging" + uid: Mapster.Packages.ExpressionDebugging + href: ExpressionDebugging.md +- name: Dependency Injection + uid: Mapster.Packages.DependencyInjection + href: Dependency-Injection.md +- name: "EF 6 and EF Core Support" + uid: Mapster.Packages.EntityFramework + href: EF-6-and-EF-Core.md +- name: FastExpressionCompiler + uid: Mapster.Packages.FastExpressionCompiler + href: FastExpressionCompiler.md +- name: Immutable + uid: Mapster.Packages.Immutable + href: Immutable.md +- name: "Json.net Support" + uid: Mapster.Packages.JsonNet + href: Json.net.md diff --git a/docs/articles/settings/Before-after-mapping.md b/docs/articles/settings/Before-after-mapping.md new file mode 100644 index 00000000..cff135a2 --- /dev/null +++ b/docs/articles/settings/Before-after-mapping.md @@ -0,0 +1,109 @@ +--- +uid: Mapster.Settings.BeforeAfterMapping +title: "Settings - Before and After mapping" +--- + +## Before mapping action + +You can perform actions before mapping started by using `BeforeMapping` method. + +```csharp +TypeAdapterConfig.ForType() + .BeforeMapping((src, result) => result.Initialize()); +``` + +## After mapping action + +You can perform actions after each mapping by using `AfterMapping` method. For instance, you might would like to validate object after each mapping. + +```csharp +TypeAdapterConfig.ForType() + .AfterMapping((src, result) => result.Validate()); +``` + +Or you can set for all mappings to types which implemented a specific interface by using `ForDestinationType` method. + +```csharp +TypeAdapterConfig.GlobalSettings.ForDestinationType() + .AfterMapping(result => result.Validate()); +``` + +## Code generation + +`BeforeMapping` and `AfterMapping` accept action which allowed you to pass multiple statements. In code generation, you might need to pass expression instead of action using `BeforeMappingInline` and `AfterMappingInline`, expression can be translated into code, but action cannot. + +### Single line statement + +For single line statement, you can directly change from `BeforeMapping` and `AfterMapping` to `BeforeMappingInline` and `AfterMappingInline`. + +```csharp +TypeAdapterConfig.GlobalSettings.ForDestinationType() + .AfterMappingInline(result => result.Validate()); +``` + +### Multiple statements + +For multiple statements, you need to declare a method for actions. + +```csharp +public static void Validate(Dto dto) { + action1(dto); + action2(dto); + ... +} +``` + +Then you can reference the method to `BeforeMappingInline` and `AfterMappingInline`. + +```csharp +TypeAdapterConfig.GlobalSettings.ForDestinationType() + .AfterMappingInline(result => PocoToDtoMapper.Validate(result)); +``` + +## Overloads with `destination` parameter + +You can use `BeforeMapping` with `destination` to construct final (`result`) object. + +```csharp +TypeAdapterConfig, IEnumerable>.NewConfig() + .BeforeMapping((src, result, destination) => + { + if (!ReferenceEquals(result, destination) && destination != null && result is ICollection resultCollection) + { + foreach (var item in destination) + { + resultCollection.Add(item); + } + } +}); + +IEnumerable source = new List { 1, 2, 3, }; +IEnumerable destination = new List { 0, }; + +var result = source.Adapt(destination); + +destination.ShouldBe(new List { 0, }); +source.ShouldBe(new List { 1, 2, 3, }); +result.ShouldBe(new List { 0, 1, 2, 3, }); +``` + +Same with `AfterMapping`. + +```csharp +TypeAdapterConfig.NewConfig() + .ConstructUsing((simplePoco, dto) => new SimpleDto()) + .AfterMapping((src, result, destination) => result.Name += $"{destination.Name}xxx"); + +var poco = new SimplePoco +{ + Id = Guid.NewGuid(), + Name = "test", +}; + +var oldDto = new SimpleDto { Name = "zzz", }; +var result = poco.Adapt(oldDto); + +result.ShouldNotBeSameAs(oldDto); +result.Id.ShouldBe(poco.Id); +result.Name.ShouldBe(poco.Name + "zzzxxx"); +``` diff --git a/docs/articles/settings/Constructor-mapping.md b/docs/articles/settings/Constructor-mapping.md new file mode 100644 index 00000000..7cac272f --- /dev/null +++ b/docs/articles/settings/Constructor-mapping.md @@ -0,0 +1,79 @@ +--- +uid: Mapster.Settings.ConstructorMapping +title: "Settings - Constructor mapping" +--- + +## Custom Destination Object Creation + +You can provide a function call to create your destination objects instead of using the default object creation +(which expects an empty constructor). To do so, use the `ConstructUsing` method when configuring. This method expects +a function that will provide the destination instance. You can call your own constructor, a factory method, +or anything else that provides an object of the expected type. + +```csharp +//Example using a non-default constructor +TypeAdapterConfig.NewConfig() + .ConstructUsing(src => new TDestination(src.Id, src.Name)); + +//Example using an object initializer +TypeAdapterConfig.NewConfig() + .ConstructUsing(src => new TDestination{Unmapped = "unmapped"}); + +//Example using an overload with `destination` parameter +TypeAdapterConfig.NewConfig() + .ConstructUsing((src, destination) => new TDestination(src.Id, destination?.Name ?? src.Name)); +``` + +### Map to constructor + +By default, Mapster will only map to fields and properties. You can configure to map to constructors by `MapToConstructor`. + +```csharp +//global level +TypeAdapterConfig.GlobalSettings.Default.MapToConstructor(true); + +//type pair +TypeAdapterConfig.NewConfig().MapToConstructor(true); +``` + +To define custom mapping, you need to use Pascal case. + +```csharp +class Poco { + public string Id { get; set; } + ... +} +class Dto { + public Dto(string code, ...) { + ... + } +} +``` + +```csharp +TypeAdapterConfig.NewConfig() + .MapToConstructor(true) + .Map('Code', 'Id'); //use Pascal case +``` + +If a class has 2 or more constructors, Mapster will automatically select largest number of parameters that satisfy mapping. + +```csharp +class Poco { + public int Foo { get; set; } + public int Bar { get; set; } +} +class Dto { + public Dto(int foo) { ... } + public Dto(int foo, int bar) { ...} //<-- Mapster will use this constructor + public Dto(int foo, int bar, int baz) { ... } +} +``` + +Or you can also explicitly pass ConstructorInfo to the method. + +```csharp +var ctor = typeof(Dto).GetConstructor(new[] { typeof(int), typeof(int) }); +TypeAdapterConfig.NewConfig() + .MapToConstructor(ctor); +``` diff --git a/docs/articles/settings/Custom-conversion-logic.md b/docs/articles/settings/Custom-conversion-logic.md new file mode 100644 index 00000000..0edaaa23 --- /dev/null +++ b/docs/articles/settings/Custom-conversion-logic.md @@ -0,0 +1,49 @@ +--- +uid: Mapster.Settings.CustomConversionLogic +title: "Settings - Custom conversion logic" +--- + +## Custom type conversion + +In some cases, you may want to have complete control over how an object is mapped. You can register specific transformations using the `MapWith` method. + +```csharp +//Example of transforming string to char[]. +TypeAdapterConfig.NewConfig() + .MapWith(str => str.ToCharArray()); +``` + +`MapWith` also useful if you would like to copy instance rather than deep copy the object, for instance, `JObject` or `DbGeography`, these should treat as primitive types rather than POCO. + + ```csharp +TypeAdapterConfig.NewConfig() + .MapWith(json => json); +``` + +In case you would like to combine `MapWith` with other settings, for example, `PreserveReference`, `Include`, or `AfterMapping`, you can pass `applySettings` to true. + +```csharp +TypeAdapterConfig.NewConfig() + .PreserveReference(true) + .MapWith(poco => poco.ToDto(), applySettings: true); +``` + +## Custom mapping data to existing object + +You can control mapping to existing object logic by `MapToTargetWith`. For example, you can copy data to existing array. + +```csharp +TypeAdapterConfig.NewConfig() + .MapToTargetWith((src, dest) => Array.Copy(src, dest, src.Length)); +``` + +NOTE: if you set `MapWith` setting but no `MapToTargetWith` setting, Mapster will use logic from `MapWith` setting. + +### Custom actions after mapping + +You might not need to specify custom mapping logic completely. You can let Mapster do the mapping, and you do logic where Mapster cannot cover by using `AfterMapping`. + +```csharp +TypeAdapterConfig.NewConfig() + .AfterMapping((src, dest) => SpecialSetFn(src, dest)); +``` diff --git a/docs/articles/settings/Object-references.md b/docs/articles/settings/Object-references.md new file mode 100644 index 00000000..86d2d955 --- /dev/null +++ b/docs/articles/settings/Object-references.md @@ -0,0 +1,55 @@ +--- +uid: Mapster.Settings.ObjectReferences +title: "Settings - Object references" +--- + +## Preserve reference (preventing circular reference stackoverflow) + +When mapping objects with circular references, a stackoverflow exception will result. This is because Mapster will get stuck in a loop trying to recursively map the circular reference. If you would like to map circular references or preserve references (such as 2 properties pointing to the same object), you can do it by setting `PreserveReference` to `true` + +```csharp +TypeAdapterConfig + .NewConfig() + .PreserveReference(true); +``` + +NOTE: in Mapster setting is per type pair, not per hierarchy (see [**Configuration for nested Classes**](xref:Mapster.Configuration.NestedMapping)). Therefore, you need to apply config to all type pairs. + +NOTE: you might need to use `MaxDepth`. `PreserveReference` doesn't support EF Query (`ProjectTo`) + +## MaxDepth + +Rather than `PreserveReference`, you could also try `MaxDepth`. `MaxDepth` will map until it reaches the defined limit. Unlike `PreserveReference`, `MaxDepth` also works with queryable projection. + +```csharp +TypeAdapterConfig + .NewConfig() + .MaxDepth(3); +``` + +NOTE 1: `MaxDepth` starts with 1, means you will copy only primitives. POCO class and collection of POCO each count as a depth of 1. + +NOTE 2: even `MaxDepth` has no maximum value, you shouldn't input large number. Each depth will generate a mapping logic, otherwise it will consume a lot of memory. + +## Shallow copy + +By default, Mapster will recursively map nested objects. You can do shallow copying by setting `ShallowCopyForSameType` to `true`. + +```csharp +TypeAdapterConfig + .NewConfig() + .ShallowCopyForSameType(true); +``` + +## Mapping very large objects + +For performance optimization, Mapster tried to inline class mapping. This process will takes time if your models are complex. + +![inline-class-mapping-diagram](https://cloud.githubusercontent.com/assets/21364231/25666644/ce38c8c0-3029-11e7-8793-8a51c519c2a0.png) + +You can skip inlining process by: + +```csharp +TypeAdapterConfig.GlobalSettings.Default + .AvoidInlineMapping(true); +``` diff --git a/docs/articles/settings/Setting-values.md b/docs/articles/settings/Setting-values.md new file mode 100644 index 00000000..a08dc9df --- /dev/null +++ b/docs/articles/settings/Setting-values.md @@ -0,0 +1,55 @@ +--- +uid: Mapster.Settings.SettingValues +title: "Settings - Setting values" +--- + +## Computed value + +You can use `Map` method to specify logic to compute value. For example, compute full name from first name and last name. + +```csharp +TypeAdapterConfig.NewConfig() + .Map(dest => dest.FullName, src => src.FirstName + " " + src.LastName); +``` + +## Transform value + +While `Map` method specify logic for single property, `AddDestinationTransform` allows transforms for all items of a type, such as trimming all strings. But really any operation can be performed on the destination value before assignment. + +### Trim string + +```csharp +TypeAdapterConfig.NewConfig() + .AddDestinationTransform((string x) => x.Trim()); +``` + +### Null replacement + +```csharp +TypeAdapterConfig.NewConfig() + .AddDestinationTransform((string x) => x ?? ""); +``` + +### Return empty collection if null + +```csharp +config.Default.AddDestinationTransform(DestinationTransform.EmptyCollectionIfNull); +``` + +## Passing run-time value + +In some cases, you might would like to pass runtime values (ie, current user). On configuration, we can receive run-time value by `MapContext.Current.Parameters`. + +```csharp +TypeAdapterConfig.NewConfig() + .Map(dest => dest.CreatedBy, + src => MapContext.Current.Parameters["user"]); +``` + +To pass run-time value, we need to use `BuildAdapter` method, and call `AddParameters` method to add each parameter. + +```csharp +var dto = poco.BuildAdapter() + .AddParameters("user", this.User.Identity.Name) + .AdaptToType(); +``` diff --git a/docs/articles/settings/Shallow-merge.md b/docs/articles/settings/Shallow-merge.md new file mode 100644 index 00000000..ba101264 --- /dev/null +++ b/docs/articles/settings/Shallow-merge.md @@ -0,0 +1,24 @@ +--- +uid: Mapster.Settings.ShallowMerge +title: "Settings - Shallow merge" +--- + +## Deep copy vs. shallow copy + +By default, Mapster will recursively map nested objects (deep copy). You can do shallow copying by setting `ShallowCopyForSameType` to `true`. + +```csharp +TypeAdapterConfig + .NewConfig() + .ShallowCopyForSameType(true); +``` + +## Copy vs. Merge + +By default, Mapster will map all properties, even source properties containing null values. You can copy only properties that have values (merge) by using `IgnoreNullValues` method. + +```csharp +TypeAdapterConfig + .NewConfig() + .IgnoreNullValues(true); +``` diff --git a/docs/articles/settings/custom/Custom-mapping.md b/docs/articles/settings/custom/Custom-mapping.md new file mode 100644 index 00000000..b687c081 --- /dev/null +++ b/docs/articles/settings/custom/Custom-mapping.md @@ -0,0 +1,109 @@ +--- +uid: Mapster.Settings.Custom.Mapping +title: "Settings - Custom Mapping" +--- + +## Custom member mapping + +You can customize how Mapster maps values to a property. + +```csharp +TypeAdapterConfig + .NewConfig() + .Map(dest => dest.FullName, + src => $"{src.FirstName} {src.LastName}"); +``` + +You can even map when source and destination property types are different. + +```csharp +TypeAdapterConfig + .NewConfig() + .Map(dest => dest.Gender, //Genders.Male or Genders.Female + src => src.GenderString); //"Male" or "Female" +``` + +### Mapping with condition + +The Map configuration can accept a third parameter that provides a condition based on the source. +If the condition is not met, Mapster will retry with next conditions. Default condition should be added at the end without specifying condition. If you do not specify default condition, null or default value will be assigned. + +```csharp +TypeAdapterConfig + .NewConfig() + .Map(dest => dest.FullName, src => "Sig. " + src.FullName, srcCond => srcCond.Country == "Italy") + .Map(dest => dest.FullName, src => "Sr. " + src.FullName, srcCond => srcCond.Country == "Spain") + .Map(dest => dest.FullName, src => "Mr. " + src.FullName); +``` + +NOTE: if you would like to skip mapping, when condition is met, you can use `IgnoreIf` (xref:Mapster.Settings.Custom.IgnoringMembers#ignore-conditionally). + +### Mapping to non-public members + +`Map` command can map to private member by specify name of the members. + +```csharp +TypeAdapterConfig + .NewConfig() + .Map("PrivateDestName", "PrivateSrcName"); +``` + +For more information about mapping non-public members, please see [Mapping non-public members](xref:Mapster.Settings.Custom.NonPublicMembers). + +### Deep destination property + +`Map` can be defined to map deep destination property. + +```csharp +TypeAdapterConfig.NewConfig() + .Map(dest => dest.Child.Name, src => src.Name); +``` + +### Null propagation + +If `Map` contains only property path, null propagation will be applied. + +```csharp +TypeAdapterConfig.NewConfig() + .Map(dest => dest.Name, src => src.Child.Name); +``` + +From above example, if `src.Child` is null, mapping will return null instead of throw `NullPointerException`. + +### Multiple sources + +**Example 1**: Include property to Poco + +```csharp +public class SubDto +{ + public string Extra { get; set; } +} +public class Dto +{ + public string Name { get; set; } + public SubDto SubDto { get; set; } +} +public class Poco +{ + public string Name { get; set; } + public string Extra { get; set; } +} +``` + +In this case, you would like to map all properties from `Dto` to `Poco`, and also include all properties from `Dto.SubDto` to `Poco`. You can do this by just mapping `dto.SubDto` to `poco` in configuration. + +```csharp +TypeAdapterConfig.NewConfig() + .Map(poco => poco, dto => dto.SubDto); +``` + +**Example 2**: Mapping 2 objects to poco + +In this example, you have `Dto1` and `Dto2`, and you would like to map both objects to a `Poco`. You can do this by wrapping `Dto1` and `Dto2` into a tuple. And then mapping `tuple.Item1` and `tuple.Item2` to `Poco`. + +```csharp +TypeAdapterConfig<(Dto1, Dto2), Poco>.NewConfig() + .Map(dest => dest, src => src.Item1) + .Map(dest => dest, src => src.Item2); +``` diff --git a/docs/articles/settings/custom/Ignoring-members.md b/docs/articles/settings/custom/Ignoring-members.md new file mode 100644 index 00000000..a958ad89 --- /dev/null +++ b/docs/articles/settings/custom/Ignoring-members.md @@ -0,0 +1,69 @@ +--- +uid: Mapster.Settings.Custom.IgnoringMembers +title: "Settings - Ignoring members" +--- + +## Ignore Extension Method + +Mapster will automatically map properties with the same names. You can ignore members by using the `Ignore` method. + +```csharp +TypeAdapterConfig + .NewConfig() + .Ignore(dest => dest.Id); +``` + +## Rule based ignore with `IgnoreMember` + +You can ignore based on member information by `IgnoreMember` command. Please see [Rule-based-member-mapping](xref:Mapster.Settings.Custom.RuleBasedMapping) for more info. + +```csharp +TypeAdapterConfig.GlobalSettings.Default + .IgnoreMember((member, side) => !validTypes.Contains(member.Type)); +``` + +### IgnoreNonMapped + +You can ignore all non-mapped members by IgnoreNonMapped command. For example, we would like to map only Id and Name. + +```csharp +TypeAdapterConfig + .NewConfig() + .Map(dest => dest.Id, src => src.Id) + .Map(dest => dest.Name, src => src.Name) + .IgnoreNonMapped(true); +``` + +## Using the `AdaptIgnore` attribute + +You can ignore member by decorate with `[AdaptIgnore]`, and you can ignore custom attributes by `IgnoreAttribute` command. Please see [Setting by attributes](xref:Mapster.Settings.Custom.Attributes) for more info. + +```csharp +public class Product { + public string Id { get; set; } + public string Name { get; set; } + + [AdaptIgnore] + public decimal Price { get; set; } +} +``` + +### Ignore conditionally with `IgnoreIf` Extension Method + +You can ignore members conditionally, with condition based on source or target. When the condition is met, mapping of the property will be skipped altogether. This is the difference from custom `Map` with condition, where destination is set to `null` when condition is met. + +```csharp +TypeAdapterConfig + .NewConfig() + .IgnoreIf((src, dest) => !string.IsNullOrEmpty(dest.Name), dest => dest.Name); +``` + +### `IgnoreNullValues` Extension Method + +You might would like to merge from input object, By default, Mapster will map all properties, even source properties containing null values. You can copy only properties that have values by using `IgnoreNullValues` method. + +```csharp +TypeAdapterConfig + .NewConfig() + .IgnoreNullValues(true); +``` diff --git a/docs/articles/settings/custom/Mapping-non-public-members.md b/docs/articles/settings/custom/Mapping-non-public-members.md new file mode 100644 index 00000000..21864fa5 --- /dev/null +++ b/docs/articles/settings/custom/Mapping-non-public-members.md @@ -0,0 +1,58 @@ +--- +uid: Mapster.Settings.Custom.NonPublicMembers +title: "Settings - Mapping non-public members" +--- + +## `EnableNonPublicMembers` Extension Method + +This will allow Mapster to set to all non-public members. + +```csharp +//type pair +TypeAdapterConfig.NewConfig().EnableNonPublicMembers(true); + +//global +TypeAdapterConfig.GlobalSettings.Default.EnableNonPublicMembers(true); +``` + +## `AdaptMember` attribute + +You can also map non-public members with `AdaptMember` attribute. + +```csharp +public class Product +{ + [AdaptMember] + private string HiddenId { get; set; } + public string Name { get; set; } +} +``` + +## `Map` + +The `Map` extension can map to private member by specify name of the members. + +```csharp +TypeAdapterConfig + .NewConfig() + .Map("PrivateDestName", "PrivateSrcName"); +``` + +## `IncludeMember` + +With the `IncludeMember` extension, you can select which access modifier to allow. + +```csharp +TypeAdapterConfig.GlobalSettings.Default + .IncludeMember((member, side) => member.AccessModifier == AccessModifier.Internal + || member.AccessModifier == AccessModifier.ProtectedInternal); +``` + +### Note for non-public member mapping + +If type doesn't contain public properties, Mapster will treat type as primitive, you must also declare type pair to ensure Mapster will apply non-public member mapping. + +```csharp +TypeAdapterConfig.GlobalSettings.Default.EnableNonPublicMembers(true); +TypeAdapterConfig.NewConfig(); +``` diff --git a/docs/articles/settings/custom/Mapping-readonly-prop.md b/docs/articles/settings/custom/Mapping-readonly-prop.md new file mode 100644 index 00000000..c027b911 --- /dev/null +++ b/docs/articles/settings/custom/Mapping-readonly-prop.md @@ -0,0 +1,39 @@ +--- +uid: Mapster.Settings.Custom.ReadonlyProperty +title: "Settings - Mapping readonly properties" +--- + +## Non public setter + +Mapster can map to non public setter automatically. + +```csharp +public class Order { + public string Id { get; set; } + public ICollection Items { get; private set; } +} +``` + +## Using `UseDestinationValue` attribute + +You can make your type pure readonly and annotate with `[UseDestinationValue]`. + +```csharp +public class Order { + public string Id { get; set; } + + [UseDestinationValue] + public ICollection Items { get; } = new List(); +} +``` + +## Convention based setup using `UseDestinationValue` Extension Method + +Or you can apply without annotate each type, for example, if you would like all readonly `ICollection<>` to use destination value. + +```csharp +TypeAdapterConfig.GlobalSettings.Default + .UseDestinationValue(member => member.SetterModifier == AccessModifier.None && + member.Type.IsGenericType && + member.Type.GetGenericTypeDefinition() == typeof(ICollection<>)); +``` diff --git a/docs/articles/settings/custom/Naming-convention.md b/docs/articles/settings/custom/Naming-convention.md new file mode 100644 index 00000000..f84e64e7 --- /dev/null +++ b/docs/articles/settings/custom/Naming-convention.md @@ -0,0 +1,79 @@ +--- +uid: Mapster.Settings.Custom.NamingConvention +title: "Settings - Naming convention" +--- + +## Flexible name + +By default, Mapster will map property with case sensitive name. You can adjust to flexible name mapping by setting `NameMatchingStrategy.Flexible` to `NameMatchingStrategy` method. This setting will allow matching between `PascalCase`, `camelCase`, `lower_case`, and `UPPER_CASE`. + +```csharp +//global +TypeAdapterConfig.GlobalSettings.Default.NameMatchingStrategy(NameMatchingStrategy.Flexible); + +//type pair +TypeAdapterConfig.NewConfig().NameMatchingStrategy(NameMatchingStrategy.Flexible); +``` + +## Ignore cases + +Flexible name could not map between `MiXcAsE` and `MixCase`, because flexible name will break `MiXcAsE` into `Mi-Xc-As-E` rather than just `Mix-Case`. In this case, we need to use `IgnoreCase` to perform case insensitive matching. + +```csharp +TypeAdapterConfig.GlobalSettings.Default.NameMatchingStrategy(NameMatchingStrategy.IgnoreCase); +``` + +## Prefix & Replace + +For custom rules, you can use either `ConvertSourceMemberName` or `ConvertDestinationMemberName` up to which side you would like to convert. For example, you might would like to add `m_` to all properties. + +```csharp +TypeAdapterConfig.NewConfig() + .NameMatchingStrategy(NameMatchingStrategy.ConvertSourceMemberName(name => "m_" + name)); +``` + +This example is to replace foreign letter from name. + +```csharp +TypeAdapterConfig.NewConfig() + .NameMatchingStrategy(NameMatchingStrategy.ConvertSourceMemberName(name => name.Replace("Ä", "A")); +``` + +## Naming Convention with `IDictionary` + +If you would like to change case from POCO to `IDictionary` to camelCase, you can use `ToCamelCase`. Another way around, if you would like to map `IDictionary` back to POCO, you can use `FromCamelCase`. + +```csharp +TypeAdapterConfig>.NewConfig() + .NameMatchingStrategy(NameMatchingStrategy.ToCamelCase); +TypeAdapterConfig, Poco>.NewConfig() + .NameMatchingStrategy(NameMatchingStrategy.FromCamelCase); +``` + +> [!NOTE] +> Mapping from `IDictionary` to POCO, you can also use `Flexible` or `IgnoreCase`, but both will be slower since it will scan through dictionary entries rather than lookup. + +## Rule based Naming using `GetMemberName` Extension Method + +You can change name based on rule by `GetMemberName` method. For example, if we would like to rename property based on `JsonProperty` attribute. + +```csharp +TypeAdapterConfig.GlobalSettings.Default + .GetMemberName(member => member.GetCustomAttributes(true) + .OfType() + .FirstOrDefault()?.PropertyName); //if return null, property will not be renamed +``` + +Then in your class: + +```csharp +public class Poco +{ + [JsonProperty("code")] + public string Id { get; set; } + + ... +} +``` + +With above config, `Id` will be mapped to `code`. diff --git a/docs/articles/settings/custom/Rule-based-member-mapping.md b/docs/articles/settings/custom/Rule-based-member-mapping.md new file mode 100644 index 00000000..7901dd48 --- /dev/null +++ b/docs/articles/settings/custom/Rule-based-member-mapping.md @@ -0,0 +1,82 @@ +--- +uid: Mapster.Settings.Custom.RuleBasedMapping +title: "Settings - Rule-based member mapping" +--- + +By default, Mapster will include public fields and properties, but we can change this behavior by `IncludeMember` and `IgnoreMember` method. The methods require predicate, and input types of predicate are: + +```csharp +public interface IMemberModel +{ + Type Type { get; } + string Name { get; } + object Info { get; } + AccessModifier SetterModifier { get; } + AccessModifier AccessModifier { get; } + IEnumerable GetCustomAttributes(bool inherit); +} + +public enum MemberSide +{ + Source, + Destination, +} +``` + +## Not allow fields + +If you would like to allow only properties not public field to be mapped, you can check from `Info`. Possible values could be `PropertyInfo`, `FieldInfo`, or `ParameterInfo`. In this case, we will reject member of type `FieldInfo`. + +```csharp +TypeAdapterConfig.GlobalSettings.Default + .IgnoreMember((member, side) => member.Info is FieldInfo); +``` + +## Allow only some list of types to be mapped + +Suppose you are working with EF, and you would like to skip all navigation properties. Then we will allow only short list of types. + +### Allow by types + +```csharp +TypeAdapterConfig.GlobalSettings.Default + .IgnoreMember((member, side) => !validTypes.Contains(member.Type)); +``` + +### Allow by Namespaces + +```csharp +TypeAdapterConfig.GlobalSettings.Default + .IgnoreMember((member, side) => !member.Type.Namespace.StartsWith("System")); +``` + +### Allow internal members + +If you would like to map members marked as internal, you can do it by: + +```csharp + TypeAdapterConfig.GlobalSettings.Default + .IncludeMember((member, side) => member.AccessModifier == AccessModifier.Internal + || member.AccessModifier == AccessModifier.ProtectedInternal); +``` + +### Allow only `DataMember` attribute + +If you would like to include all members decorated with `DataMember` attribute, and ignore all members with no `DataMember` attribute, you can set up by: + +```csharp +TypeAdapterConfig.GlobalSettings.Default + .IncludeMember((member, side) => member.GetCustomAttributes(true).OfType().Any()); +TypeAdapterConfig.GlobalSettings.Default + .IgnoreMember((member, side) => !member.GetCustomAttributes(true).OfType().Any()); +``` + +### Turn-off non-public setters using `IgnoreMember` Extension Method + +Mapster always allows non-public setters. But you can override by: + +```csharp +TypeAdapterConfig.GlobalSettings.Default + .IgnoreMember((member, side) => side == MemberSide.Destination + && member.SetterModifier != AccessModifier.Public); +``` diff --git a/docs/articles/settings/custom/Setting-by-attributes.md b/docs/articles/settings/custom/Setting-by-attributes.md new file mode 100644 index 00000000..b81dcd22 --- /dev/null +++ b/docs/articles/settings/custom/Setting-by-attributes.md @@ -0,0 +1,108 @@ +--- +uid: Mapster.Settings.Custom.Attributes +title: "Settings - Setting by attributes" +--- + +## `AdaptIgnore` attribute + +When a property decorated with `[AdaptIgnore]`, that property will be excluded from Mapping. For example, if we would like to exclude price to be mapped. + +```csharp +public class Product { + public string Id { get; set; } + public string Name { get; set; } + + [AdaptIgnore] + public decimal Price { get; set; } +} +``` + +`[AdaptIgnore]` will both ignore when type are used as source or destination. You can ignore only one side by passing `MemberSide`. + +```csharp +public class Product { + public string Id { get; set; } + public string Name { get; set; } + + [AdaptIgnore(MemberSide.Source)] + public decimal Price { get; set; } +} +``` + +Above example, `Price` will be ignored only when `Product` is used as source. + +## `IgnoreAttribute` usage + +You can ignore members annotated with any attributes by using the `IgnoreAttribute` method. + +```csharp +TypeAdapterConfig.GlobalSettings.Default + .IgnoreAttribute(typeof(JsonIgnoreAttribute)); +``` + +However `IgnoreAttribute` will ignore both source and destination. If you would like to ignore only one side, you can use `IgnoreMember`. + +```csharp +config.IgnoreMember((member, side) => member.HasCustomAttribute(typeof(NotMapAttribute)) && side == MemberSide.Source); +``` + +## `AdaptMember` attribute + +`AdaptMember` attribute allows you to customize member mapping by attributes. + +### Map to different name + +With `AdaptMember` attribute, you can specify name of source or target to be mapped. For example, if we would like to map `Id` to `Code`. + +```csharp +public class Product { + [AdaptMember("Code")] + public string Id { get; set; } + public string Name { get; set; } +} +``` + +### Map to non-public members + +By default, Mapster only map public members. You can enable mapping to non-public members by `AdaptMember` attribute: + +```csharp +public class Product { + [AdaptMember] + private string HiddenId { get; set; } + public string Name { get; set; } +} +``` + +### Rename from custom attributes + +You can rename member to be matched by `GetMemberName`. For example, if we would like to rename property based on `JsonProperty` attribute. + +```csharp +TypeAdapterConfig.GlobalSettings.Default + .GetMemberName(member => member.GetCustomAttributes(true) + .OfType() + .FirstOrDefault()?.PropertyName); //if return null, property will not be renamed +``` + +### Using `IncludeAttribute` + +And if we would like to include non-public members decorated with `JsonProperty` attribute, we can do it by `IncludeAttribute`. + +```csharp +TypeAdapterConfig.GlobalSettings.Default + .IncludeAttribute(typeof(JsonPropertyAttribute)); +``` + +### Using the `UseDestinationValue` attribute + +You can tell Mapster to use existing property object to map data rather than create new object. + +```csharp +public class Order { + public string Id { get; set; } + + [UseDestinationValue] + public ICollection Items { get; } = new List(); +} +``` diff --git a/docs/articles/settings/custom/Two-ways.md b/docs/articles/settings/custom/Two-ways.md new file mode 100644 index 00000000..cc0ec0ae --- /dev/null +++ b/docs/articles/settings/custom/Two-ways.md @@ -0,0 +1,60 @@ +--- +uid: Mapster.Settings.Custom.TwoWaysMapping +title: "Settings - Two-ways mapping" +--- + +If you need to map object from POCO to DTO, and map back from DTO to POCO. You can define the setting once by using `TwoWays`. + +```csharp +TypeAdapterConfig + .NewConfig() + .TwoWays() + .Map(dto => dto.Code, poco => poco.Id); //<-- this setting will apply dto.Code = poco.Id & poco.Id = dto.Code for reverse mapping +``` + +NOTE: `TwoWays` command need to call before setting to take effect. + +```csharp +TypeAdapterConfig + .NewConfig() + .Map(dto => dto.Foo, poco => poco.Bar) //<-- this map only apply to Poco->Dto + .TwoWays() + .Map(dto => dto.Foz, poco => poco.Baz); //<-- this map will apply both side +``` + +## Flattening + +By default, Mapster will perform flattening. + +```csharp +class Staff { + public string Name { get; set; } + public Staff Supervisor { get; set; } + ... +} + +struct StaffDto { + public string SupervisorName { get; set; } +} +``` + +Above example, without any setup, you can map from POCO to DTO and you will get `SupervisorName` from `Supervisor.Name`. + +## Using `Unflattening` + +However, unflattening process needed to be defined. You can map to `Supervisor.Name` from `SupervisorName` by `Unflattening` setting. + +```csharp +TypeAdapterConfig.NewConfig() + .Unflattening(true); +``` + +## Using `TwoWays` + +Or you can use `TwoWays` to define both flattening and unflattening in one setting. + +```csharp +TypeAdapterConfig + .NewConfig() + .TwoWays(); //<-- this will also map poco.Supervisor.Name = dto.SupervisorName for reverse mapping +``` diff --git a/docs/articles/settings/custom/toc.yml b/docs/articles/settings/custom/toc.yml new file mode 100644 index 00000000..19010846 --- /dev/null +++ b/docs/articles/settings/custom/toc.yml @@ -0,0 +1,25 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/dotnet/docfx/main/schemas/toc.schema.json +- name: Custom mapping + uid: Mapster.Settings.Custom.Mapping + href: Custom-mapping.md +- name: Custom naming convention + uid: Mapster.Settings.Custom.NamingConvention + href: Naming-convention.md +- name: Setting by attributes + uid: Mapster.Settings.Custom.Attributes + href: Setting-by-attributes.md +- name: Ignoring members + uid: Mapster.Settings.Custom.IgnoringMembers + href: Ignoring-members.md +- name: Rule-based member matching + uid: Mapster.Settings.Custom.RuleBasedMapping + href: Rule-based-member-mapping.md +- name: Mapping readonly prop + uid: Mapster.Settings.Custom.ReadonlyProperty + href: Mapping-readonly-prop.md +- name: Mapping non-public members + uid: Mapster.Settings.Custom.NonPublicMembers + href: Mapping-non-public-members.md +- name: Two ways & unflattening mapping + uid: Mapster.Settings.Custom.TwoWaysMapping + href: Two-ways.md diff --git a/docs/articles/settings/toc.yml b/docs/articles/settings/toc.yml new file mode 100644 index 00000000..2d4a0ea4 --- /dev/null +++ b/docs/articles/settings/toc.yml @@ -0,0 +1,21 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/dotnet/docfx/main/schemas/toc.schema.json +- name: Constructor mapping + uid: Mapster.Settings.ConstructorMapping + href: Constructor-mapping.md +- name: Before & after mapping + uid: Mapster.Settings.BeforeAfterMapping + href: Before-after-mapping.md +- name: Setting values + uid: Mapster.Settings.SettingValues + href: Setting-values.md +- name: Shallow & merge mapping + uid: Mapster.Settings.ShallowMerge + href: Shallow-merge.md +- name: Recursive & object references + uid: Mapster.Settings.ObjectReferences + href: Object-references.md +- name: Custom conversion logic + uid: Mapster.Settings.CustomConversionLogic + href: Custom-conversion-logic.md +- name: Custom member matching logic + href: custom/toc.yml \ No newline at end of file diff --git a/docs/articles/toc.yml b/docs/articles/toc.yml new file mode 100644 index 00000000..245cfb26 --- /dev/null +++ b/docs/articles/toc.yml @@ -0,0 +1,18 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/dotnet/docfx/main/schemas/toc.schema.json +# NOTE: starting from here you can add side navigation items by nesting them under "items" +items: +- name: Mapping + href: mapping/toc.yml + topicHref: xref:Mapster.Mapping.BasicUsages +- name: Configuration + href: configuration/toc.yml + topicHref: xref:Mapster.Configuration.Overview +- name: Settings + href: settings/toc.yml + topicHref: xref:Mapster.Settings.Custom.Mapping +- name: Packages + href: packages/toc.yml + topicHref: xref:Mapster.Packages.Async +- name: Tools + href: tools/toc.yml + topicHref: xref:Mapster.Tools.MapsterTool.Overview \ No newline at end of file diff --git a/docs/articles/tools/TextTemplate.md b/docs/articles/tools/TextTemplate.md new file mode 100644 index 00000000..13d94c30 --- /dev/null +++ b/docs/articles/tools/TextTemplate.md @@ -0,0 +1,107 @@ +--- +uid: Mapster.Tools.TextTemplate +--- + +# Text Template + +```nuget + PM> Install-Package ExpressionTranslator +``` + +Although Mapster allow you to [step-into debugging](xref:Mapster.Packages.ExpressionDebugging), but all mapping are generated at runtime. Therefore, error will be captured at runtime, and we also lose the power of static analysis to find the usage. + +Here are steps to add code generation. + +1. Create text template + + ![add-new-text-item](https://user-images.githubusercontent.com/5763993/56052976-f9377580-5d7c-11e9-841c-0a911fdb3a7f.png) + +2. In template, add references & mapping logic + + ```xml + <#@ template debug="true" language="C#" #> + <#@ output extension=".g.cs" #> + <#@ Assembly Name="netstandard" #> + <#@ Assembly Name="System.Core" #> + <#@ Assembly Name="System.Runtime" #> + <#@ Assembly Name="System.Linq.Expressions" #> + <#@ Assembly Name="$(TargetDir)/$(ProjectName).dll" #> + <#@ Assembly Name="$(TargetDir)/Mapster.dll" #> + <#@ Assembly Name="$(TargetDir)/ExpressionTranslator.dll" #> + <#@ import namespace="ExpressionDebugger" #> + <#@ import namespace="Mapster" #> + <#@ import namespace="YourNamespace" #> + ``` + + ```csharp + <# + //this line is to generate all nested mapping in 1 file + TypeAdapterConfig.GlobalSettings.SelfContainedCodeGeneration = true; + var cust = default(Customer); + var def = new ExpressionDefinitions + { + IsStatic = true, //change to false if you want instance + MethodName = "Map", + Namespace = "YourNamespace", + TypeName = "CustomerMapper" + }; + var code = cust.BuildAdapter() + .CreateMapExpression() + .ToScript(def); + WriteLine(code); + #> + ``` + +3. Generate code by right click on template file, and select `Run Custom Tool`. + +That's it. Done! + +--- + +## Additional Information + +### Links + +- Example: [CustomerMapper]( +https://github.com/MapsterMapper/Mapster/blob/master/src/Benchmark/CustomerMapper.tt) +- [code generation and T4 Documentation](https://docs.microsoft.com/en-us/visualstudio/modeling/code-generation-and-t4-text-templates?view=vs-2019) (Microsoft) + +### Q & A + +Q: How to pass lambda to Before/After mapping? +A: Please use [`BeforeMappingInline` and `AfterMappingInline` instead](xref:Mapster.Settings.BeforeAfterMapping) + +Q: Can I generate multiple outputs from single template? +A: You can: [How to: Create multiple output files from a single t4 template using tangible edi](https://stackoverflow.com/questions/33575419/how-to-create-multiple-output-files-from-a-single-t4-template-using-tangible-edi) + +Q: After running template file, it said library XXX not found. +A: Some unused libraries will be excluded during build. You can direct reference to dll in template file. Or [tell Visual Studio to copy all reference libraries to output](https://stackoverflow.com/questions/43837638/how-to-get-net-core-projects-to-copy-nuget-references-to-build-output/43841481). + +```xml + +true + + +true +``` + +Q: After running template file on Mac, it said `netstandard` is not found. +A: You need direct reference. + +```xml + +<#@ Assembly Name="netstandard" #> + + +<#@ Assembly Name="/usr/local/share/dotnet/sdk/2.2.103/Microsoft/Microsoft.NET.Build.Extensions/net461/lib/netstandard.dll" #> +``` + +Q: After running template file in .NET Core project on Windows, it said, System.Runtime version 4.2.x.x not found. +A: You can build using .NET Framework version. Otherwise, [you need to update assembly binding in Visual Studio config file](https://stackoverflow.com/questions/51550265/t4-template-could-not-load-file-or-assembly-system-runtime-version-4-2-0-0) + +Q: After running template file, it said Compile items are duplicated. +A: You can set property to skip generated items. + +```xml +**/*.g.cs +``` diff --git a/docs/articles/tools/mapster-tool/Attribute-base-Code-generation.md b/docs/articles/tools/mapster-tool/Attribute-base-Code-generation.md new file mode 100644 index 00000000..07dcd00e --- /dev/null +++ b/docs/articles/tools/mapster-tool/Attribute-base-Code-generation.md @@ -0,0 +1,120 @@ +--- +uid: Mapster.Tools.MapsterTool.AttributesDtoGeneration +title: Mapster Tool +--- + +## Attribute-based Dto model generation + +Annotate your class with `[AdaptFrom]`, `[AdaptTo]`, or `[AdaptTwoWays]`. + +Example: + +```csharp +[AdaptTo("[name]Dto")] +public class Student { + ... +} +``` + +Then Mapster will generate: + +```csharp +public class StudentDto { + ... +} +``` + +### Ignore some properties on generation + +By default, code generation will ignore properties that annotated `[AdaptIgnore]` attribute. But you can add more settings which include `IgnoreAttributes`, `IgnoreNoAttributes`, `IgnoreNamespaces`. + +Example: + +```csharp +[AdaptTo("[name]Dto", IgnoreNoAttributes = new[] { typeof(DataMemberAttribute) })] +public class Student { + + [DataMember] + public string Name { get; set; } //this property will be generated + + public string LastName { get; set; } //this will not be generated +} +``` + +### Change property types + +By default, if property type annotated with the same adapt attribute, code generation will forward to that type. (For example, `Student` has `ICollection`, after code generation `StudentDto` will has `ICollection`). + +You can override this by `[PropertyType(typeof(Target))]` attribute. This annotation can be annotated to either on property or on class. + +For example: + +```csharp +[AdaptTo("[name]Dto")] +public class Student { + public ICollection Enrollments { get; set; } +} + +[AdaptTo("[name]Dto"), PropertyType(typeof(DataItem))] +public class Enrollment { + [PropertyType(typeof(string))] + public Grade? Grade { get; set; } +} +``` + +This will generate: + +```csharp +public class StudentDto { + public ICollection Enrollments { get; set; } +} +public class EnrollmentDto { + public string Grade { get; set; } +} +``` + +#### Generate readonly properties + +For `[AdaptTo]` and `[AdaptTwoWays]`, you can generate readonly properties with `MapToConstructor` setting. + +For example: + +```csharp +[AdaptTo("[name]Dto", MapToConstructor = true)] +public class Student { + public string Name { get; set; } +} +``` + +This will generate: + +```csharp +public class StudentDto { + public string Name { get; } + + public StudentDto(string name) { + this.Name = name; + } +} +``` + +### Generate nullable properties + +For `[AdaptFrom]`, you can generate nullable properties with `IgnoreNullValues` setting. + +For example: + +```csharp +[AdaptFrom("[name]Merge", IgnoreNullValues = true)] +public class Student { + public int Age { get; set; } +} +``` + +This will generate: + +```csharp +public class StudentMerge { + public int? Age { get; set; } +} +``` diff --git a/docs/articles/tools/mapster-tool/Attribute-based-Extension-generation.md b/docs/articles/tools/mapster-tool/Attribute-based-Extension-generation.md new file mode 100644 index 00000000..ad4d7a81 --- /dev/null +++ b/docs/articles/tools/mapster-tool/Attribute-based-Extension-generation.md @@ -0,0 +1,32 @@ +--- +uid: Mapster.Tools.MapsterTool.AttributeBasedExtensionGeneration +title: Mapster Tool +--- + +## Generate extension methods via attributes + +### Generate using `[GenerateMapper]` attribute + +For any POCOs annotate with `[AdaptFrom]`, `[AdaptTo]`, or `[AdaptTwoWays]`, you can add `[GenerateMapper]` in order to generate extension methods. + +Example: + +```csharp +[AdaptTo("[name]Dto"), GenerateMapper] +public class Student { + ... +} +``` + +Then Mapster will generate: + +```csharp +public class StudentDto { + ... +} +public static class StudentMapper { + public static StudentDto AdaptToDto(this Student poco) { ... } + public static StudentDto AdaptTo(this Student poco, StudentDto dto) { ... } + public static Expression> ProjectToDto => ... +} +``` diff --git a/docs/articles/tools/mapster-tool/Configuration-based-Code-generation.md b/docs/articles/tools/mapster-tool/Configuration-based-Code-generation.md new file mode 100644 index 00000000..82b92741 --- /dev/null +++ b/docs/articles/tools/mapster-tool/Configuration-based-Code-generation.md @@ -0,0 +1,33 @@ +--- +uid: Mapster.Tools.MapsterTool.ConfigurationBasedCodeGeneration +title: Mapster Tool +--- + +## Configuration-based code generation + +If you have configuration, it must be in `IRegister` + +```csharp +public class MyRegister : IRegister +{ + public void Register(TypeAdapterConfig config) + { + config.NewConfig(); + } +} +``` + +### Advanced configuration + +You can also generate extension methods and add extra settings from configuration. + +```csharp +public class MyRegister : IRegister +{ + public void Register(TypeAdapterConfig config) + { + config.NewConfig() + .GenerateMapper(MapType.Map | MapType.MapToTarget); + } +} +``` diff --git a/docs/articles/tools/mapster-tool/Fluent-API-Code-generation.md b/docs/articles/tools/mapster-tool/Fluent-API-Code-generation.md new file mode 100644 index 00000000..1d4076ef --- /dev/null +++ b/docs/articles/tools/mapster-tool/Fluent-API-Code-generation.md @@ -0,0 +1,175 @@ +--- +uid: Mapster.Tools.MapsterTool.FluentAPI +title: Mapster Tool +--- + +## Configuration class + +Create a configuration class implement `ICodeGenerationRegister`. + +```csharp +public class MyRegister : ICodeGenerationRegister +{ + public void Register(CodeGenerationConfig config) + { + config.AdaptTo("[name]Dto") + .ForAllTypesInNamespace(Assembly.GetExecutingAssembly(), "Sample.CodeGen.Domains"); + + config.GenerateMapper("[name]Mapper") + .ForType() + .ForType(); + } +} +``` + +## Generate models + +Declare `AdaptFrom`, `AdaptTo`, or `AdaptTwoWays`. + +Example: + +```csharp +config.AdaptTo("[name]Dto") + .ForType(); +``` + +Then Mapster will generate: + +```csharp +public class StudentDto { + ... +} +``` + +### Add types to generate + +You can add types by `ForTypes`, `ForAllTypesInNamespace`, `ForType<>`, and you can remove added types using `ExcludeTypes`: + +```csharp +config.AdaptTo("[name]Dto") + .ForAllTypesInNamespace(Assembly.GetExecutingAssembly(), "Sample.CodeGen.Domains") + .ExcludeTypes(typeof(SchoolContext)) + .ExcludeTypes(type => type.IsEnum) +``` + +### Ignore some properties on generation + +By default, code generation will ignore properties that annotated `[AdaptIgnore]` attribute. But you can add more settings which include `IgnoreAttributes`, `IgnoreNoAttributes`, `IgnoreNamespaces`. + +Example: + +```csharp +config.AdaptTo("[name]Dto") + .ForType() + .IgnoreNoAttributes (typeof(DataMemberAttribute)); + +public class Student { + [DataMember] + public string Name { get; set; } //this property will be generated + public string LastName { get; set; } //this will not be generated +} +``` + +### Ignore a property + +```csharp +config.AdaptTo("[name]Dto") + .ForType(cfg => { + cfg.Ignore(poco => poco.LastName); + }); +``` + +### Change a property name, type + +```csharp +config.AdaptTo("[name]Dto") + .ForType(cfg => { + cfg.Map(poco => poco.LastName, "Surname"); //change property name + cfg.Map(poco => poco.Grade, typeof(string)); //change property type + }); +``` + +### Forward property types + +By default, code generation will forward type on the same declaration. (For example, `Student` has `ICollection`, after code generation `StudentDto` will has `ICollection`). + +You can override this by `AlterType`. + +```csharp +config.AdaptTo("[name]Dto") + .ForAllTypesInNamespace(Assembly.GetExecutingAssembly(), "Sample.CodeGen.Domains") + .AlterType(); //forward all Student to Person +``` + +### Generate readonly properties + +For `AdaptTo` and `AdaptTwoWays`, you can generate readonly properties with `MapToConstructor` setting. + +For example: + +```csharp +config.AdaptTo("[name]Dto") + .ForType() + .MapToConstructor(true); +``` + +This will generate: + +```csharp +public class StudentDto { + public string Name { get; } + + public StudentDto(string name) { + this.Name = name; + } +} +``` + +### Generate nullable properties + +For `AdaptFrom`, you can generate nullable properties with `IgnoreNullValues` setting. + +For example: + +```csharp +config.AdaptFrom("[name]Merge") + .ForType() + .IgnoreNullValues(true); +``` + +This will generate: + +```csharp +public class StudentMerge { + public int? Age { get; set; } +} +``` + +## Generate extension methods + +### Generate using `GenerateMapper` + +For any POCOs declared with `AdaptFrom`, `AdaptTo`, or `AdaptTwoWays`, you can declare `GenerateMapper` in order to generate extension methods. + +Example: + +```csharp +config.AdaptTo("[name]Dto") + .ForType(); + +config.GenerateMapper("[name]Mapper") + .ForType(); +``` + +Then Mapster will generate: + +```csharp +public class StudentDto { + ... +} +public static class StudentMapper { + public static StudentDto AdaptToDto(this Student poco) { ... } + public static StudentDto AdaptTo(this Student poco, StudentDto dto) { ... } + public static Expression> ProjectToDto => ... +} +``` diff --git a/docs/articles/tools/mapster-tool/Interface-base-Code-generation.md b/docs/articles/tools/mapster-tool/Interface-base-Code-generation.md new file mode 100644 index 00000000..6fca9b8d --- /dev/null +++ b/docs/articles/tools/mapster-tool/Interface-base-Code-generation.md @@ -0,0 +1,47 @@ +--- +uid: Mapster.Tools.MapsterTool.Interfaces +title: Mapster Tool +--- + +## Generate mapper from interface + +Annotate your interface with `[Mapper]` in order for tool to pickup for generation. + +This is example interface. + +```csharp +[Mapper] +public interface IProductMapper +{ + ProductDTO Map(Product customer); +} +``` + +You can add multiple members as you want. All member names are flexible, but signature must be in following patterns: + +```csharp +[Mapper] +public interface ICustomerMapper +{ + //for queryable + Expression> ProjectToDto { get; } + + //map from POCO to DTO + CustomerDTO MapToDto(Customer customer); + + //map to existing object + Customer MapToExisting(CustomerDTO dto, Customer customer); +} +``` + +If you have configuration, it must be in `IRegister` + +```csharp +public class MyRegister : IRegister +{ + public void Register(TypeAdapterConfig config) + { + config.NewConfig(); + } +} +``` diff --git a/docs/articles/tools/mapster-tool/Mapster-Tool-Overview.md b/docs/articles/tools/mapster-tool/Mapster-Tool-Overview.md new file mode 100644 index 00000000..00d551bb --- /dev/null +++ b/docs/articles/tools/mapster-tool/Mapster-Tool-Overview.md @@ -0,0 +1,197 @@ +--- +uid: Mapster.Tools.MapsterTool.Overview +--- + +# Mapster Tool Overview + +The Mapster.Tool is a command-line tool that helps you generate DTOs (Data Transfer Objects) and mapping code based on your domain models or interfaces. It supports various generation strategies, including Fluent API configuration, attribute-based annotations, and interface-based definitions. + +## Install Mapster.Tool + +```bash +#skip this step if you already have dotnet-tools.json +dotnet new tool-manifest + +dotnet tool install Mapster.Tool +``` + +## Install Mapster + +For lightweight dependency, you can just install `Mapster.Core`. + +```nuget +PM> Install-Package Mapster.Core +``` + +However, if you need `TypeAdapterConfig` for advance configuration, you still need `Mapster`. + +```nuget +PM> Install-Package Mapster +``` + +## Commands + +Mapster.Tool provides 3 commands: + +- **model**: generate models from entities +- **extension**: generate extension methods from entities +- **mapper**: generate mappers from interfaces + +And Mapster.Tool provides following options + +- `-a`: Define input assembly +- `-b`: Specify base namespace for generating dynamic outputs & namespaces +- `-n`: Define namespace of generated classes +- `-o`: Define output directory +- `-p`: Print full type name (if your DTOs/POCOs having the same name) +- `-r`: Generate record types instead of POCO types +- -s: Skip generating existing files + +### csproj integration + +#### Generate manually + +add following code to your `csproj` file: + +```xml + + + + + + + +``` + +to generate run following command on `csproj` file directory: + +```bash +dotnet msbuild -t:Mapster +``` + +#### Generate automatically on build + +add following code to your `csproj` file: + +```xml + + + + + + +``` + +#### Clean up + +add following code to your `csproj` file: + +```xml + + + + + + +``` + +to clean up run following command: + +```bash +dotnet msbuild -t:CleanGenerated +``` + +#### Generate full type name + +If your POCOs and DTOs have the same name, you might need to generate using full type name, by adding `-p` flag: + +```xml + + + + + + + +``` + +#### Dynamic outputs & namespaces + +For example you have following structure. + +```text +Sample.CodeGen +- Domains + - Sub1 + - Domain1 + - Sub2 + - Domain2 +``` + +And if you can specify base namespace as `Sample.CodeGen.Domains`: + +```xml + +``` + +Code will be generated to: + +```text +Sample.CodeGen +- Generated + - Sub1 + - Dto1 + - Sub2 + - Dto2 +``` + +### Generate DTOs and mapping codes + +There are 3 flavors, to generate DTOs and mapping codes: + +- [Fluent API](xref:Mapster.Tools.MapsterTool.FluentAPI): if you don't want to touch your domain classes, or generate DTOs from domain types in different assembly. +- [Attributes](xref:Mapster.Tools.MapsterTool.AttributesDtoGeneration): if you would like to keep mapping declaration close to your domain classes. +- [Interfaces](xref:Mapster.Tools.MapsterTool.Interfaces): if you already have DTOs, and you would like to define mapping through interfaces. + +### Sample + +- https://github.com/MapsterMapper/Mapster/tree/master/src/Sample.CodeGen + +### Troubleshooting + +#### System.IO.FileNotFoundException + +If you get an error similar to `Unhandled exception. System.IO.FileNotFoundException: Could not load file or assembly '...'. The system cannot find the file specified.` and you are using Mapster.Tool 8.4.1 or newer, then you can try one of the following workarounds: + +**Workaround 1** + +Ensure that you are using Mapster.Tool version 8.4.2-pre01 or newer. The latest pre-release version is: [![NuGet](https://img.shields.io/nuget/vpre/Mapster.Tool.svg)](https://www.nuget.org/packages/Mapster.Tool) + +**Workaround 2** + +Add `true` to your csproj file as follows: + +```xml + + + net8.0 + true + + [...] +``` + +**Workaround 3** + +Change your `dotnet build` command to `dotnet build -p:CopyLocalLockFileAssemblies=true` as follows: + +```xml + + [...] + + + [...] + + [...] + +``` diff --git a/docs/articles/tools/mapster-tool/toc.yml b/docs/articles/tools/mapster-tool/toc.yml new file mode 100644 index 00000000..627f1be7 --- /dev/null +++ b/docs/articles/tools/mapster-tool/toc.yml @@ -0,0 +1,19 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/dotnet/docfx/main/schemas/toc.schema.json +- name: Overview and Installation + uid: Mapster.Tools.MapsterTool.Overview + href: Mapster-Tool-Overview.md +- name: Fluent API + uid: Mapster.Tools.MapsterTool.FluentAPI + href: Fluent-API-Code-generation.md +- name: Attribute-based Dto model generation + uid: Mapster.Tools.MapsterTool.AttributesDtoGeneration + href: Attribute-base-Code-generation.md +- name: Configuration based Code generation + uid: Mapster.Tools.MapsterTool.ConfigurationBasedCodeGeneration + href: Configuration-based-Code-generation.md +- name: Attribute based Extensions generation + uid: Mapster.Tools.MapsterTool.AttributeBasedExtensionGeneration + href: Attribute-based-Extension-generation.md +- name: Interfaces + uid: Mapster.Tools.MapsterTool.Interfaces + href: Interface-base-Code-generation.md diff --git a/docs/articles/tools/toc.yml b/docs/articles/tools/toc.yml new file mode 100644 index 00000000..88d5ca38 --- /dev/null +++ b/docs/articles/tools/toc.yml @@ -0,0 +1,8 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/dotnet/docfx/main/schemas/toc.schema.json +items: +- name: Mapster Tool + topicHref: xref:Mapster.Tools.MapsterTool.Overview + href: mapster-tool/toc.yml +- name: Text Template + uid: Mapster.Tools.TextTemplate + href: TextTemplate.md diff --git a/docs/config/filterConfig.yml b/docs/config/filterConfig.yml new file mode 100644 index 00000000..7d1cde95 --- /dev/null +++ b/docs/config/filterConfig.yml @@ -0,0 +1,14 @@ +# YAML-Language Server: $schema=https://raw.githubusercontent.com/dotnet/docfx/main/schemas/filterconfig.schema.json +# Filter configuration file +# More information about filter configuration can be found at https://dotnet.github.io/docfx/docs/dotnet-api-docs.html#custom-filter-rules +# To exclude specific namespaces, types, or members from the generated API documentation, uncomment and modify the rules below, using appropriate regular expressions. +# apiRules: +# - exclude: +# uidRegex: ^PartialNamespace\.NextNamespace$ +# type: Namespace +# - exclude: +# uidRegex: SomeTypeName$ +# type: Type +# - exclude: +# uidRegex: __SomeMemberName(\(.*\))?$ +# type: Member \ No newline at end of file diff --git a/docs/docfx.json b/docs/docfx.json new file mode 100644 index 00000000..4220df2e --- /dev/null +++ b/docs/docfx.json @@ -0,0 +1,111 @@ +{ + "$schema": "https://raw.githubusercontent.com/dotnet/docfx/main/schemas/docfx.schema.json", + // "metadata": [ + // { + // "src": [ + // { + // "files": [ + // "**/Mapster.sln" + // ], + // "src": "../src" + // } + // ], + // "includeExplicitInterfaceImplementations": false, + // "disableDefaultFilter": false, + // // "filter": "config/filterConfig.yml", + // "properties": { + // "TargetFramework" : "net9.0" + // }, + // "noRestore": false, + // "output": "api", + // "memberLayout": "samePage", + // "outputFormat": "apiPage" + // } + // ], + "build": { + "globalMetadata": { + "_appTitle": "Mapster - The Mapper of Your Domain", + "_appName": "Mapster", + "_appFaviconPath": "images/mapster-logo.svg", + "_appLogoPath": "images/mapster-logo.svg", + "_copyrightFooter": "© 2025 Here comes your Name, All rights reserved.", + "_ownerName": "Awesome Developer", + "_githubProfileLink": "https://github.com/MapsterMapper", + "_enableSearch": true, + "_disableAffix": false, + "_disableBreadcrumb": false, + "_disableNextArticle": false, + "_disableTocFilter": false, + "_disableToc": false, + "_lang": "en", + "_gitConribute": { + "branch": "development" + }, + "pdf": false + }, + "content": [ + { + "files": [ + "articles/**.md", + "articles/toc.yml", + "articles/**/toc.yml", + "toc.yml", + "index.md" + ], + "exclude": [ + "**/.include/**", + "articles/_Sidebar.md" + ] + }, + { + "src": "../", + "files": [ + "README.md" + ] + }, + { + "files": [ + + "api/Reference.md" + ], + // Include the following lines, if we are generating metadata API Docs + "exclude": [ + "api/**.yml", + "index.md" + ] + } + ], + "resource": [ + { + "files": [ + "images/*.{png,ico,svg}", + "articles/.assets/*.{png,gif,svg}" + ], + "dot": true + } + ], + "template": [ + "default", + "modern" + ], + "xref": [ "https://learn.microsoft.com/en-us/dotnet/.xrefmap.json" ], + "markdownEngineProperties": { + "markdigExtensions": [ + "yaml", + "definitionlists", + "diagrams", + "advanced", + "alerts", + "footers", + "footnotes", + "medialinks" + ] + }, + "output": "_site", + "sitemap": { + "baseUrl": "https://mapstermapper.github.io/Mapster", + "priority": 0.1, + "changefreq": "monthly" + } + } +} diff --git a/docs/images/mapster-logo.png b/docs/images/mapster-logo.png new file mode 100644 index 00000000..72ea8e10 Binary files /dev/null and b/docs/images/mapster-logo.png differ diff --git a/docs/images/mapster-logo.svg b/docs/images/mapster-logo.svg new file mode 100644 index 00000000..ae2543fd --- /dev/null +++ b/docs/images/mapster-logo.svg @@ -0,0 +1,88 @@ + + + + + + Layer 1 + + + + + + + + + + + + + diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..3e38bb07 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,6 @@ +--- +uid: Mapster.Overview +_layout: landing +--- + +[!INCLUDE [landing-page](../README.md)] diff --git a/docs/toc.yml b/docs/toc.yml new file mode 100644 index 00000000..b42c2d45 --- /dev/null +++ b/docs/toc.yml @@ -0,0 +1,12 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/dotnet/docfx/main/schemas/toc.schema.json +# NOTE: Those top level items will be shown as the Top Navigation Menu Entries +# IMPORTANT: Do not add the explicit toc.yml files under the appropriate folders here, as this would otherwhise attempt to show them in the top navigation instead of the side navigation TOCs. +# See: https://dotnet.github.io/docfx/docs/table-of-contents.html#navigation-bar +- name: Home + uid: Mapster.Overview + href: index.md +- name: Documentation + href: articles/ +- name: Reference + uid: Mapster.References + href: api/Reference.md diff --git a/src/Benchmark.Development/Benchmark.Development.csproj b/src/Benchmark.Development/Benchmark.Development.csproj new file mode 100644 index 00000000..2213d7a4 --- /dev/null +++ b/src/Benchmark.Development/Benchmark.Development.csproj @@ -0,0 +1,37 @@ + + + + Exe + net10.0 + true + enable + enable + True + Benchmark.Development.snk + False + 7.4.0 + 12.0 + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Benchmark.Development/Benchmark.Development.snk b/src/Benchmark.Development/Benchmark.Development.snk new file mode 100644 index 00000000..beae8ece Binary files /dev/null and b/src/Benchmark.Development/Benchmark.Development.snk differ diff --git a/src/Benchmark.Development/Benchmarks/Config.cs b/src/Benchmark.Development/Benchmarks/Config.cs new file mode 100644 index 00000000..87dad0fe --- /dev/null +++ b/src/Benchmark.Development/Benchmarks/Config.cs @@ -0,0 +1,60 @@ +using Benchmark.Development; +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Exporters; +using BenchmarkDotNet.Exporters.Csv; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Loggers; +using Perfolizer.Models; + +namespace Benchmark.Benchmarks +{ + public class Config : ManualConfig + { + public Config() + { + AddLogger(ConsoleLogger.Default); + + AddExporter(CsvExporter.Default); + AddExporter(MarkdownExporter.GitHub); + AddExporter(HtmlExporter.Default); + + AddDiagnoser(MemoryDiagnoser.Default); + AddColumn(TargetMethodColumn.Method); + + AddColumn(JobCharacteristicColumn.AllColumns); + AddColumnProvider(DefaultColumnProviders.Params); + AddColumn(StatisticColumn.Mean); + + AddColumn(StatisticColumn.StdDev); + AddColumn(StatisticColumn.Error); + + AddColumn(BaselineRatioColumn.RatioMean); + AddColumnProvider(DefaultColumnProviders.Metrics); + + + + foreach (var version in MapsterVersion.Get()) + { + AddJob(Job.ShortRun + .WithLaunchCount(1) + .WithWarmupCount(2) + .WithIterationCount(10) + .WithCustomBuildConfiguration("nuget-bench") + .WithMsBuildArguments($"/p:SciVersion={version}") + .WithId($"v{version}") + ); + } + + AddJob(Job.ShortRun + .WithLaunchCount(1) + .WithWarmupCount(2) + .WithIterationCount(10) + .WithCustomBuildConfiguration("developer-bench") + .WithId("developer")); + + Options |= ConfigOptions.JoinSummary; + } + } +} \ No newline at end of file diff --git a/src/Benchmark.Development/Benchmarks/TestAll.cs b/src/Benchmark.Development/Benchmarks/TestAll.cs new file mode 100644 index 00000000..b3e28a76 --- /dev/null +++ b/src/Benchmark.Development/Benchmarks/TestAll.cs @@ -0,0 +1,30 @@ +using Benchmark.Classes; +using BenchmarkDotNet.Attributes; + +namespace Benchmark.Benchmarks +{ + public class TestAll + { + private Foo _fooInstance; + private Customer _customerInstance; + + [Params(100_000)]//, 1_000_000)] + public int Iterations { get; set; } + + [Benchmark] + public void MapsterTest() + { + TestAdaptHelper.TestMapsterAdapter(_fooInstance, Iterations); + TestAdaptHelper.TestMapsterAdapter(_customerInstance, Iterations); + } + + [GlobalSetup(Target = nameof(MapsterTest))] + public void SetupMapster() + { + _fooInstance = TestAdaptHelper.SetupFooInstance(); + _customerInstance = TestAdaptHelper.SetupCustomerInstance(); + TestAdaptHelper.ConfigureMapster(_fooInstance, MapsterCompilerType.Default); + TestAdaptHelper.ConfigureMapster(_customerInstance, MapsterCompilerType.Default); + } + } +} \ No newline at end of file diff --git a/src/Benchmark.Development/Benchmarks/TestComplexTypes.cs b/src/Benchmark.Development/Benchmarks/TestComplexTypes.cs new file mode 100644 index 00000000..62d7a764 --- /dev/null +++ b/src/Benchmark.Development/Benchmarks/TestComplexTypes.cs @@ -0,0 +1,26 @@ +using Benchmark.Classes; +using BenchmarkDotNet.Attributes; + +namespace Benchmark.Benchmarks +{ + public class TestComplexTypes + { + private Customer _customerInstance; + + [Params(1000, 10_000, 100_000, 1_000_000)] + public int Iterations { get; set; } + + [Benchmark] + public void MapsterTest() + { + TestAdaptHelper.TestMapsterAdapter(_customerInstance, Iterations); + } + + [GlobalSetup(Target = nameof(MapsterTest))] + public void SetupMapster() + { + _customerInstance = TestAdaptHelper.SetupCustomerInstance(); + TestAdaptHelper.ConfigureMapster(_customerInstance, MapsterCompilerType.Default); + } + } +} \ No newline at end of file diff --git a/src/Benchmark.Development/Benchmarks/TestSimpleTypes.cs b/src/Benchmark.Development/Benchmarks/TestSimpleTypes.cs new file mode 100644 index 00000000..8678a8ec --- /dev/null +++ b/src/Benchmark.Development/Benchmarks/TestSimpleTypes.cs @@ -0,0 +1,26 @@ +using Benchmark.Classes; +using BenchmarkDotNet.Attributes; + +namespace Benchmark.Benchmarks +{ + public class TestSimpleTypes + { + private Foo _fooInstance; + + [Params(1000, 10_000, 100_000, 1_000_000)] + public int Iterations { get; set; } + + [Benchmark] + public void MapsterTest() + { + TestAdaptHelper.TestMapsterAdapter(_fooInstance, Iterations); + } + + [GlobalSetup(Target = nameof(MapsterTest))] + public void SetupMapster() + { + _fooInstance = TestAdaptHelper.SetupFooInstance(); + TestAdaptHelper.ConfigureMapster(_fooInstance, MapsterCompilerType.Default); + } + } +} \ No newline at end of file diff --git a/src/Benchmark.Development/Classes/Customer.cs b/src/Benchmark.Development/Classes/Customer.cs new file mode 100644 index 00000000..5fac9cef --- /dev/null +++ b/src/Benchmark.Development/Classes/Customer.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; + +namespace Benchmark.Classes +{ + public class Address + { + public int Id { get; set; } + public string Street { get; set; } + public string City { get; set; } + public string Country { get; set; } + } + + public class AddressDTO + { + public int Id { get; set; } + public string City { get; set; } + public string Country { get; set; } + } + + public class Customer + { + public int Id { get; set; } + public string Name { get; set; } + public decimal? Credit { get; set; } + public Address Address { get; set; } + public Address HomeAddress { get; set; } + public Address[] Addresses { get; set; } + public ICollection
WorkAddresses { get; set; } + } + + public class CustomerDTO + { + public int Id { get; set; } + public string Name { get; set; } + public Address Address { get; set; } + public AddressDTO HomeAddress { get; set; } + public AddressDTO[] Addresses { get; set; } + public List WorkAddresses { get; set; } + public string AddressCity { get; set; } + } +} diff --git a/src/Benchmark.Development/Classes/Foo.cs b/src/Benchmark.Development/Classes/Foo.cs new file mode 100644 index 00000000..063541b8 --- /dev/null +++ b/src/Benchmark.Development/Classes/Foo.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; + +namespace Benchmark.Classes +{ + public class Foo + { + public string Name { get; set; } + + public int Int32 { get; set; } + + public long Int64 { set; get; } + + public int? NullInt { get; set; } + + public float Floatn { get; set; } + + public double Doublen { get; set; } + + public DateTime DateTime { get; set; } + + public Foo Foo1 { get; set; } + + public IEnumerable Foos { get; set; } + + public Foo[] FooArr { get; set; } + + public int[] IntArr { get; set; } + + public IEnumerable Ints { get; set; } + } +} diff --git a/src/Benchmark.Development/Directory.Build.props b/src/Benchmark.Development/Directory.Build.props new file mode 100644 index 00000000..8c415f63 --- /dev/null +++ b/src/Benchmark.Development/Directory.Build.props @@ -0,0 +1,18 @@ + + + + false + + + + chaowlert;eric_swann;andrerav + Copyright (c) $([System.DateTime]::Now.ToString(`yyyy`)) Chaowlert Chaisrichalermpol, Eric Swann, Andreas Ravnestad + false + + MIT + false + true + false + 12 + + \ No newline at end of file diff --git a/src/Benchmark.Development/MapsterVersion.cs b/src/Benchmark.Development/MapsterVersion.cs new file mode 100644 index 00000000..bfca6a69 --- /dev/null +++ b/src/Benchmark.Development/MapsterVersion.cs @@ -0,0 +1,12 @@ +namespace Benchmark.Development +{ + internal static class MapsterVersion + { + + internal static string[] Get() => + [ + "7.4.0", + "9.0.0-pre01" + ]; + } +} diff --git a/src/Benchmark.Development/Program.cs b/src/Benchmark.Development/Program.cs new file mode 100644 index 00000000..32e641ed --- /dev/null +++ b/src/Benchmark.Development/Program.cs @@ -0,0 +1,11 @@ +using Benchmark.Benchmarks; +using BenchmarkDotNet.Running; + +var switcher = new BenchmarkSwitcher(new[] + { + typeof(TestSimpleTypes), + typeof(TestComplexTypes), + typeof(TestAll), + }); + +switcher.Run(args, new Config()); diff --git a/src/Benchmark.Development/TestAdaptHelper.cs b/src/Benchmark.Development/TestAdaptHelper.cs new file mode 100644 index 00000000..c6e06739 --- /dev/null +++ b/src/Benchmark.Development/TestAdaptHelper.cs @@ -0,0 +1,105 @@ +using Benchmark.Classes; +using Mapster; +using System.Linq.Expressions; + +namespace Benchmark +{ + public static class TestAdaptHelper + { + + public static Customer SetupCustomerInstance() + { + return new Customer + { + Address = new Address { City = "istanbul", Country = "turkey", Id = 1, Street = "istiklal cad." }, + HomeAddress = new Address { City = "istanbul", Country = "turkey", Id = 2, Street = "istiklal cad." }, + Id = 1, + Name = "Eduardo Najera", + Credit = 234.7m, + WorkAddresses = new List
+ { + new Address {City = "istanbul", Country = "turkey", Id = 5, Street = "istiklal cad."}, + new Address {City = "izmir", Country = "turkey", Id = 6, Street = "konak"} + }, + Addresses = new[] + { + new Address {City = "istanbul", Country = "turkey", Id = 3, Street = "istiklal cad."}, + new Address {City = "izmir", Country = "turkey", Id = 4, Street = "konak"} + } + }; + } + + public static Foo SetupFooInstance() + { + return new Foo + { + Name = "foo", + Int32 = 12, + Int64 = 123123, + NullInt = 16, + DateTime = DateTime.Now, + Doublen = 2312112, + Foo1 = new Foo { Name = "foo one" }, + Foos = new List + { + new Foo {Name = "j1", Int64 = 123, NullInt = 321}, + new Foo {Name = "j2", Int32 = 12345, NullInt = 54321}, + new Foo {Name = "j3", Int32 = 12345, NullInt = 54321} + }, + FooArr = new[] + { + new Foo {Name = "a1"}, + new Foo {Name = "a2"}, + new Foo {Name = "a3"} + }, + IntArr = new[] { 1, 2, 3, 4, 5 }, + Ints = new[] { 7, 8, 9 } + }; + } + + private static readonly Func _defaultCompiler = TypeAdapterConfig.GlobalSettings.Compiler; + + private static void SetupCompiler(MapsterCompilerType type) + { + TypeAdapterConfig.GlobalSettings.Compiler = type switch + { + MapsterCompilerType.Default => _defaultCompiler, + // MapsterCompilerType.Roslyn => exp => exp.CompileWithDebugInfo(), + // MapsterCompilerType.FEC => exp => exp.CompileFast(), + _ => throw new ArgumentOutOfRangeException(nameof(type)), + }; + } + public static void ConfigureMapster(Foo fooInstance, MapsterCompilerType type) + { + SetupCompiler(type); + TypeAdapterConfig.GlobalSettings.Compile(typeof(Foo), typeof(Foo)); //recompile + fooInstance.Adapt(); //exercise + } + + public static void ConfigureMapster(Customer customerInstance, MapsterCompilerType type) + { + SetupCompiler(type); + TypeAdapterConfig.GlobalSettings.Compile(typeof(Customer), typeof(CustomerDTO)); //recompile + customerInstance.Adapt(); //exercise + } + + public static void TestMapsterAdapter(TSrc item, int iterations) + where TSrc : class + where TDest : class, new() + { + Loop(item, get => get.Adapt(), iterations); + } + + private static void Loop(T item, Action action, int iterations) + { + for (var i = 0; i < iterations; i++) action(item); + } + } + + public enum MapsterCompilerType + { + Default, + Roslyn, + FEC, + } +} \ No newline at end of file diff --git a/src/Benchmark/Benchmark.csproj b/src/Benchmark/Benchmark.csproj index 739dc721..ba4b6b9f 100644 --- a/src/Benchmark/Benchmark.csproj +++ b/src/Benchmark/Benchmark.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net9.0 true **/*.g.cs @@ -19,11 +19,11 @@ - + - - + + diff --git a/src/ExpressionDebugger/ExpressionDebugger.csproj b/src/ExpressionDebugger/ExpressionDebugger.csproj index 530de3d6..59e98815 100644 --- a/src/ExpressionDebugger/ExpressionDebugger.csproj +++ b/src/ExpressionDebugger/ExpressionDebugger.csproj @@ -1,7 +1,7 @@  - net6.0;net7.0 + net10.0;net9.0;net8.0; True Chaowlert Chaisrichalermpol Step into debugging from linq expressions @@ -12,14 +12,14 @@ True true ExpressionDebugger.snk - 2.2.0 + 10.0.0-pre01 https://github.com/chaowlert/ExpressionDebugger/blob/master/LICENSE 8.0 enable - + diff --git a/src/ExpressionTranslator/ExpressionTranslator.csproj b/src/ExpressionTranslator/ExpressionTranslator.csproj index 8d4f5de6..d2da32b5 100644 --- a/src/ExpressionTranslator/ExpressionTranslator.csproj +++ b/src/ExpressionTranslator/ExpressionTranslator.csproj @@ -1,7 +1,7 @@  - net6.0;net7.0 + net10.0;net9.0;net8.0 True Chaowlert Chaisrichalermpol Translate from linq expressions to C# code @@ -12,7 +12,7 @@ True true ExpressionTranslator.snk - 2.4.3 + 10.0.0-pre01 ExpressionDebugger MIT icon.png @@ -26,6 +26,6 @@ - + diff --git a/src/Mapster.Async.Tests/AsyncTest.cs b/src/Mapster.Async.Tests/AsyncTest.cs index 1d85146d..caefe5ce 100644 --- a/src/Mapster.Async.Tests/AsyncTest.cs +++ b/src/Mapster.Async.Tests/AsyncTest.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using MapsterMapper; using Microsoft.VisualStudio.TestTools.UnitTesting; using Shouldly; @@ -87,6 +88,22 @@ public async Task NestedAsync() dtoOwnership.Owner.Name.ShouldBe("John Doe"); } + [TestMethod] + public async Task SimplyAsync() + { + TypeAdapterConfig.NewConfig() + .AfterMappingAsync(async dest => { dest.Name = await GetName(); }); + + var poco = new Poco { Id = "foo" }; + var dto = await poco.AdaptAsync(); + dto.Name.ShouldBe("bar"); + + IMapper instance = new Mapper(); + + var destination = await instance.MapAsync(poco); + destination.Name.ShouldBe("bar"); + } + private static async Task GetName() { await Task.Delay(1); diff --git a/src/Mapster.Async.Tests/Mapster.Async.Tests.csproj b/src/Mapster.Async.Tests/Mapster.Async.Tests.csproj index fea91e85..c031e5cf 100644 --- a/src/Mapster.Async.Tests/Mapster.Async.Tests.csproj +++ b/src/Mapster.Async.Tests/Mapster.Async.Tests.csproj @@ -1,16 +1,16 @@ - + - net7.0;net6.0 - + net10.0;net9.0;net8.0; + true false - - - - + + + + diff --git a/src/Mapster.Async/Mapster.Async.csproj b/src/Mapster.Async/Mapster.Async.csproj index 132ef0a5..8c980a39 100644 --- a/src/Mapster.Async/Mapster.Async.csproj +++ b/src/Mapster.Async/Mapster.Async.csproj @@ -1,13 +1,13 @@  - net7.0;net6.0 + net10.0;net9.0;net8.0; Async supports for Mapster true Mapster;Async true Mapster.Async.snk - 2.0.1-pre02 + 10.0.0-pre01 diff --git a/src/Mapster.Async/TypeAdapterExtensions.cs b/src/Mapster.Async/TypeAdapterExtensions.cs index b42a754e..6797c1b2 100644 --- a/src/Mapster.Async/TypeAdapterExtensions.cs +++ b/src/Mapster.Async/TypeAdapterExtensions.cs @@ -1,4 +1,5 @@ -using System; +using MapsterMapper; +using System; using System.Collections.Generic; using System.Threading.Tasks; @@ -101,5 +102,44 @@ public static async Task AdaptToAsync(this IAdapterB } } + + /// + /// Map asynchronously to destination type. + /// + /// Destination type to map. + /// + /// Type of destination object that mapped. + public static async Task AdaptAsync(this object? source) + { + return await source.BuildAdapter().AdaptToTypeAsync(); + } + + + /// + /// Map asynchronously to destination type. + /// + /// Destination type to map. + /// + /// Configuration + /// Type of destination object that mapped. + public static async Task AdaptAsync(this object? source, TypeAdapterConfig config) + { + return await source.BuildAdapter(config).AdaptToTypeAsync(); + } + + } + + public static class IMapperAsyncExtentions + { + /// + /// Map asynchronously to destination type. + /// + /// Destination type to map. + /// + /// Type of destination object that mapped. + public static async Task MapAsync(this IMapper mapper, object? source) + { + return await mapper.From(source).AdaptToTypeAsync(); + } } } diff --git a/src/Mapster.Core/Attributes/AdaptWithAttribute.cs b/src/Mapster.Core/Attributes/AdaptWithAttribute.cs new file mode 100644 index 00000000..e12a4aea --- /dev/null +++ b/src/Mapster.Core/Attributes/AdaptWithAttribute.cs @@ -0,0 +1,17 @@ +using System; + +namespace Mapster +{ + [AttributeUsage(AttributeTargets.Class + | AttributeTargets.Struct + | AttributeTargets.Property + | AttributeTargets.Field, AllowMultiple = true)] + public class AdaptWithAttribute : Attribute + { + public AdaptDirectives AdaptDirective { get; set; } + public AdaptWithAttribute(AdaptDirectives directive) + { + AdaptDirective = directive; + } + } +} diff --git a/src/Mapster.Core/Enums/AdaptDirectives.cs b/src/Mapster.Core/Enums/AdaptDirectives.cs new file mode 100644 index 00000000..a273b1d6 --- /dev/null +++ b/src/Mapster.Core/Enums/AdaptDirectives.cs @@ -0,0 +1,8 @@ +namespace Mapster +{ + public enum AdaptDirectives + { + None = 0, + DestinationAsRecord = 1 + } +} diff --git a/src/Mapster.Core/Enums/ProjectToTypeAutoMapping.cs b/src/Mapster.Core/Enums/ProjectToTypeAutoMapping.cs new file mode 100644 index 00000000..4e95caa0 --- /dev/null +++ b/src/Mapster.Core/Enums/ProjectToTypeAutoMapping.cs @@ -0,0 +1,11 @@ +using System; + +namespace Mapster.Enums +{ + public enum ProjectToTypeAutoMapping + { + AllTypes = 0, + WithoutCollections = 1, + OnlyPrimitiveTypes = 2, + } +} diff --git a/src/Mapster.Core/Mapster.Core.csproj b/src/Mapster.Core/Mapster.Core.csproj index a15e23cc..715f5974 100644 --- a/src/Mapster.Core/Mapster.Core.csproj +++ b/src/Mapster.Core/Mapster.Core.csproj @@ -1,10 +1,10 @@  Lightweight library for Mapster and Mapster CodeGen - net7.0;net6.0 + netstandard2.0 Mapster.Core - mapster - 1.2.1-pre04 + Mapster + 10.0.0-pre01 enable true true diff --git a/src/Mapster.Core/Utils/ProjectToTypeVisitors.cs b/src/Mapster.Core/Utils/ProjectToTypeVisitors.cs new file mode 100644 index 00000000..5ee11e7a --- /dev/null +++ b/src/Mapster.Core/Utils/ProjectToTypeVisitors.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using System.Linq.Expressions; + +namespace Mapster.Utils +{ + public sealed class TopLevelMemberNameVisitor : ExpressionVisitor + { + public string? MemeberName { get; private set; } + + public override Expression Visit(Expression node) + { + if (node == null) + return null; + switch (node.NodeType) + { + case ExpressionType.MemberAccess: + { + if (string.IsNullOrEmpty(MemeberName)) + MemeberName = ((MemberExpression)node).Member.Name; + + return base.Visit(node); + } + } + + return base.Visit(node); + } + } + + public sealed class QuoteVisitor : ExpressionVisitor + { + public List Quotes { get; private set; } = new(); + + public override Expression Visit(Expression node) + { + if (node == null) + return null; + switch (node.NodeType) + { + case ExpressionType.Quote: + { + Quotes.Add((UnaryExpression)node); + return base.Visit(node); + } + } + + return base.Visit(node); + } + } +} diff --git a/src/Mapster.Core/Utils/RecordTypeIdentityHelper.cs b/src/Mapster.Core/Utils/RecordTypeIdentityHelper.cs new file mode 100644 index 00000000..b65d3742 --- /dev/null +++ b/src/Mapster.Core/Utils/RecordTypeIdentityHelper.cs @@ -0,0 +1,60 @@ +using System; +using System.Linq; +using System.Reflection; + +namespace Mapster.Utils +{ + /// + /// CheckTools from Distinctive features of RecordType according to specification: + /// https://github.com/dotnet/docs/blob/main/docs/csharp/language-reference/builtin-types/record.md + /// + public static class RecordTypeIdentityHelper + { + private static bool IsRecordСonstructor(Type type) + { + var ctors = type.GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public).ToList(); + + if (ctors.Count < 2) + return false; + + var isRecordTypeCtor = + ctors + .Where(x => x.IsFamily == true || (type.IsSealed && x.IsPrivate == true)) // add target from Sealed record + .Any(x => x.GetParameters() + .Any(y => y.ParameterType == type)); + + if (isRecordTypeCtor) + return true; + + return false; + } + + private static bool IsIncludedRecordCloneMethod(Type type) + { + if( type.GetMethod("$")?.MethodImplementationFlags.HasFlag(MethodImplAttributes.IL) == true) + return true; + + return false; + } + + public static bool IsRecordType(Type type) + { + if (IsRecordСonstructor(type) && IsIncludedRecordCloneMethod(type)) + return true; + + return false; + } + + public static bool IsDirectiveTagret(Type type) + { + var arrt = type.GetCustomAttributes()?.FirstOrDefault()?.AdaptDirective; + + if (arrt == null) + return false; + if (arrt == AdaptDirectives.DestinationAsRecord) + return true; + + return false; + } + } +} diff --git a/src/Mapster.DependencyInjection.Tests/Mapster.DependencyInjection.Tests.csproj b/src/Mapster.DependencyInjection.Tests/Mapster.DependencyInjection.Tests.csproj index e0b62dae..4a3b0ed0 100644 --- a/src/Mapster.DependencyInjection.Tests/Mapster.DependencyInjection.Tests.csproj +++ b/src/Mapster.DependencyInjection.Tests/Mapster.DependencyInjection.Tests.csproj @@ -1,17 +1,17 @@ - + - net7.0;net6.0 - + net10.0;net9.0;net8.0; + true false - - - - - + + + + + diff --git a/src/Mapster.DependencyInjection/Mapster.DependencyInjection.csproj b/src/Mapster.DependencyInjection/Mapster.DependencyInjection.csproj index 4723750a..246ad210 100644 --- a/src/Mapster.DependencyInjection/Mapster.DependencyInjection.csproj +++ b/src/Mapster.DependencyInjection/Mapster.DependencyInjection.csproj @@ -1,19 +1,19 @@  - net7.0;net6.0 + net10.0;net9.0;net8.0; Dependency Injection supports for Mapster true Mapster;DependencyInjection true Mapster.DependencyInjection.snk - 1.0.1-pre02 + 10.0.0-pre01 - + diff --git a/src/Mapster.EF6/Mapster.EF6.csproj b/src/Mapster.EF6/Mapster.EF6.csproj index e7b1489b..acfe07f5 100644 --- a/src/Mapster.EF6/Mapster.EF6.csproj +++ b/src/Mapster.EF6/Mapster.EF6.csproj @@ -1,17 +1,17 @@  - net7.0;net6.0 + net10.0;net9.0;net8.0; EF6 plugin for Mapster true Mapster;EF6 True true Mapster.EF6.snk - 2.0.1-pre02 + 10.0.0-pre01 - + diff --git a/src/Mapster.EFCore.Tests/EFCoreTest.cs b/src/Mapster.EFCore.Tests/EFCoreTest.cs index 10ad5507..6fba5c15 100644 --- a/src/Mapster.EFCore.Tests/EFCoreTest.cs +++ b/src/Mapster.EFCore.Tests/EFCoreTest.cs @@ -1,11 +1,12 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Mapster.EFCore.Tests.Models; using MapsterMapper; using Microsoft.EntityFrameworkCore; using Microsoft.VisualStudio.TestTools.UnitTesting; using Shouldly; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; namespace Mapster.EFCore.Tests { @@ -45,6 +46,34 @@ public void TestFindObject() first.Grade.ShouldBe(Grade.F); } + [TestMethod] + public async Task TestFindSingleObjectUsingProjectToType() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString("N")) + .Options; + var context = new SchoolContext(options); + DbInitializer.Initialize(context); + + var mapsterInstance = new Mapper(); + + var query = context.Students.Where(s => s.ID == 1); + + async Task FirstExecute() => + await mapsterInstance.From(query) + .ProjectToType() + .FirstOrDefaultAsync(); + + await Should.NotThrowAsync(async () => + { + var first = await FirstExecute(); + + first.ShouldNotBeNull(); + first.ID.ShouldBe(1); + first.LastName.ShouldBe("Alexander"); + }); + } + [TestMethod] public void MapperInstance_From_OrderBy() { @@ -67,6 +96,27 @@ public void MapperInstance_From_OrderBy() var last = orderedQuery.Last(); last.LastName.ShouldBe("Olivetto"); } + + [TestMethod] + public void MergeIncludeWhenUsingEFCoreProjectToType() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString("N")) + .Options; + var context = new SchoolContext(options); + DbInitializer.Initialize(context); + + var mapsterInstance = new Mapper(); + + var query = context.Students + .Include(x => x.Enrollments.OrderByDescending(x => x.StudentID).Take(1)) + .EFCoreProjectToType(); + + var first = query.First(); + + first.Enrollments.Count.ShouldBe(1); + first.LastName.ShouldBe("Alexander"); + } } public class StudentDto diff --git a/src/Mapster.EFCore.Tests/Mapster.EFCore.Tests.csproj b/src/Mapster.EFCore.Tests/Mapster.EFCore.Tests.csproj index ca407d5b..94549270 100644 --- a/src/Mapster.EFCore.Tests/Mapster.EFCore.Tests.csproj +++ b/src/Mapster.EFCore.Tests/Mapster.EFCore.Tests.csproj @@ -1,21 +1,21 @@ - + - net7.0;net6.0 - + net10.0;net9.0;net8.0; + true false - - - - - + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/Mapster.EFCore/EFCoreExtensions.cs b/src/Mapster.EFCore/EFCoreExtensions.cs new file mode 100644 index 00000000..4ed597f0 --- /dev/null +++ b/src/Mapster.EFCore/EFCoreExtensions.cs @@ -0,0 +1,96 @@ +using Mapster.Enums; +using Mapster.Models; +using Mapster.Utils; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Linq.Expressions; + +namespace Mapster.EFCore +{ + public static class EFCoreExtensions + { + public static IQueryable EFCoreProjectToType(this IQueryable source, + TypeAdapterConfig? config = null, ProjectToTypeAutoMapping autoMapConfig = ProjectToTypeAutoMapping.WithoutCollections) + { + var allInclude = new IncludeVisitor(); + allInclude.Visit(source.Expression); + + if (config == null) + { + config = TypeAdapterConfig.GlobalSettings + .Clone() + .ForType(source.ElementType, typeof(TDestination)) + .Config; + + var mapTuple = new TypeTuple(source.ElementType, typeof(TDestination)); + + TypeAdapterRule rule; + config.RuleMap.TryGetValue(mapTuple, out rule); + + if(rule != null) + { + rule.Settings.ProjectToTypeMapConfig = autoMapConfig; + + foreach (var item in allInclude.IncludeExpression) + { + var find = rule.Settings.Resolvers.Find(x => x.SourceMemberName == item.Key); + if (find != null) + { + find.Invoker = (LambdaExpression)item.Value.Operand; + find.SourceMemberName = null; + } + else + rule.Settings.ProjectToTypeResolvers.TryAdd(item.Key, item.Value); + } + } + } + else + { + config = config.Clone() + .ForType(source.ElementType, typeof(TDestination)) + .Config; + } + + return source.ProjectToType(config); + } + } + + + internal class IncludeVisitor : ExpressionVisitor + { + public Dictionary IncludeExpression { get; protected set; } = new(); + private bool IsInclude(Expression node) => node.Type.Name.StartsWith("IIncludableQueryable"); + + [return: NotNullIfNotNull("node")] + public override Expression Visit(Expression node) + { + if (node == null) + return null; + + switch (node.NodeType) + { + case ExpressionType.Call: + { + if (IsInclude(node)) + { + var QuoteVisiter = new QuoteVisitor(); + QuoteVisiter.Visit(node); + + foreach (var item in QuoteVisiter.Quotes) + { + var memberv = new TopLevelMemberNameVisitor(); + memberv.Visit(item); + + IncludeExpression.TryAdd(memberv.MemeberName, item); + } + } + return base.Visit(node); + } + } + + return base.Visit(node); + } + } + +} diff --git a/src/Mapster.EFCore/Mapster.EFCore.csproj b/src/Mapster.EFCore/Mapster.EFCore.csproj index 40c4294e..d721aa9e 100644 --- a/src/Mapster.EFCore/Mapster.EFCore.csproj +++ b/src/Mapster.EFCore/Mapster.EFCore.csproj @@ -1,18 +1,18 @@  - net7.0;net6.0 + net10.0;net9.0;net8.0; EFCore plugin for Mapster true Mapster;EFCore True true Mapster.EFCore.snk - 5.1.1-pre02 + 10.0.0-pre01 - + diff --git a/src/Mapster.EFCore/MapsterQueryable.cs b/src/Mapster.EFCore/MapsterQueryable.cs index 8d9ffb8a..cc4cd6be 100644 --- a/src/Mapster.EFCore/MapsterQueryable.cs +++ b/src/Mapster.EFCore/MapsterQueryable.cs @@ -82,11 +82,21 @@ public TResult ExecuteAsync(Expression expression, CancellationToken ca { var enumerable = ((IAsyncQueryProvider)_provider).ExecuteAsync(expression, cancellationToken); var enumerableType = typeof(TResult); + if (!IsAsyncEnumerableType(enumerableType)) + { + return enumerable; + } var elementType = enumerableType.GetGenericArguments()[0]; var wrapType = typeof(MapsterAsyncEnumerable<>).MakeGenericType(elementType); return (TResult) Activator.CreateInstance(wrapType, enumerable, _builder); } + private static bool IsAsyncEnumerableType(Type type) + { + return type.GetInterfaces() + .Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>)); + } + public IAsyncEnumerable ExecuteEnumerableAsync(Expression expression, CancellationToken cancellationToken = default) { var enumerable = ((IAsyncQueryProvider)_provider).ExecuteAsync>(expression, cancellationToken); diff --git a/src/Mapster.Immutable.Tests/Mapster.Immutable.Tests.csproj b/src/Mapster.Immutable.Tests/Mapster.Immutable.Tests.csproj index 4b52ed69..027742ff 100644 --- a/src/Mapster.Immutable.Tests/Mapster.Immutable.Tests.csproj +++ b/src/Mapster.Immutable.Tests/Mapster.Immutable.Tests.csproj @@ -1,20 +1,20 @@ - + - net7.0;net6.0 - + net10.0;net9.0;net8.0; + true false - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/Mapster.Immutable/ImmutableAdapter.cs b/src/Mapster.Immutable/ImmutableAdapter.cs index 9ab7a3a5..596cac4d 100644 --- a/src/Mapster.Immutable/ImmutableAdapter.cs +++ b/src/Mapster.Immutable/ImmutableAdapter.cs @@ -69,7 +69,7 @@ protected override Expression CreateBlockExpression(Expression source, Expressio return Expression.Empty(); } - protected override Expression CreateInlineExpression(Expression source, CompileArgument arg) + protected override Expression CreateInlineExpression(Expression source, CompileArgument arg, bool IsRequiredOnly = false) { return CreateInstantiationExpression(source, arg); } diff --git a/src/Mapster.Immutable/Mapster.Immutable.csproj b/src/Mapster.Immutable/Mapster.Immutable.csproj index 0d6addb3..1f36f685 100644 --- a/src/Mapster.Immutable/Mapster.Immutable.csproj +++ b/src/Mapster.Immutable/Mapster.Immutable.csproj @@ -1,20 +1,20 @@  - net7.0;net6.0 + net10.0;net9.0;net8.0; Immutable collection supports for Mapster true Mapster;Immutable true Mapster.Immutable.snk - 1.0.1-pre02 + 10.0.0-pre01 enable - + diff --git a/src/Mapster.JsonNet.Tests/Mapster.JsonNet.Tests.csproj b/src/Mapster.JsonNet.Tests/Mapster.JsonNet.Tests.csproj index b7173dfe..b4c020b6 100644 --- a/src/Mapster.JsonNet.Tests/Mapster.JsonNet.Tests.csproj +++ b/src/Mapster.JsonNet.Tests/Mapster.JsonNet.Tests.csproj @@ -1,16 +1,16 @@ - + - net7.0;net6.0 - + net10.0;net9.0;net8.0; + true false - - - - + + + + diff --git a/src/Mapster.JsonNet/JsonAdapter.cs b/src/Mapster.JsonNet/JsonAdapter.cs index c44fe773..75691f7a 100644 --- a/src/Mapster.JsonNet/JsonAdapter.cs +++ b/src/Mapster.JsonNet/JsonAdapter.cs @@ -51,7 +51,7 @@ protected override Expression CreateBlockExpression(Expression source, Expressio throw new System.NotImplementedException(); } - protected override Expression CreateInlineExpression(Expression source, CompileArgument arg) + protected override Expression CreateInlineExpression(Expression source, CompileArgument arg, bool IsRequiredOnly = false) { throw new System.NotImplementedException(); } diff --git a/src/Mapster.JsonNet/Mapster.JsonNet.csproj b/src/Mapster.JsonNet/Mapster.JsonNet.csproj index 0a22e89b..3201ca43 100644 --- a/src/Mapster.JsonNet/Mapster.JsonNet.csproj +++ b/src/Mapster.JsonNet/Mapster.JsonNet.csproj @@ -1,17 +1,17 @@  - net7.0;net6.0 + net10.0;net9.0;net8.0; Json.net conversion supports for Mapster true Mapster;Json.net true Mapster.JsonNet.snk - 1.1.1-pre03 + 10.0.0-pre01 - + diff --git a/src/Mapster.SourceGenerator/Mapster.SourceGenerator.csproj b/src/Mapster.SourceGenerator/Mapster.SourceGenerator.csproj index f436bada..b4b88947 100644 --- a/src/Mapster.SourceGenerator/Mapster.SourceGenerator.csproj +++ b/src/Mapster.SourceGenerator/Mapster.SourceGenerator.csproj @@ -1,13 +1,13 @@  - net7.0;net6.0 + netstandard2.0 Source generator to generate mapping using Mapster - source-generator;mapster + Mapster;source-generator true Mapster.SourceGenerator.snk https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet5/nuget/v3/index.json ;$(RestoreAdditionalProjectSources) - 6.5.1 + 10.0.0-pre01 enable false @@ -18,12 +18,12 @@ - - + + - + diff --git a/src/Mapster.Tests/Mapster.Tests.csproj b/src/Mapster.Tests/Mapster.Tests.csproj index 1ea56308..d6b78546 100644 --- a/src/Mapster.Tests/Mapster.Tests.csproj +++ b/src/Mapster.Tests/Mapster.Tests.csproj @@ -1,19 +1,21 @@ - net7.0;net6.0 + net10.0;net9.0;net8.0; + true false Mapster.Tests Mapster.Tests.snk true true + 11.0 - - - - - + + + + + diff --git a/src/Mapster.Tests/WhenCtorNullableParamMapping.cs b/src/Mapster.Tests/WhenCtorNullableParamMapping.cs new file mode 100644 index 00000000..bef0b16f --- /dev/null +++ b/src/Mapster.Tests/WhenCtorNullableParamMapping.cs @@ -0,0 +1,115 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shouldly; + +namespace Mapster.Tests +{ + [TestClass] + public class WhenCtorNullableParamMapping + { + [TestMethod] + public void Dto_To_Domain_MapsCorrectly() + { + var config = new TypeAdapterConfig(); + + config.Default.MapToConstructor(true); + config + .NewConfig() + .Include(); + + + var dtoDerived = new DerivedDtoTestClass + { + DerivedProperty = "DerivedValue", + AbstractProperty = "AbstractValue" + }; + + var dto = new DtoTestClass + { + AbstractType = dtoDerived + }; + + var domain = dto.Adapt(config); + + domain.AbstractType.ShouldNotBe(null); + domain.AbstractType.ShouldBeOfType(); + + var domainDerived = (DerivedDomainTestClass)domain.AbstractType; + domainDerived.DerivedProperty.ShouldBe(dtoDerived.DerivedProperty); + domainDerived.AbstractProperty.ShouldBe(dtoDerived.AbstractProperty); + + } + + [TestMethod] + public void Dto_To_Domain_AbstractClassNull_MapsCorrectly() + { + var config = new TypeAdapterConfig(); + + config.Default.MapToConstructor(true); + config + .NewConfig() + .Include(); + + var dto = new DtoTestClass + { + AbstractType = null + }; + + var domain = dto.Adapt(config); + + domain.AbstractType.ShouldBeNull(); + } + + + #region Immutable classes with private setters, map via ctors + private abstract class AbstractDomainTestClass + { + public string AbstractProperty { get; private set; } + + protected AbstractDomainTestClass(string abstractProperty) + { + AbstractProperty = abstractProperty; + } + } + + private class DerivedDomainTestClass : AbstractDomainTestClass + { + public string DerivedProperty { get; private set; } + + /// + public DerivedDomainTestClass(string abstractProperty, string derivedProperty) + : base(abstractProperty) + { + DerivedProperty = derivedProperty; + } + } + + private class DomainTestClass + { + public AbstractDomainTestClass? AbstractType { get; private set; } + + public DomainTestClass( + AbstractDomainTestClass? abstractType) + { + AbstractType = abstractType; + } + } + #endregion + + #region DTO classes + private abstract class AbstractDtoTestClass + { + public string AbstractProperty { get; set; } + } + + private class DerivedDtoTestClass : AbstractDtoTestClass + { + public string DerivedProperty { get; set; } + } + + private class DtoTestClass + { + public AbstractDtoTestClass? AbstractType { get; set; } + } + #endregion + } +} diff --git a/src/Mapster.Tests/WhenExplicitMappingRequired.cs b/src/Mapster.Tests/WhenExplicitMappingRequired.cs index b5d89492..25b43474 100644 --- a/src/Mapster.Tests/WhenExplicitMappingRequired.cs +++ b/src/Mapster.Tests/WhenExplicitMappingRequired.cs @@ -13,6 +13,7 @@ public class WhenExplicitMappingRequired public void TestCleanup() { TypeAdapterConfig.GlobalSettings.RequireExplicitMapping = false; + TypeAdapterConfig.GlobalSettings.RequireExplicitMappingPrimitive = false; TypeAdapterConfig.GlobalSettings.Clear(); } @@ -140,8 +141,60 @@ public void UnmappedChildPocoShouldFailed() setter.Compile(); // Should fail here } + [TestMethod] + public void RequireExplicitMappingPrimitiveWork() + { + TypeAdapterConfig.GlobalSettings.RequireExplicitMappingPrimitive = true; + + TypeAdapterConfig.NewConfig(); + + Should.Throw(() => + { + TypeAdapterConfig.GlobalSettings.Compile(); // throw CompileException + }); + + byte byteSource = 10; + + byteSource.Adapt(); // Should work when the type is mapped to itself + + Should.Throw(() => + { + byteSource.Adapt(); // throw CompileException, Do not map to another primitive type without registering the configuration + }); + + Should.NotThrow(() => + { + TypeAdapterConfig.NewConfig(); + + byteSource.Adapt(); // Not throw CompileException when config is registering + }); + + Should.NotThrow(() => + { + TypeAdapterConfig.NewConfig() + .Map(dest=> dest.MyProperty, src=> int.Parse(src.MyProperty)); + // it work works because int.Parse return Type Int. Type is mapped to itself (int -> int) without config. + + var sourceMapconfig = new Source783() { MyProperty = "128" }; + var resultMapconfig = sourceMapconfig.Adapt(); + + resultMapconfig.MyProperty.ShouldBe(128); + }); + + } + + #region TestClasses + + public class Source783 + { + public string MyProperty { get; set; } = ""; + } + public class Destination783 + { + public int MyProperty { get; set; } + } public enum NameEnum { diff --git a/src/Mapster.Tests/WhenHandlingUnmappedMembers.cs b/src/Mapster.Tests/WhenHandlingUnmappedMembers.cs index f6bcf0fe..6c0b3572 100644 --- a/src/Mapster.Tests/WhenHandlingUnmappedMembers.cs +++ b/src/Mapster.Tests/WhenHandlingUnmappedMembers.cs @@ -19,7 +19,7 @@ public void TestCleanup() public void No_Errors_Thrown_With_Default_Configuration_On_Unmapped_Primitive() { TypeAdapterConfig.GlobalSettings.RequireDestinationMemberSource = false; - TypeAdapterConfig.NewConfig().Compile(); + TypeAdapterConfig.NewConfig().Compile(); var source = new SimplePoco {Id = Guid.NewGuid(), Name = "TestName"}; diff --git a/src/Mapster.Tests/WhenIgnoreMapping.cs b/src/Mapster.Tests/WhenIgnoreMapping.cs index 585e911c..245c4e63 100644 --- a/src/Mapster.Tests/WhenIgnoreMapping.cs +++ b/src/Mapster.Tests/WhenIgnoreMapping.cs @@ -1,4 +1,6 @@ using System; +using System.Linq; +using System.Reflection; using Microsoft.VisualStudio.TestTools.UnitTesting; using Newtonsoft.Json; using Shouldly; @@ -55,6 +57,109 @@ public void TestIgnoreMember() poco2.Name.ShouldBeNull(); } + /// + /// https://github.com/MapsterMapper/Mapster/issues/707 + /// + [TestMethod] + public void WhenClassIgnoreCtorParamGetDefaultValue() + { + var config = new TypeAdapterConfig() + { + RequireDestinationMemberSource = true, + }; + config.Default + .NameMatchingStrategy(new NameMatchingStrategy + { + SourceMemberNameConverter = input => input.ToLowerInvariant(), + DestinationMemberNameConverter = input => input.ToLowerInvariant(), + }) + ; + config + .NewConfig() + .MapToConstructor(GetConstructor()) + .Ignore(e => e.Id); + + var source = new A707 { Text = "test" }; + var dest = new B707(123, "Hello"); + + var docKind = source.Adapt(config); + var mapTotarget = source.Adapt(dest,config); + + docKind.Id.ShouldBe(0); + mapTotarget.Id.ShouldBe(123); + mapTotarget.Text.ShouldBe("test"); + } + + /// + /// https://github.com/MapsterMapper/Mapster/issues/723 + /// + [TestMethod] + public void MappingToIntefaceWithIgnorePrivateSetProperty() + { + TypeAdapterConfig + .NewConfig() + .TwoWays() + .Ignore(dest => dest.Ignore); + + InterfaceDestination723 dataDestination = new Data723() { Inter = "IterDataDestination", Ignore = "IgnoreDataDestination" }; + + Should.NotThrow(() => + { + var isourse = dataDestination.Adapt(); + var idestination = dataDestination.Adapt(); + }); + + } + + #region TestClasses + + public interface InterfaceDestination723 + { + public string Inter { get; set; } + public string Ignore { get; } + } + + public interface InterfaceSource723 + { + public string Inter { get; set; } + } + + private class Data723 : InterfaceSource723, InterfaceDestination723 + { + public string Ignore { get; set; } + + public string Inter { get; set; } + } + + static ConstructorInfo? GetConstructor() + { + var parameterlessCtorInfo = typeof(TDestination).GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public, new Type[0]); + + var ctors = typeof(TDestination).GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + var validCandidateCtors = ctors.Except(new[] { parameterlessCtorInfo }).ToArray(); + var ctorToUse = validCandidateCtors.Length == 1 + ? validCandidateCtors.First() + : validCandidateCtors.OrderByDescending(c => c.GetParameters().Length).First(); + + return ctorToUse; + } + public class A707 + { + public string? Text { get; set; } + } + + public class B707 + { + public int Id { get; private set; } + public string Text { get; private set; } + + public B707(int id, string text) + { + Id = id; + Text = text; + } + } + public class Poco { public Guid Id { get; set; } @@ -67,5 +172,7 @@ public class Dto [JsonIgnore] public string Name { get; set; } } + + #endregion TestClasses } } diff --git a/src/Mapster.Tests/WhenIgnoringConditionally.cs b/src/Mapster.Tests/WhenIgnoringConditionally.cs index a92106f0..9014d133 100644 --- a/src/Mapster.Tests/WhenIgnoringConditionally.cs +++ b/src/Mapster.Tests/WhenIgnoringConditionally.cs @@ -164,6 +164,8 @@ public void IgnoreIf_Apply_To_RecordType() .Compile(); var poco = new SimplePoco { Id = 1, Name = "TestName" }; + + var srt = poco.BuildAdapter().CreateMapToTargetExpression(); var dto = TypeAdapter.Adapt(poco); dto.Id.ShouldBe(1); @@ -187,10 +189,11 @@ public class SimpleDto public string Name { get; set; } } + [AdaptWith(AdaptDirectives.DestinationAsRecord)] public class SimpleRecord { - public int Id { get; } - public string Name { get; } + public int Id { get; private set; } + public string Name { get; private set; } public SimpleRecord(int id, string name) { diff --git a/src/Mapster.Tests/WhenIgnoringNonMapped.cs b/src/Mapster.Tests/WhenIgnoringNonMapped.cs index 59a082ca..320feff1 100644 --- a/src/Mapster.Tests/WhenIgnoringNonMapped.cs +++ b/src/Mapster.Tests/WhenIgnoringNonMapped.cs @@ -12,6 +12,7 @@ public void Should_Ignore_Non_Mapped() { TypeAdapterConfig.NewConfig() .Map(dest => dest.Id, src => src.Id) + .RequireDestinationMemberSource(true) .IgnoreNonMapped(true) .Compile(); diff --git a/src/Mapster.Tests/WhenMappingDerived.cs b/src/Mapster.Tests/WhenMappingDerived.cs index 01bfac41..e6399389 100644 --- a/src/Mapster.Tests/WhenMappingDerived.cs +++ b/src/Mapster.Tests/WhenMappingDerived.cs @@ -44,6 +44,82 @@ public void WhenMappingDerivedWithoutMembers() Assert.AreEqual(inputEntity.Id, result.Id); } + /// + /// https://github.com/MapsterMapper/Mapster/issues/794 + /// + [TestMethod] + public void WhenMapToTargetDerivedWithNullRegression() + { + var config = new TypeAdapterConfig(); + + config + .NewConfig() + .Map(dest => dest.Nested, src => src.NestedDTO) + .IgnoreNonMapped(true) + .IgnoreNullValues(true); + config + .NewConfig() + .Map(dest => dest.SomeBaseProperty, src => src.SomeBasePropertyDTO) + .Include() + .IgnoreNonMapped(true) + .IgnoreNullValues(true); + + config + .NewConfig() + .Map(dest => dest.SomeDerivedProperty, src => src.SomeDerivedPropertyDTO) + .IgnoreNonMapped(true) + .IgnoreNullValues(true); + config + .NewConfig() + .MapWith(src => src.Adapt()); + + + var container = new Container794(); + var containerDTO = new ContainerDTO794(); + + container.Nested = null; + containerDTO.NestedDTO = new DerivedDTO794(); + + containerDTO.Adapt(container, config); + + (container.Nested is Derived794E).ShouldBeTrue(); // is not Base794 type, MapWith is working when Polymorphic mapping to null + } + + internal class Derived794E : Derived794 + { + + } + + internal class Base794 + { + public string SomeBaseProperty { get; set; } + } + + internal class BaseDTO794 + { + public string SomeBasePropertyDTO { get; set; } + } + + internal class Derived794 : Base794 + { + public string SomeDerivedProperty { get; set; } + } + + internal class DerivedDTO794 : BaseDTO794 + { + public string SomeDerivedPropertyDTO { get; set; } + } + + internal class Container794 + { + public Base794 Nested { get; set; } + } + + internal class ContainerDTO794 + { + public BaseDTO794 NestedDTO { get; set; } + } + internal class BaseDto { public long Id { get; set; } diff --git a/src/Mapster.Tests/WhenMappingInitProperty.cs b/src/Mapster.Tests/WhenMappingInitProperty.cs new file mode 100644 index 00000000..6c1352de --- /dev/null +++ b/src/Mapster.Tests/WhenMappingInitProperty.cs @@ -0,0 +1,63 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shouldly; + +namespace Mapster.Tests; + +[TestClass] +public class WhenMappingInitProperty +{ + + #region Tests + /// + /// From Issue #672 + /// https://github.com/MapsterMapper/Mapster/issues/672 + /// + [TestMethod] + public void WhenMappingToHiddenandNewInitFieldDestination() + { + var source = new Source672() { Id = 156}; + var c = source.Adapt(); + var s = source.Adapt(new BDestination()); + + ((ADestination)c).Id.ShouldBe(default); // Hidden Base member is not mapping + s.Id.ShouldBe(156); + } + + [TestMethod] + public void WhenMappingToHiddenandNewInitFieldWithConstructUsing() + { + TypeAdapterConfig.NewConfig().ConstructUsing(_ => new BDestination()); + + + var source = new Source672() { Id = 256 }; + var c = source.Adapt(); + var s = source.Adapt(new BDestination()); + + ((ADestination)c).Id.ShouldBe(default); // Hidden Base member is not mapping + s.Id.ShouldBe(256); + } + + + #endregion Tests + + + #region TestClasses + + class Source672 + { + public long Id { get; init; } + } + + class ADestination + { + public int Id { get; init; } + } + + class BDestination : ADestination + { + public new long Id { get; init; } + } + + + #endregion TestClasses +} diff --git a/src/Mapster.Tests/WhenMappingMemberNameContainingPeriod.cs b/src/Mapster.Tests/WhenMappingMemberNameContainingPeriod.cs new file mode 100644 index 00000000..d86ee07d --- /dev/null +++ b/src/Mapster.Tests/WhenMappingMemberNameContainingPeriod.cs @@ -0,0 +1,277 @@ +using System; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Reflection.Emit; +using System.Runtime.CompilerServices; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Mapster.Tests; + +[TestClass] +public class WhenMappingMemberNameContainingPeriod +{ + private const string MemberName = "Some.Property.With.Periods"; + + [TestMethod] + public void Property_Name_Containing_Periods_Is_Supported() + { + // Create a target type with a property that contains periods + Type targetType = new TestTypeBuilder() + .AddProperty(MemberName) + .CreateType(); + + // Call the local function defined below, the actual test method + CallStaticLocalTestMethod( + nameof(Test), + new Type[] { targetType }); + + // The actual test method adapting Source to the target type and back to the source to verify mapping the property with periods + static void Test() + { + // Get expression for mapping the property with periods + Expression> getPropertyExpression = BuildGetPropertyExpression(MemberName); + + // Create the config + TypeAdapterConfig + .NewConfig() + .TwoWays() + .Map(getPropertyExpression, src => src.Value); + + // Execute the mapping both ways + Source source = new() { Value = 551 }; + TTarget target = source.Adapt(); + Source adaptedSource = target.Adapt(); + + Assert.AreEqual(source.Value, adaptedSource.Value); + } + } + + [TestMethod] + public void Constructor_Parameter_Name_Containing_Periods_Is_Supported() + { + // Create a target type with a property that contains periods + Type targetTypeWithProperty = new TestTypeBuilder() + .AddProperty(MemberName) + .CreateType(); + + // Create a target type with a constructor parameter that contains periods + Type targetTypeWithConstructor = new TestTypeBuilder() + .AddConstructorWithReadOnlyProperty(MemberName) + .CreateType(); + + // Call the local function defined below, the actual test method + CallStaticLocalTestMethod( + nameof(Test), + new Type[] { targetTypeWithProperty, targetTypeWithConstructor }); + + // The actual test method + static void Test() + where TWithProperty : new() + { + // Create the config + TypeAdapterConfig + .NewConfig() + .TwoWays() + .MapToConstructor(true); + + // Create delegate for setting the property value on TWithProperty + Expression> setPropertyExpression = BuildSetPropertyExpression(MemberName); + Action setProperty = setPropertyExpression.Compile(); + + // Create the source object + int value = 551; + TWithProperty source = new(); + setProperty.Invoke(source, value); + + // Map + TWithConstructor target = source.Adapt(); + TWithProperty adaptedSource = target.Adapt(); + + // Create delegate for getting the property from TWithProperty + Expression> getPropertyExpression = BuildGetPropertyExpression(MemberName); + Func getProperty = getPropertyExpression.Compile(); + + // Verify + Assert.AreEqual(value, getProperty.Invoke(adaptedSource)); + } + } + + [TestMethod] + public void Using_Property_Path_String_Is_Supported() + { + // Create a target type with a property that contains periods + Type targetType = new TestTypeBuilder() + .AddProperty(MemberName) + .CreateType(); + + // Create the config, both ways + TypeAdapterConfig + .GlobalSettings + .NewConfig(typeof(Source), targetType) + .Map(MemberName, nameof(Source.Value)); + TypeAdapterConfig + .GlobalSettings + .NewConfig(targetType, typeof(Source)) + .Map(nameof(Source.Value), MemberName); + + // Execute the mapping both ways + Source source = new() { Value = 551 }; + object target = source.Adapt(typeof(Source), targetType); + Source adaptedSource = target.Adapt(); + + Assert.AreEqual(source.Value, adaptedSource.Value); + } + + private static void CallStaticLocalTestMethod(string methodName, Type[] genericArguments, [CallerMemberName] string caller = "Unknown") + { + MethodInfo genericMethodInfo = typeof(WhenMappingMemberNameContainingPeriod) + .GetMethods(BindingFlags.NonPublic | BindingFlags.Static) + .Single(x => x.Name.Contains($"<{caller}>") && x.Name.Contains(methodName)); + + MethodInfo method = genericMethodInfo.MakeGenericMethod(genericArguments); + + method.Invoke(null, null); + } + + private static Expression> BuildGetPropertyExpression(string propertyName) + { + ParameterExpression param = Expression.Parameter(typeof(T), "x"); + MemberExpression property = Expression.Property(param, propertyName); + return Expression.Lambda>(property, param); + } + + private static Expression> BuildSetPropertyExpression(string propertyName) + { + ParameterExpression param = Expression.Parameter(typeof(T), "x"); + ParameterExpression value = Expression.Parameter(typeof(TProperty), "value"); + MemberExpression property = Expression.Property(param, propertyName); + BinaryExpression assign = Expression.Assign(property, value); + return Expression.Lambda>(assign, param, value); + } + + private class Source + { + public int Value { get; set; } + } + + private class TestTypeBuilder + { + private readonly TypeBuilder _typeBuilder; + + public TestTypeBuilder() + { + AssemblyBuilder assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly( + new AssemblyName("Types"), + AssemblyBuilderAccess.Run); + ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule(""); + _typeBuilder = moduleBuilder.DefineType( + "Types.Target", + TypeAttributes.Public | + TypeAttributes.Class | + TypeAttributes.Sealed | + TypeAttributes.AutoClass | + TypeAttributes.AnsiClass | + TypeAttributes.BeforeFieldInit | + TypeAttributes.AutoLayout, + null); + } + + public TestTypeBuilder AddConstructorWithReadOnlyProperty(string parameterName) + { + // Add read-only property + FieldBuilder fieldBuilder = AddProperty(parameterName, false); + + // Build the constructor with the parameter for the property + ConstructorBuilder constructorBuilder = _typeBuilder.DefineConstructor( + MethodAttributes.Public, + CallingConventions.Standard, + new Type[] { typeof(TParameter) }); + + // Define the parameter name + constructorBuilder.DefineParameter(1, ParameterAttributes.None, MemberName); + + ILGenerator constructorIL = constructorBuilder.GetILGenerator(); + + // Call the base class constructor + constructorIL.Emit(OpCodes.Ldarg_0); + constructorIL.Emit(OpCodes.Call, typeof(object).GetConstructor(Type.EmptyTypes)); + + // Set the property value + constructorIL.Emit(OpCodes.Ldarg_0); + constructorIL.Emit(OpCodes.Ldarg_1); + constructorIL.Emit(OpCodes.Stfld, fieldBuilder); + + constructorIL.Emit(OpCodes.Ret); + + return this; + } + + public TestTypeBuilder AddProperty(string propertyName) + { + AddProperty(propertyName, true); + return this; + } + + private FieldBuilder AddProperty(string propertyName, bool addSetter) + { + Type propertyType = typeof(T); + FieldBuilder fieldBuilder = _typeBuilder.DefineField($"_{propertyName}", propertyType, FieldAttributes.Private); + PropertyBuilder propertyBuilder = _typeBuilder.DefineProperty(propertyName, PropertyAttributes.None, propertyType, null); + + AddGetMethod(_typeBuilder, propertyBuilder, fieldBuilder, propertyName, propertyType); + if (addSetter) + { + AddSetMethod(_typeBuilder, propertyBuilder, fieldBuilder, propertyName, propertyType); + } + + return fieldBuilder; + } + + public Type CreateType() => _typeBuilder.CreateType(); + + private static PropertyBuilder AddGetMethod(TypeBuilder typeBuilder, PropertyBuilder propertyBuilder, FieldBuilder fieldBuilder, string propertyName, Type propertyType) + { + MethodBuilder getMethodBuilder = typeBuilder.DefineMethod( + "get_" + propertyName, + MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, + propertyType, + Type.EmptyTypes); + ILGenerator getMethodGenerator = getMethodBuilder.GetILGenerator(); + + getMethodGenerator.Emit(OpCodes.Ldarg_0); + getMethodGenerator.Emit(OpCodes.Ldfld, fieldBuilder); + getMethodGenerator.Emit(OpCodes.Ret); + + propertyBuilder.SetGetMethod(getMethodBuilder); + + return propertyBuilder; + } + + private static PropertyBuilder AddSetMethod(TypeBuilder typeBuilder, PropertyBuilder propertyBuilder, FieldBuilder fieldBuilder, string propertyName, Type propertyType) + { + MethodBuilder setMethodBuilder = typeBuilder.DefineMethod( + $"set_{propertyName}", + MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, + null, + new Type[] { propertyType }); + + ILGenerator setMethodGenerator = setMethodBuilder.GetILGenerator(); + Label modifyProperty = setMethodGenerator.DefineLabel(); + Label exitSet = setMethodGenerator.DefineLabel(); + + setMethodGenerator.MarkLabel(modifyProperty); + setMethodGenerator.Emit(OpCodes.Ldarg_0); + setMethodGenerator.Emit(OpCodes.Ldarg_1); + setMethodGenerator.Emit(OpCodes.Stfld, fieldBuilder); + + setMethodGenerator.Emit(OpCodes.Nop); + setMethodGenerator.MarkLabel(exitSet); + setMethodGenerator.Emit(OpCodes.Ret); + + propertyBuilder.SetSetMethod(setMethodBuilder); + + return propertyBuilder; + } + } +} diff --git a/src/Mapster.Tests/WhenMappingNullableEnumRegression.cs b/src/Mapster.Tests/WhenMappingNullableEnumRegression.cs new file mode 100644 index 00000000..c6b4281e --- /dev/null +++ b/src/Mapster.Tests/WhenMappingNullableEnumRegression.cs @@ -0,0 +1,110 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shouldly; +using System; + +namespace Mapster.Tests +{ + [TestClass] + public class WhenMappingNullableEnumRegression + { + /// + /// https://github.com/MapsterMapper/Mapster/issues/640 + /// + [TestMethod] + public void NullEnumToNullClass() + { + TypeAdapterConfig + .NewConfig() + .MapWith(s => s == null ? null : new KeyValueData(s.ToString(), Enums.Manager)); + + MyClass myClass = new() { TypeEmployer = MyEnum.User }; + + MyClass myClassNull = new() { TypeEmployer = null}; + + + var _result = myClass?.Adapt(); // Work + + var _resultNull = myClassNull.Adapt(); // Null Not Error When (object)s if (MyEnum)s - NullReferenceException + + _result.TypeEmployer.Key.ShouldBe(MyEnum.User.ToString()); + + _resultNull.TypeEmployer.ShouldBeNull(); + } + + /// + /// https://github.com/MapsterMapper/Mapster/issues/640 + /// + [Ignore] // Will work after RecordType fix + [TestMethod] + public void UpdateNullEnumToClass() + { + TypeAdapterConfig + .NewConfig() + .MapWith(s => s == null ? null : new KeyValueData(s.ToString(), Enums.Manager)); + + MyClass myClass = new() { TypeEmployer = MyEnum.User }; + + var mDest2 = new MyDestination() { TypeEmployer = new KeyValueData("Admin", null) }; + + var _MyDestination = myClass?.Adapt(); // Work + var _result = _MyDestination.Adapt(mDest2); + + _result.TypeEmployer.Key.ShouldBe(MyEnum.User.ToString()); + } + } + + #region TestClasses + + class MyDestination + { + public KeyValueData? TypeEmployer { get; set; } + } + + class MyClass + { + public MyEnum? TypeEmployer { get; set; } + } + + enum MyEnum + { + Anonymous = 0, + User = 2, + } + + class FakeResourceManager + { + + } + + class Enums + { + protected Enums(string data) {} + public static FakeResourceManager Manager { get; set; } + } + + record KeyValueData + { + private readonly string? keyHolder; + private string? description; + + public KeyValueData(string key, FakeResourceManager manager) + { + this.keyHolder = key?.ToString(); + Description = manager?.ToString(); + } + + public string Key + { + get => keyHolder!; + set { } + } + + public string? Description + { + get => description; + set => description ??= value; + } + } + + #endregion TestClasses +} diff --git a/src/Mapster.Tests/WhenMappingNullablePrimitives.cs b/src/Mapster.Tests/WhenMappingNullablePrimitives.cs index 6e4bb32e..8d0fec3f 100644 --- a/src/Mapster.Tests/WhenMappingNullablePrimitives.cs +++ b/src/Mapster.Tests/WhenMappingNullablePrimitives.cs @@ -1,6 +1,6 @@ -using System; -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.VisualStudio.TestTools.UnitTesting; using Shouldly; +using System; namespace Mapster.Tests { @@ -134,8 +134,49 @@ public void Can_Map_From_Non_Nullable_Source_To_Nullable_Target() poco.IsImport.GetValueOrDefault().ShouldBeTrue(); } + /// + /// https://github.com/MapsterMapper/Mapster/issues/414 + /// + [TestMethod] + public void MappingNullTuple() + { + TypeAdapterConfig<(string?, string?, Application414), Output414>.NewConfig() + .Map(dest => dest, src => src.Item1) + .Map(dest => dest, src => src.Item2) + .Map(dest => dest.Application, src => src.Item3 == null ? (Application414)null : new Application414() + { + Id = src.Item3.Id, + Name = src.Item3.Name + }); + + (string, string, Application414) source = (null, null, null); + + var result = source.Adapt(); + + result.Item1.ShouldBeNull(); + result.Item2.ShouldBeNull(); + result.Application.ShouldBeNull(); + } + #region TestClasses + + public class Output414 + { + public string Item1 { get; set; } + + public string Item2 { get; set; } + + public Application414 Application { get; set; } + } + + public class Application414 + { + public string Name { get; set; } + + public int Id { get; set; } + } + public class NullablePrimitivesPoco { public Guid Id { get; set; } diff --git a/src/Mapster.Tests/WhenMappingObjectRegression.cs b/src/Mapster.Tests/WhenMappingObjectRegression.cs new file mode 100644 index 00000000..d6cf7776 --- /dev/null +++ b/src/Mapster.Tests/WhenMappingObjectRegression.cs @@ -0,0 +1,156 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shouldly; +using System; + +namespace Mapster.Tests +{ + [TestClass] + public class WhenMappingObjectRegression + { + /// + /// https://github.com/MapsterMapper/Mapster/issues/524 + /// + [TestMethod] + public void TSourceIsObjectUpdate() + { + var source = new Source524 { X1 = 123 }; + var _result = Somemap(source); + + _result.X1.ShouldBe(123); + } + + /// + /// https://github.com/MapsterMapper/Mapster/issues/524 + /// + [TestMethod] + public void TSourceIsObjectUpdateUseDynamicCast() + { + var source = new Source524 { X1 = 123 }; + var _result = SomemapWithDynamic(source); + + _result.X1.ShouldBe(123); + } + + [TestMethod] + public void UpdateManyDest() + { + var source = new Source524 { X1 = 123 }; + var _result = SomemapManyDest(source); + + _result.X1.ShouldBe(123); + _result.X2.ShouldBe(127); + } + + [TestMethod] + public void UpdateToRealObject() + { + var source = new Source524 { X1 = 123 }; + var RealObject = new Object(); + + var _result = source.Adapt(RealObject); + + _result.ShouldBeOfType(); + ((Source524)_result).X1.ShouldBe(source.X1); + + } + + [TestMethod] + public void RealObjectCastToDestination() /// Warning potential Infinity Loop in ObjectAdapter!!! + { + var source = new Source524 { X1 = 123 }; + var RealObject = new Object(); + + var _result = RealObject.Adapt(source); + + _result.ShouldBeOfType(); + ((Source524)_result).X1.ShouldBe(source.X1); + } + + [TestMethod] + public void UpdateObjectInsaider() + { + var _source = new InsaderObject() { X1 = 1 }; + var _Destination = new InsaderObject() { X1 = 2 }; + + var _result = _source.Adapt(_Destination); + + _result.X1.ShouldBe(_source.X1); + } + + [TestMethod] + public void UpdateObjectInsaiderToObject() + { + var _source = new InsaderObject() { X1 = 1 }; + var _Destination = new InsaderObject() { X1 = new Object() }; + + var _result = _source.Adapt(_Destination); + + _result.X1.ShouldBe(_source.X1); + } + + [TestMethod] + public void UpdateObjectInsaiderWhenObjectinTSource() + { + var _source = new InsaderObject() { X1 = new Object() }; + var _Destination = new InsaderObject() { X1 = 3 }; + + var _result = _source.Adapt(_Destination); + + _result.X1.ShouldBe(_source.X1); + } + + + #region TestFunctions + + Dest524 Somemap(object source) + { + var dest = new Dest524 { X1 = 321 }; + var dest1 = source.Adapt(dest); + + return dest; + } + + ManyDest524 SomemapManyDest(object source) + { + var dest = new ManyDest524 { X1 = 321, X2 = 127 }; + var dest1 = source.Adapt(dest); + + return dest; + } + + Dest524 SomemapWithDynamic(object source) + { + var dest = new Dest524 { X1 = 321 }; + var dest1 = source.Adapt(dest, source.GetType(), dest.GetType()); + + return dest; + } + + #endregion TestFunctions + + #region TestClasses + class Source524 + { + public int X1 { get; set; } + } + class Dest524 + { + public int X1 { get; set; } + } + + class ManyDest524 + { + public int X1 { get; set;} + + public int X2 { get; set;} + } + + class InsaderObject + { + public Object X1 { get; set;} + } + + + #endregion TestClasses + } +} diff --git a/src/Mapster.Tests/WhenMappingPrimitiveCustomMappingRegression.cs b/src/Mapster.Tests/WhenMappingPrimitiveCustomMappingRegression.cs new file mode 100644 index 00000000..d694ac67 --- /dev/null +++ b/src/Mapster.Tests/WhenMappingPrimitiveCustomMappingRegression.cs @@ -0,0 +1,147 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shouldly; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Mapster.Tests +{ + [TestClass] + public class WhenMappingPrimitiveCustomMappingRegression + { + [TestMethod] + public void CustomMappingDateTimeToPrimitive() + { + TypeAdapterConfig + .NewConfig() + .MapWith(src => new DateTimeOffset(src).ToUnixTimeSeconds()); + + TypeAdapterConfig + .NewConfig() + .MapWith(src => src.ToShortDateString()); + + var _source = new DateTime(2023, 10, 27, 0, 0, 0, DateTimeKind.Utc); + + var _resultToLong = _source.Adapt(); + var _resultToString = _source.Adapt(); + + _resultToLong.ShouldBe(new DateTimeOffset(new DateTime(2023, 10, 27, 0, 0, 0, DateTimeKind.Utc)).ToUnixTimeSeconds()); + _resultToString.ShouldNotBe(_source.ToString()); + _resultToString.ShouldBe(_source.ToShortDateString()); + } + + /// + /// https://github.com/MapsterMapper/Mapster/issues/561 + /// + [TestMethod] + public void MappingToPrimitiveInsiderWithCustomMapping() + { + TypeAdapterConfig, string?> + .NewConfig() + .MapToTargetWith((source, target) => source.HasValue ? source.Value : target); + + var sourceNull = new Source561 { Name = new Optional561(null) }; + var target = new Source561 { Name = new Optional561("John") }.Adapt(); + + var TargetDestinationFromNull = new Target561() { Name = "Me" }; + var NullToupdateoptional = sourceNull.Adapt(TargetDestinationFromNull); + var _result = sourceNull.Adapt(target); + + target.Name.ShouldBe("John"); + NullToupdateoptional.Name.ShouldBe("Me"); + } + + /// + /// https://github.com/MapsterMapper/Mapster/issues/407 + /// + [TestMethod] + public void MappingDatetimeToLongWithCustomMapping() + { + TypeAdapterConfig + .NewConfig() + .MapWith(src => new DateTimeOffset(src).ToUnixTimeSeconds()); + + TypeAdapterConfig + .NewConfig() + .MapWith(src => new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(src).Date); + + var emptySource = new Source407() { Time = DateTime.UtcNow.Date }; + var fromC1 = new DateTime(2023, 10, 27,0,0,0,DateTimeKind.Utc); + var fromC2 = new DateTimeOffset(new DateTime(2025, 11, 23, 0, 0, 0, DateTimeKind.Utc)).ToUnixTimeSeconds(); + var c1 = new Source407 { Time = fromC1 }; + var c2 = new Destination407 { Time = fromC2 }; + + var _result = c1.Adapt(); + var _resultLongtoDateTime = c2.Adapt(); + + _result.Time.ShouldBe(new DateTimeOffset(new DateTime(2023, 10, 27, 0, 0, 0, DateTimeKind.Utc)).ToUnixTimeSeconds()); + _resultLongtoDateTime.Time.ShouldBe(new DateTime(2025, 11, 23).Date); + } + + /// + /// https://github.com/MapsterMapper/Mapster/issues/407 + /// + [TestMethod] + public void CustomMappingPrimitiveToProjection() + { + TypeAdapterConfig + .NewConfig() + .MapWith(src => new DateTimeOffset(src).ToUnixTimeSeconds()); + + TypeAdapterConfig + .NewConfig() + .MapWith(src => new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(src).Date); + + var _sourceList = new List(); + _sourceList.Add(new Source407 { Time = new DateTime(2023, 10, 27, 0, 0, 0, DateTimeKind.Utc) }); + var _fromC2List = new List(); + _fromC2List.Add(new Destination407 { Time = new DateTimeOffset(new DateTime(2025, 11, 23, 0, 0, 0, DateTimeKind.Utc)).ToUnixTimeSeconds() }); + + var _resultProjectionDateTimeTolong = _sourceList.AsQueryable().ProjectToType().ToList(); + var _resultProjectionLongToDateTime = _fromC2List.AsQueryable().ProjectToType().ToList(); + + _resultProjectionDateTimeTolong[0].Time.ShouldBe(new DateTimeOffset(new DateTime(2023, 10, 27, 0, 0, 0, DateTimeKind.Utc)).ToUnixTimeSeconds()); + _resultProjectionLongToDateTime[0].Time.ShouldBe(new DateTime(2025, 11, 23).Date); + } + + } + + + #region TestClasses + + public class Source407 + { + public DateTime Time { get; set; } + } + + public class Destination407 + { + public long Time { get; set; } + } + + class Optional561 + { + public Optional561(T? value) + { + if (value != null) + HasValue = true; + + Value = value; + } + + public bool HasValue { get; } + public T? Value { get; } + } + + class Source561 + { + public Optional561 Name { get; set; } + } + + class Target561 + { + public string? Name { get; set; } + } + + #endregion TestClasses +} diff --git a/src/Mapster.Tests/WhenMappingPrimitives.cs b/src/Mapster.Tests/WhenMappingPrimitives.cs index 1e8fb91e..b41207e6 100644 --- a/src/Mapster.Tests/WhenMappingPrimitives.cs +++ b/src/Mapster.Tests/WhenMappingPrimitives.cs @@ -60,6 +60,7 @@ public void ValueType_String_Object_Is_Always_Primitive() targetDto.Obj.ShouldBeSameAs(sourceDto.Obj); } + [Ignore] [TestMethod] public void Immutable_Class_With_No_Mapping_Should_Error() { diff --git a/src/Mapster.Tests/WhenMappingPrivateFieldsAndProperties.cs b/src/Mapster.Tests/WhenMappingPrivateFieldsAndProperties.cs index c51ce675..c87dcb07 100644 --- a/src/Mapster.Tests/WhenMappingPrivateFieldsAndProperties.cs +++ b/src/Mapster.Tests/WhenMappingPrivateFieldsAndProperties.cs @@ -61,6 +61,7 @@ public void Should_Map_Private_Field_To_New_Object_Correctly() dto.Name.ShouldBe(customerName); } + [TestMethod] public void Should_Map_Private_Property_To_New_Object_Correctly() { @@ -78,21 +79,20 @@ public void Should_Map_Private_Property_To_New_Object_Correctly() } [TestMethod] - public void Should_Map_To_Private_Fields_Correctly() + public void Should_Map_To_Private_Fields_Correctly() { - SetUpMappingNonPublicFields(); - - var dto = new CustomerDTO + SetUpMappingNonPublicFields(); + + var dto = new CustomerDTOWithPrivateGet { Id = 1, Name = "Customer 1" }; - var customer = dto.Adapt(); + var customer = dto.Adapt(); - Assert.IsNotNull(customer); - Assert.IsTrue(customer.HasId(dto.Id)); - customer.Name.ShouldBe(dto.Name); + customer.HasId().ShouldBe(1); + customer.Name.ShouldBe("Customer 1"); } [TestMethod] @@ -108,9 +108,8 @@ public void Should_Map_To_Private_Properties_Correctly() var customer = dto.Adapt(); - Assert.IsNotNull(customer); - customer.Id.ShouldBe(dto.Id); - Assert.IsTrue(customer.HasName(dto.Name)); + customer.Id.ShouldBe(1); + customer.HasName().ShouldBe("Customer 1"); } [TestMethod] @@ -167,10 +166,10 @@ private static void SetUpMappingNonPublicProperties() public class CustomerWithPrivateField { - private readonly int _id; + private int _id; public string Name { get; private set; } - private CustomerWithPrivateField() { } + public CustomerWithPrivateField() { } public CustomerWithPrivateField(int id, string name) { @@ -178,28 +177,28 @@ public CustomerWithPrivateField(int id, string name) Name = name; } - public bool HasId(int id) + public int HasId() { - return _id == id; + return _id; } } - public class CustomerWithPrivateProperty + public class CustomerWithPrivateProperty { public int Id { get; private set; } private string Name { get; set; } - private CustomerWithPrivateProperty() { } + public CustomerWithPrivateProperty() { } - public CustomerWithPrivateProperty(int id, string name) + public CustomerWithPrivateProperty(int id, string name) { Id = id; Name = name; } - public bool HasName(string name) + public string HasName() { - return Name == name; + return Name; } } @@ -228,6 +227,12 @@ public class CustomerDTO public string Name { get; set; } } + public class CustomerDTOWithPrivateGet + { + public int Id { private get; set; } + public string Name { private get; set; } + } + public class Pet { public string Name { get; set; } diff --git a/src/Mapster.Tests/WhenMappingRecordRegression.cs b/src/Mapster.Tests/WhenMappingRecordRegression.cs new file mode 100644 index 00000000..bdb9b833 --- /dev/null +++ b/src/Mapster.Tests/WhenMappingRecordRegression.cs @@ -0,0 +1,978 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shouldly; +using System; +using System.Collections.Generic; +using static Mapster.Tests.WhenMappingDerived; + +namespace Mapster.Tests +{ + /// + /// Tests for https://github.com/MapsterMapper/Mapster/issues/537 + /// + [TestClass] + public class WhenMappingRecordRegression + { + [TestMethod] + public void AdaptRecordToRecord() + { + TypeAdapterConfig + .NewConfig() + .Ignore(dest => dest.Y); + + var _source = new TestRecord() { X = 700 }; + var _destination = new TestRecordY() { X = 500 , Y = 200 }; + + var _destination2 = new TestRecordY() { X = 300, Y = 400 }; + var _result = _source.Adapt(_destination); + + var result2 = _destination.Adapt(_destination2); + + _result.X.ShouldBe(700); + _result.Y.ShouldBe(200); + object.ReferenceEquals(_result, _destination).ShouldBeFalse(); + } + + [TestMethod] + public void AdaptPositionalRecordToPositionalRecord() + { + var _sourcePositional = new TestRecordPositional(600); + var _destinationPositional = new TestRecordPositional(900); + var _positionalResult = _sourcePositional.Adapt(_destinationPositional); + + _positionalResult.X.ShouldBe(600); + object.ReferenceEquals(_destinationPositional, _positionalResult).ShouldBeFalse(); + } + + [TestMethod] + public void AdaptRecordStructToRecordStruct() + { + var _sourceStruct = new TestRecordStruct() { X = 1000 }; + var _destinationStruct = new TestRecordStruct() { X = 800 }; + var _structResult = _sourceStruct.Adapt(_destinationStruct); + + _structResult.X.ShouldBe(1000); + _destinationStruct.X.Equals(_structResult.X).ShouldBeFalse(); + } + + [TestMethod] + public void AdaptRecordToClass() + { + var _sourсe = new TestRecordPositional(200); + var _destination = new TestClassProtectedCtr(400); + var _result = _sourсe.Adapt(_destination); + + _destination.ShouldBeOfType(); + _destination.X.ShouldBe(200); + object.ReferenceEquals(_destination, _result).ShouldBeTrue(); + } + + [TestMethod] + public void AdaptClassToRecord() + { + var _sourсe = new TestClassProtectedCtr(200); + var _destination = new TestRecordPositional(400); + var _result = _sourсe.Adapt(_destination); + + _destination.ShouldBeOfType(); + _result.X.ShouldBe(200); + object.ReferenceEquals(_destination, _result).ShouldBeFalse(); + } + + [TestMethod] + public void AdaptToSealtedRecord() + { + var _sourceRecord = new TestRecord() { X = 2000 }; + var _destinationSealtedRecord = new TestSealedRecord() { X = 3000 }; + var _RecordResult = _sourceRecord.Adapt(_destinationSealtedRecord); + + _RecordResult.X.ShouldBe(2000); + object.ReferenceEquals(_destinationSealtedRecord, _RecordResult).ShouldBeFalse(); + } + + [TestMethod] + public void AdaptToSealtedPositionalRecord() + { + var _sourceRecord = new TestRecord() { X = 2000 }; + var _destinationSealtedPositionalRecord = new TestSealedRecordPositional(4000); + var _RecordResult = _sourceRecord.Adapt(_destinationSealtedPositionalRecord); + + _RecordResult.X.ShouldBe(2000); + object.ReferenceEquals(_destinationSealtedPositionalRecord, _RecordResult).ShouldBeFalse(); + } + + [TestMethod] + public void AdaptClassToClassPublicCtrIsNotInstanse() + { + var _source = new TestClassPublicCtr(200); + var _destination = new TestClassPublicCtr(400); + var _result = _source.Adapt(_destination); + + _destination.ShouldBeOfType(); + _destination.X.ShouldBe(200); + object.ReferenceEquals(_destination, _result).ShouldBeTrue(); + } + + [TestMethod] + public void AdaptClassToClassProtectdCtrIsNotInstanse() + { + var _source = new TestClassPublicCtr(200); + var _destination = new TestClassProtectedCtr(400); + var _result = _source.Adapt(_destination); + + _destination.ShouldBeOfType(); + _destination.X.ShouldBe(200); + object.ReferenceEquals(_destination, _result).ShouldBeTrue(); + } + + /// + /// https://github.com/MapsterMapper/Mapster/issues/615 + /// + [TestMethod] + public void AdaptClassIncludeStruct() + { + TypeAdapterConfig + .ForType() + .Map(x => x.TestStruct, x => x.SourceWithStruct.TestStruct); + + var source = new SourceWithClass + { + SourceWithStruct = new SourceWithStruct + { + TestStruct = new TestStruct("A") + } + }; + + var destination = source.Adapt(); + destination.TestStruct.Property.ShouldBe("A"); + } + + /// + /// https://github.com/MapsterMapper/Mapster/issues/482 + /// + [TestMethod] + public void AdaptClassToClassFromPrivatePropertyIsNotInstanse() + { + var _source = new TestClassPublicCtr(200); + var _destination = new TestClassProtectedCtrPrivateProperty(400, "Me"); + var _result = _source.Adapt(_destination); + + _destination.ShouldBeOfType(); + _destination.X.ShouldBe(200); + _destination.Name.ShouldBe("Me"); + object.ReferenceEquals(_destination, _result).ShouldBeTrue(); + } + + /// + /// https://github.com/MapsterMapper/Mapster/issues/427 + /// + [TestMethod] + public void UpdateNullable() + { + var _source = new UserAccount("123", "123@gmail.com", new DateTime(2023, 9, 24)); + var _update = new UpdateUser + { + Id = "123", + }; + var configDate = new TypeAdapterConfig(); + + configDate.ForType() + .Map(dest => dest.Modified, src => new DateTime(2025, 9, 24)) + .IgnoreNullValues(true); + + _update.Adapt(_source, configDate); + + var _sourceEmailUpdate = new UserAccount("123", "123@gmail.com", new DateTime(2023, 9, 24)); + var _updateEmail = new UpdateUser + { + Email = "245@gmail.com", + }; + + var config = new TypeAdapterConfig(); + config.ForType() + .IgnoreNullValues(true); + + var _resultEmail = _updateEmail.Adapt(_sourceEmailUpdate, config); + + _source.Id.ShouldBe("123"); + _source.Created.ShouldBe(new DateTime(2023, 9, 24)); + _source.Modified.ShouldBe(new DateTime(2025, 9, 24)); + _source.Email.ShouldBe("123@gmail.com"); + _sourceEmailUpdate.Id.ShouldBe("123"); + _sourceEmailUpdate.Created.ShouldBe(new DateTime(2023, 9, 24)); + _sourceEmailUpdate.Modified.ShouldBe(null); + _sourceEmailUpdate.Email.ShouldBe("245@gmail.com"); + + } + + /// + /// https://github.com/MapsterMapper/Mapster/issues/569 + /// + [TestMethod] + public void ImplicitOperatorCurrentWorkFromClass() + { + var guid = Guid.NewGuid(); + var pocoWithGuid1 = new PocoWithGuid { Id = guid }; + var pocoWithId2 = new PocoWithId { Id = new Id(guid) }; + + var pocoWithId1 = pocoWithGuid1.Adapt(); + var pocoWithGuid2 = pocoWithId2.Adapt(); + + pocoWithId1.Id.ToString().Equals(guid.ToString()).ShouldBeTrue(); + pocoWithGuid2.Id.Equals(guid).ShouldBeTrue(); + + var _result = pocoWithId1.Adapt(pocoWithGuid2); + + _result.Id.ToString().Equals(guid.ToString()).ShouldBeTrue(); // Guid value transmitted + object.ReferenceEquals(_result, pocoWithGuid2).ShouldBeTrue(); // Not created new instanse from class pocoWithGuid2 + _result.ShouldBeOfType(); + + } + + [TestMethod] + public void DetectFakeRecord() + { + var _source = new TestClassPublicCtr(200); + var _destination = new FakeRecord { X = 300 }; + var _result = _source.Adapt(_destination); + _destination.X.ShouldBe(200); + object.ReferenceEquals(_destination, _result).ShouldBeTrue(); + } + + [TestMethod] + public void OnlyInlineRecordWorked() + { + var _sourcePoco = new InlinePoco501() { MyInt = 1 , MyString = "Hello" }; + var _sourceOnlyInitRecord = new OnlyInitRecord501 { MyInt = 2, MyString = "Hello World" }; + + var _resultOnlyinitRecord = _sourcePoco.Adapt(); + var _updateResult = _sourceOnlyInitRecord.Adapt(_resultOnlyinitRecord); + + _resultOnlyinitRecord.MyInt.ShouldBe(1); + _resultOnlyinitRecord.MyString.ShouldBe("Hello"); + _updateResult.MyInt.ShouldBe(2); + _updateResult.MyString.ShouldBe("Hello World"); + } + + [TestMethod] + public void MultyCtorRecordWorked() + { + var _sourcePoco = new InlinePoco501() { MyInt = 1, MyString = "Hello" }; + var _sourceMultyCtorRecord = new MultiCtorRecord (2, "Hello World"); + + var _resultMultyCtorRecord = _sourcePoco.Adapt(); + var _updateResult = _sourceMultyCtorRecord.Adapt(_resultMultyCtorRecord); + + _resultMultyCtorRecord.MyInt.ShouldBe(1); + _resultMultyCtorRecord.MyString.ShouldBe("Hello"); + _updateResult.MyInt.ShouldBe(2); + _updateResult.MyString.ShouldBe("Hello World"); + } + + [TestMethod] + public void MultiCtorAndInlineRecordWorked() + { + var _sourcePoco = new MultiCtorAndInlinePoco() { MyInt = 1, MyString = "Hello", MyEmail = "123@gmail.com", InitData="Test"}; + var _sourceMultiCtorAndInline = new MultiCtorAndInlineRecord(2, "Hello World") { InitData = "Worked", MyEmail = "243@gmail.com" }; + + var _resultMultiCtorAndInline = _sourcePoco.Adapt(); + var _updateResult = _sourceMultiCtorAndInline.Adapt(_resultMultiCtorAndInline); + + _resultMultiCtorAndInline.MyInt.ShouldBe(1); + _resultMultiCtorAndInline.MyString.ShouldBe("Hello"); + _resultMultiCtorAndInline.MyEmail.ShouldBe("123@gmail.com"); + _resultMultiCtorAndInline.InitData.ShouldBe("Test"); + _updateResult.MyInt.ShouldBe(2); + _updateResult.MyString.ShouldBe("Hello World"); + _updateResult.MyEmail.ShouldBe("243@gmail.com"); + _updateResult.InitData.ShouldBe("Worked"); + } + + + [TestMethod] + public void MappingInterfaceToInterface() + { + TypeAdapterConfig + .ForType() + .Map(dest => dest.TempLength, src => src.Temp.Length); + + + var sourceBase = new SampleInterfaceClsBase + { + ActivityData = new SampleActivityData + { + Data = new SampleActivityParsedData + { + Steps = new List { "A", "B", "C" } + }, + Temp = "Temp data" + + } + + }; + var sourceDerived = new SampleInterfaceClsDerived + { + ActivityData = new SampleActivityData + { + Data = new SampleActivityParsedData + { + Steps = new List { "X", "Y", "Z" } + }, + Temp = "Update Temp data" + + } + + }; + + var sourceExt = new SampleInterfaceClsExtentions + { + ActivityData = new SampleActivityDataExtentions + { + Data = new SampleActivityParsedData + { + Steps = new List { "o", "o", "o" } + }, + Temp = "Extentions data", + TempLength = "Extentions data".Length + + } + + }; + + var TargetBase = sourceBase.Adapt(); + var targetDerived = sourceDerived.Adapt(); + var update = targetDerived.Adapt(TargetBase); + + var targetExtention = sourceExt.Adapt(); + + + var updExt = targetDerived.Adapt(targetExtention); + + targetDerived.ShouldNotBeNull(); + targetDerived.ShouldSatisfyAllConditions( + () => targetDerived.ActivityData.ShouldBe(sourceDerived.ActivityData), + () => update.ActivityData.ShouldBe(targetDerived.ActivityData), + + ()=> updExt.ActivityData.ShouldBe(targetExtention.ActivityData), + () => ((SampleActivityDataExtentions)updExt.ActivityData).Temp.ShouldBe(sourceDerived.ActivityData.Temp), + () => ((SampleActivityDataExtentions)updExt.ActivityData).TempLength.ShouldBe(sourceDerived.ActivityData.Temp.Length), + // IActivityData interface and all its derivatives do not provide access to the Data property for all implementations of the SampleActivityData class, + // so this property will not be changed by mapping + () => ((SampleActivityDataExtentions)updExt.ActivityData).Data.ShouldBe(((SampleActivityDataExtentions)targetExtention.ActivityData).Data) + + ); + + } + + /// + /// https://github.com/MapsterMapper/Mapster/issues/456 + /// + [TestMethod] + public void WhenRecordReceivedIgnoreCtorParamProcessing() + { + TypeAdapterConfig.NewConfig() + .Ignore(dest => dest.Name); + + TypeAdapterConfig.NewConfig() + .Ignore(dest => dest.User); + + var userDto = new UserDto456("Amichai"); + var user = new UserRecord456("John"); + var DtoInsider = new DtoInside(userDto); + var UserInsider = new UserInside(user, new UserRecord456("Skot")); + + var map = userDto.Adapt(); + var maptoTarget = userDto.Adapt(user); + + var MapToTargetInsider = DtoInsider.Adapt(UserInsider); + + map.Name.ShouldBeNullOrEmpty(); // Ignore is work set default value + maptoTarget.Name.ShouldBe("John"); // Ignore is work ignored member save value from Destination + MapToTargetInsider.User.Name.ShouldBe("John"); // Ignore is work member save value from Destination + MapToTargetInsider.SecondName.Name.ShouldBe("Skot"); // Unmached member save value from Destination + + } + + [TestMethod] + public void WhenRecordTypeWorksWithUseDestinationValueAndIgnoreNullValues() + { + + TypeAdapterConfig + .NewConfig() + .IgnoreNullValues(true); + + var _source = new SourceFromTestUseDestValue() { X = 300, Y = 200, Name = new StudentNameRecord() { Name = "John" } }; + var result = _source.Adapt(); + + var _sourceFromMapToTarget = new SourceFromTestUseDestValue() { A = 100, X = null, Y = null, Name = null }; + + var txt1 = _sourceFromMapToTarget.BuildAdapter().CreateMapExpression(); + + var txt = _sourceFromMapToTarget.BuildAdapter().CreateMapToTargetExpression(); + + var _resultMapToTarget = _sourceFromMapToTarget.Adapt(result); + + result.A.ShouldBe(0); // default Value - not match + result.S.ShouldBe("Inside Data"); // is not AutoProperty not mod by source + result.Y.ShouldBe(200); // Y is AutoProperty value transmitted from source + result.Name.Name.ShouldBe("John"); // transmitted from source standart method + + _resultMapToTarget.A.ShouldBe(100); + _resultMapToTarget.X.ShouldBe(300); // Ignore NullValues work + _resultMapToTarget.Y.ShouldBe(200); // Ignore NullValues work + _resultMapToTarget.Name.Name.ShouldBe("John"); // Ignore NullValues work + + } + + /// + /// https://github.com/MapsterMapper/Mapster/issues/771 + /// https://github.com/MapsterMapper/Mapster/issues/746 + /// + [TestMethod] + public void FixCtorParamMapping() + { + var sourceRequestPaymentDto = new PaymentDTO771("MasterCard", "1234", "12/99", "234", 12); + var sourceRequestOrderDto = new OrderDTO771(Guid.NewGuid(), Guid.NewGuid(), "order123", sourceRequestPaymentDto); + var db = new Database746(UserID: "256", Password: "123"); + + + var result = new CreateOrderRequest771(sourceRequestOrderDto).Adapt(); + var resultID = db.Adapt(new Database746()); + + + result.Order.Payment.CVV.ShouldBe("234"); + resultID.UserID.ShouldBe("256"); + } + + [TestMethod] + public void RequiredProperty() + { + var source = new Person553 { FirstMidName = "John", LastName = "Dow" }; + var destination = new Person554 { ID = 245, FirstMidName = "Mary", LastName = "Dow" }; + + TypeAdapterConfig.NewConfig() + //.Map(dest => dest.ID, source => 0) + .Ignore(x => x.ID); + + var s = source.BuildAdapter().CreateMapToTargetExpression(); + + var result = source.Adapt(destination); + + result.ID.ShouldBe(245); + result.FirstMidName.ShouldBe(source.FirstMidName); + result.LastName.ShouldBe(source.LastName); + } + + /// + /// https://github.com/MapsterMapper/Mapster/issues/842 + /// + [TestMethod] + public void ClassCtorAutomapingWorking() + { + var source = new TestRecord() { X = 100 }; + var result = source.Adapt(); + + result.X.ShouldBe(100); + } + + /// + /// https://github.com/MapsterMapper/Mapster/issues/842 + /// + [TestMethod] + public void ClassCustomCtorWitoutMapNotWorking() + { + TypeAdapterConfig.GlobalSettings.Clear(); + + var source = new TestRecord() { X = 100 }; + + Should.Throw(() => + { + source.Adapt(); + }); + } + + /// + /// https://github.com/MapsterMapper/Mapster/issues/842 + /// + [TestMethod] + public void ClassCustomCtorWithMapWorking() + { + TypeAdapterConfig.NewConfig() + .Map("y", src => src.X); + + + var source = new TestRecord() { X = 100 }; + var result = source.Adapt(); + + result.X.ShouldBe(100); + } + + /// + /// https://github.com/MapsterMapper/Mapster/issues/842 + /// + [TestMethod] + public void ClassCustomCtorInsiderUpdateWorking() + { + TypeAdapterConfig.NewConfig() + .Map("y", src => src.X); + + var source = new InsiderData() { X = new TestRecord() { X = 100 } }; + var destination = new InsiderWithCtorDestYx(); // null insider + source.Adapt(destination); + + destination.X.X.ShouldBe(100); + } + + /// + /// https://github.com/MapsterMapper/Mapster/issues/842 + /// + [TestMethod] + public void ClassUpdateAutoPropertyWitoutSetterWorking() + { + var source = new TestRecord() { X = 100 }; + var patch = new TestRecord() { X = 200 }; + var result = source.Adapt(); + + patch.Adapt(result); + + result.X.ShouldBe(200); + } + + + #region NowNotWorking + + /// + /// https://github.com/MapsterMapper/Mapster/issues/430 + /// + [Ignore] + [TestMethod] + public void CollectionUpdate() + { + List sources = new() + { + new(541), + new(234) + }; + var destination = new List(); + var _result = sources.Adapt(destination); + + destination.Count.ShouldBe(_result.Count); + } + + #endregion NowNotWorking + + } + + + #region TestClasses + + public sealed record Database746( + string Server = "", + string Name = "", + string? UserID = null, + string? Password = null); + + public record CreateOrderRequest771(OrderDTO771 Order); + + public record CreateOrderCommand771(OrderDTO771 Order); + + + public record OrderDTO771 + ( + Guid Id, + Guid CustomerId, + string OrderName, + PaymentDTO771 Payment + ); + + public record PaymentDTO771 + ( + string CardName, + string CardNumber, + string Expiration, + string CVV, + int PaymentMethod + ); + + public class Person553 + { + + public string LastName { get; set; } + public string FirstMidName { get; set; } + } + + public class Person554 + { + public required int ID { get; set; } + public string LastName { get; set; } + public string FirstMidName { get; set; } + } + + + public class SourceFromTestUseDestValue + { + public int? A { get; set; } + public int? X { get; set; } + public int? Y { get; set; } + public StudentNameRecord Name { get; set; } + } + + + public record TestRecordUseDestValue() + { + private string _s = "Inside Data"; + + public int A { get; set; } + public int X { get; set; } + + [UseDestinationValue] + public int Y { get; } + + [UseDestinationValue] + public string S { get => _s; } + + [UseDestinationValue] + public StudentNameRecord Name { get; } = new StudentNameRecord() { Name = "Marta" }; + } + + public record StudentNameRecord + { + public string Name { get; set; } + } + + public record TestRecordY() + { + public int X { get; set; } + public int Y { get; set; } + } + + public record UserInside(UserRecord456 User, UserRecord456 SecondName); + public record DtoInside(UserDto456 User); + + public record UserRecord456(string Name); + + public record UserDto456(string Name); + + public interface IActivityDataExtentions : IActivityData + { + public int TempLength { get; set; } + } + + public interface IActivityData : IActivityDataBase + { + public string Temp { get; set; } + } + + public interface IActivityDataBase + { + + } + + + public class SampleInterfaceClsExtentions + { + public IActivityDataExtentions? ActivityData { get; set; } + + public SampleInterfaceClsExtentions() + { + + } + + public SampleInterfaceClsExtentions(IActivityDataExtentions data) + { + SetActivityData(data); + } + + public void SetActivityData(IActivityDataExtentions data) + { + ActivityData = data; + } + } + + + + public class SampleInterfaceClsBase + { + public IActivityDataBase? ActivityData { get; set; } + + public SampleInterfaceClsBase() + { + + } + + public SampleInterfaceClsBase(IActivityDataBase data) + { + SetActivityData(data); + } + + public void SetActivityData(IActivityDataBase data) + { + ActivityData = data; + } + } + + public class SampleInterfaceClsDerived + { + public IActivityData? ActivityData { get; set; } + + public SampleInterfaceClsDerived() + { + + } + + public SampleInterfaceClsDerived(IActivityData data) + { + SetActivityData(data); + } + + public void SetActivityData(IActivityData data) + { + ActivityData = data; + } + } + + public class SampleActivityDataExtentions : IActivityDataExtentions + { + public SampleActivityParsedData Data { get; set; } + public string Temp { get; set; } + public int TempLength { get; set; } + } + + public class SampleActivityData : IActivityData + { + public SampleActivityParsedData Data { get; set; } + public string Temp { get; set; } + } + + public class SampleActivityParsedData + { + public List Steps { get; set; } = new List(); + } + + + + class MultiCtorAndInlinePoco + { + public int MyInt { get; set; } + public string MyString { get; set; } + public string MyEmail { get; set; } + public string InitData { get; set; } + } + + record MultiCtorAndInlineRecord + { + public MultiCtorAndInlineRecord(int myInt) + { + MyInt = myInt; + } + + public MultiCtorAndInlineRecord(int myInt, string myString) : this(myInt) + { + MyString = myString; + } + + + public int MyInt { get; private set; } + public string MyString { get; private set; } + public string MyEmail { get; set; } + public string InitData { get; init; } + } + + record MultiCtorRecord + { + public MultiCtorRecord(int myInt) + { + MyInt = myInt; + } + + public MultiCtorRecord(int myInt, string myString) : this(myInt) + { + MyString = myString; + } + + public int MyInt { get; private set; } + public string MyString { get; private set; } + } + + class InlinePoco501 + { + public int MyInt { get; set; } + public string MyString { get; set; } + } + + record OnlyInitRecord501 + { + public int MyInt { get; init; } + public string MyString { get; init; } + } + + class PocoWithGuid + { + public Guid Id { get; init; } + } + + class PocoWithId + { + public Id Id { get; init; } + } + + class Id + { + private readonly Guid _guid; + public Id(Guid id) => _guid = id; + + public static implicit operator Id(Guid value) => new(value); + public static implicit operator Guid(Id value) => value._guid; + + public override string ToString() => _guid.ToString(); + } + + public class FakeRecord + { + protected FakeRecord(FakeRecord fake) { } + public FakeRecord() { } + + public int X { get; set; } + } + + class UserAccount + { + public UserAccount(string id, string email, DateTime created) + { + Id = id; + Email = email; + Created = created; + } + protected UserAccount() { } + + public string Id { get; set; } + public string? Email { get; set; } + public DateTime Created { get; set; } + public DateTime? Modified { get; set; } + } + + class UpdateUser + { + public string? Id { get; set; } + public string? Email { get; set; } + public DateTime? Created { get; set; } + public DateTime? Modified { get; set; } + } + + class DestinationWithStruct + { + public TestStruct TestStruct { get; set; } + } + + class SourceWithClass + { + public SourceWithStruct SourceWithStruct { get; set; } + } + + class SourceWithStruct + { + public TestStruct TestStruct { get; set; } + } + + struct TestStruct + { + public string Property { get; } + public TestStruct(string property) : this() + { + Property = property; + } + } + + class TestClassPublicCtr + { + public TestClassPublicCtr() { } + + public TestClassPublicCtr(int x) + { + X = x; + } + + public int X { get; set; } + } + + class TestClassProtectedCtr + { + protected TestClassProtectedCtr() { } + + public TestClassProtectedCtr(int x) + { + X = x; + } + + public int X { get; set; } + } + + class TestClassProtectedCtrPrivateProperty + { + protected TestClassProtectedCtrPrivateProperty() { } + + public TestClassProtectedCtrPrivateProperty(int x, string name) + { + X = x; + Name = name; + } + + public int X { get; private set; } + + public string Name { get; private set; } + } + + record TestRecord() + { + public int X { set; get; } + } + + record TestRecordPositional(int X); + + record struct TestRecordStruct + { + public int X { set; get; } + } + + /// + /// Different Checked Constructor Attribute From Spec + /// https://learn.microsoft.com/ru-ru/dotnet/csharp/language-reference/proposals/csharp-9.0/records#copy-and-clone-members + /// + sealed record TestSealedRecord() + { + public int X { get; set; } + } + + sealed record TestSealedRecordPositional(int X); + + class AutoCtorDestX + { + public AutoCtorDestX(int x) + { + X = x; + } + + public int X { get; set; } + } + + class AutoCtorDestYx + { + public AutoCtorDestYx(int y) + { + X = y; + } + + public int X { get; } + } + + class InsiderData + { + public TestRecord X { set; get; } + } + + class InsiderWithCtorDestYx + { + public AutoCtorDestYx X { set; get; } + } + + #endregion TestClasses +} diff --git a/src/Mapster.Tests/WhenMappingToInterface.cs b/src/Mapster.Tests/WhenMappingToInterface.cs index 5e6f0358..00af2104 100644 --- a/src/Mapster.Tests/WhenMappingToInterface.cs +++ b/src/Mapster.Tests/WhenMappingToInterface.cs @@ -3,6 +3,8 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json.Serialization; +using Newtonsoft.Json; namespace Mapster.Tests { @@ -266,6 +268,27 @@ public void MappingToInteraceWithReadonlyProps_AllPropsInitialized() ); } + [TestMethod] + public void MappingToInterface_VerifyReadonlyPropsInterfaceRule() + { + SampleInterfaceCls source = new SampleInterfaceCls + { + ActivityData = new SampleActivityData + { + Data = new SampleActivityParsedData + { + Steps = new List { "A", "B", "C" } + } + } + }; + + SampleInterfaceCls target = source.Adapt(); + target.ShouldNotBeNull(); + target.ShouldSatisfyAllConditions( + () => target.ActivityData.ShouldBe(source.ActivityData) + ); + } + public interface IInheritedDtoWithoutProperties : IInheritedDto { } @@ -374,6 +397,42 @@ public class PropertyInitializationTestSource public int Property1 { get; set; } public int Property2 { get; set; } } + + public interface IActivityData + { + + } + + public class SampleInterfaceCls + { + [Newtonsoft.Json.JsonIgnore] + public IActivityData? ActivityData { get; set; } + + public SampleInterfaceCls() + { + + } + + public SampleInterfaceCls(IActivityData data) + { + SetActivityData(data); + } + + public void SetActivityData(IActivityData data) + { + ActivityData = data; + } + } + + public class SampleActivityData : IActivityData + { + public SampleActivityParsedData Data { get; set; } + } + + public class SampleActivityParsedData + { + public List Steps { get; set; } = new List(); + } } } diff --git a/src/Mapster.Tests/WhenMappingWithDictionary.cs b/src/Mapster.Tests/WhenMappingWithDictionary.cs index df38030b..8b53a3f6 100644 --- a/src/Mapster.Tests/WhenMappingWithDictionary.cs +++ b/src/Mapster.Tests/WhenMappingWithDictionary.cs @@ -37,7 +37,6 @@ public void Object_To_Dictionary() dict["Name"].ShouldBe(poco.Name); } - [TestMethod] public void Object_To_Dictionary_Map() { @@ -57,6 +56,25 @@ public void Object_To_Dictionary_Map() dict["Name"].ShouldBe(poco.Name); } + [TestMethod] + public void Object_To_Dictionary_Map_With_Periods() + { + var poco = new SimplePoco + { + Id = Guid.NewGuid(), + Name = "test", + }; + + var config = new TypeAdapterConfig(); + config.NewConfig>() + .Map("Key.With.Periods", c => c.Id); + var dict = poco.Adapt>(config); + + dict.Count.ShouldBe(2); + dict["Key.With.Periods"].ShouldBe(poco.Id); + dict["Name"].ShouldBe(poco.Name); + } + [TestMethod] public void Object_To_Dictionary_CamelCase() { @@ -154,6 +172,23 @@ public void Dictionary_To_Object_Map() poco.Name.ShouldBeNull(); } + [TestMethod] + public void Dictionary_To_Object_Map_With_Periods() + { + var dict = new Dictionary + { + ["Key.With.Periods"] = Guid.NewGuid(), + ["Foo"] = "test", + }; + + TypeAdapterConfig, SimplePoco>.NewConfig() + .Map(c => c.Id, "Key.With.Periods"); + + var poco = TypeAdapter.Adapt(dict); + poco.Id.ShouldBe(dict["Key.With.Periods"]); + poco.Name.ShouldBeNull(); + } + [TestMethod] public void Dictionary_To_Object_CamelCase() { diff --git a/src/Mapster.Tests/WhenMappingWithOpenGenerics.cs b/src/Mapster.Tests/WhenMappingWithOpenGenerics.cs index bd8f28fe..88cdc66e 100644 --- a/src/Mapster.Tests/WhenMappingWithOpenGenerics.cs +++ b/src/Mapster.Tests/WhenMappingWithOpenGenerics.cs @@ -1,10 +1,5 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Shouldly; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Mapster.Tests { @@ -36,6 +31,60 @@ public void Setting_From_OpenGeneric_Has_No_SideEffect() var cCopy = c.Adapt(config); } + [TestMethod] + public void MapOpenGenericsUseInherits() + { + TypeAdapterConfig.GlobalSettings + .ForType(typeof(GenericPoco<>), typeof(GenericDto<>)) + .Map("value", "Value"); + + TypeAdapterConfig.GlobalSettings + .ForType(typeof(DerivedPoco<>), typeof(DerivedDto<>)) + .Map("derivedValue", "DerivedValue") + .Inherits(typeof(GenericPoco<>), typeof(GenericDto<>)); + + var poco = new DerivedPoco { Value = 123 , DerivedValue = 42 }; + var dto = poco.Adapt>(); + dto.value.ShouldBe(poco.Value); + dto.derivedValue.ShouldBe(poco.DerivedValue); + } + + [TestMethod] + public void MapOpenGenericsUseInclude() + { + TypeAdapterConfig.GlobalSettings.Clear(); + + TypeAdapterConfig.GlobalSettings + .ForType(typeof(DerivedPoco<>), typeof(DerivedDto<>)) + .Map("derivedValue", "DerivedValue"); + + TypeAdapterConfig.GlobalSettings + .ForType(typeof(GenericPoco<>), typeof(GenericDto<>)) + .Map("value", "Value"); + + TypeAdapterConfig.GlobalSettings + .ForType(typeof(GenericPoco<>), typeof(GenericDto<>)) + .Include(typeof(DerivedPoco<>), typeof(DerivedDto<>)); + + var poco = new DerivedPoco { Value = 123, DerivedValue = 42 }; + var dto = poco.Adapt(typeof(GenericPoco<>), typeof(GenericDto<>)); + + dto.ShouldBeOfType>(); + + ((DerivedDto)dto).value.ShouldBe(poco.Value); + ((DerivedDto)dto).derivedValue.ShouldBe(poco.DerivedValue); + } + + public class DerivedPoco : GenericPoco + { + public T DerivedValue { get; set; } + } + + public class DerivedDto : GenericDto + { + public T derivedValue { get; set; } + } + public class GenericPoco { public T Value { get; set; } diff --git a/src/Mapster.Tests/WhenRequiresPropsValidation.cs b/src/Mapster.Tests/WhenRequiresPropsValidation.cs new file mode 100644 index 00000000..0652df6d --- /dev/null +++ b/src/Mapster.Tests/WhenRequiresPropsValidation.cs @@ -0,0 +1,48 @@ +using System; +using Mapster.Tests.Classes; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shouldly; + +namespace Mapster.Tests +{ + [TestClass] + public class WhenRequiresPropsValidation + { + [TestInitialize] + public void Setup() + { + TypeAdapterConfig.GlobalSettings.Clear(); + } + + [TestCleanup] + public void TestCleanup() + { + TypeAdapterConfig.GlobalSettings.Default.NameMatchingStrategy(NameMatchingStrategy.Exact); + } + + [TestMethod] + public void DestinationProps_Exist_In_Source() + { + var product = new Product {Id = Guid.NewGuid(), Title = "ProductA", CreatedUser = new User {Name = "UserA"}}; + + var dto = product.ValidateAndAdapt(); + + dto.ShouldNotBeNull(); + dto.Id.ShouldBe(product.Id); + } + + [TestMethod] + public void DestinationProps_Not_Exist_In_Source() + { + var product = new Product {Id = Guid.NewGuid(), Title = "ProductA", CreatedUser = new User {Name = "UserA"}}; + + ProductDTO productDtoRef; + var notExistingPropName = nameof(productDtoRef.CreatedUserName); + + var ex = Should.Throw(() => product.ValidateAndAdapt()); + + ex.Message.ShouldContain(notExistingPropName); + ex.Message.ShouldContain(nameof(Product)); + } + } +} diff --git a/src/Mapster.Tests/WhenRequiresPropsValidationWithAdapterConfig.cs b/src/Mapster.Tests/WhenRequiresPropsValidationWithAdapterConfig.cs new file mode 100644 index 00000000..eb845416 --- /dev/null +++ b/src/Mapster.Tests/WhenRequiresPropsValidationWithAdapterConfig.cs @@ -0,0 +1,53 @@ +using System; +using Mapster.Tests.Classes; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shouldly; + +namespace Mapster.Tests +{ + [TestClass] + public class WhenRequiresPropsValidationWithAdapterConfig + { + [TestInitialize] + public void Setup() + { + TypeAdapterConfig.GlobalSettings.Clear(); + } + + [TestCleanup] + public void TestCleanup() + { + TypeAdapterConfig.GlobalSettings.Default.NameMatchingStrategy(NameMatchingStrategy.Exact); + } + + [TestMethod] + public void DestinationProps_Not_Exist_In_Source_But_Configured() + { + var product = new Product {Id = Guid.NewGuid(), Title = "ProductA", CreatedUser = new User {Name = "UserA"}}; + + var adapterSettings = TypeAdapterConfig.NewConfig() + .Map(dest => dest.CreatedUserName, src => $"{src.CreatedUser.Name} {src.CreatedUser.Surname}"); + + var dto = product.ValidateAndAdapt(adapterSettings.Config); + + dto.ShouldNotBeNull(); + dto.CreatedUserName.ShouldBe($"{product.CreatedUser.Name} {product.CreatedUser.Surname}"); + } + + [TestMethod] + public void DestinationProps_Not_Exist_In_Source_And_MisConfigured() + { + var product = new Product {Id = Guid.NewGuid(), Title = "ProductA", CreatedUser = new User {Name = "UserA"}}; + + var adapterSettings = TypeAdapterConfig.NewConfig(); + + ProductDTO productDtoRef; + var notExistingPropName = nameof(productDtoRef.CreatedUserName); + + var ex = Should.Throw(() => product.ValidateAndAdapt(adapterSettings.Config)); + + ex.Message.ShouldContain(notExistingPropName); + ex.Message.ShouldContain(nameof(Product)); + } + } +} diff --git a/src/Mapster.Tests/WhenUseDestinatonValueMappingRegression.cs b/src/Mapster.Tests/WhenUseDestinatonValueMappingRegression.cs new file mode 100644 index 00000000..4200ada6 --- /dev/null +++ b/src/Mapster.Tests/WhenUseDestinatonValueMappingRegression.cs @@ -0,0 +1,106 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shouldly; +using System.Collections.Generic; +using System.Linq; + +namespace Mapster.Tests; + +[TestClass] +public class WhenUseDestinatonValueMappingRegression +{ + [TestClass] + public class WhenUseDestinatonMappingRegression + { + [TestMethod] + public void UseDestinatonValueUsingMapWithasParam() + { + TypeAdapterConfig> + .NewConfig() + .MapWith(src => MapThumbnailDetailsData(src).ToList()); + + var channelSrc = new ChannelSource + { + ChannelId = "123", + Thumbnails = new ThumbnailDetailsSource + { + Default = new ThumbnailSource + { + Url = "https://www.youtube.com/default.jpg" + }, + Medium = new ThumbnailSource + { + Url = "https://www.youtube.com/medium.jpg" + }, + High = new ThumbnailSource + { + Url = "https://www.youtube.com/high.jpg" + } + }, + + TempThumbnails = new List() { 1, 2, 3 } + }; + + var channelDest = channelSrc.Adapt(); + + channelDest.Thumbnails.Count.ShouldBe(3); + channelDest.TempThumbnails.Count.ShouldBe(3); + } + + + #region TestClasses + private static IEnumerable MapThumbnailDetailsData(ThumbnailDetailsSource thumbnailDetails) + { + yield return MapThumbnail(thumbnailDetails.Default, "Default"); + yield return MapThumbnail(thumbnailDetails.Medium, "Medium"); + yield return MapThumbnail(thumbnailDetails.High, "High"); + } + + private static ThumbnailDestination MapThumbnail( + ThumbnailSource thumbnail, + string thumbnailType) => + new() + { + Type = thumbnailType, + Url = thumbnail.Url.Trim(), + }; + + + public class ChannelDestination + { + public string ChannelId { get; set; } = default!; + + [UseDestinationValue] + public ICollection Thumbnails { get; } = new List(); + + [UseDestinationValue] + public ICollection TempThumbnails { get; } = new List(); + } + + public class ThumbnailDestination + { + public string Type { get; set; } = default!; + public string Url { get; set; } = default!; + } + + public class ChannelSource + { + public string ChannelId { get; set; } = default!; + public ThumbnailDetailsSource Thumbnails { get; set; } = default!; + public ICollection TempThumbnails { get; set; } = new List(); + } + + public class ThumbnailDetailsSource + { + public ThumbnailSource? Default { get; set; } + public ThumbnailSource? Medium { get; set; } + public ThumbnailSource? High { get; set; } + } + + public class ThumbnailSource + { + public string Url { get; set; } = default!; + } + + #endregion TestClasses + } +} diff --git a/src/Mapster.Tests/WhenUseTempAdapterConfig.cs b/src/Mapster.Tests/WhenUseTempAdapterConfig.cs new file mode 100644 index 00000000..1def0b88 --- /dev/null +++ b/src/Mapster.Tests/WhenUseTempAdapterConfig.cs @@ -0,0 +1,75 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Shouldly; + +namespace Mapster.Tests +{ + + public record SourceDto(string Name, int Age); + public record DestinationDto(long Id, string Name, int Age); + + [TestClass] + public class WhenUseTempAdapterConfig + { + [TestMethod] + public void Adapt_TemporaryConfig_ShouldMapInitOnlyProperties() + { + // Arrange + var source = new SourceDto("Alice", 30); + long id = 42; + + // Act + var result = source.Adapt(cfg => + { + cfg.NewConfig() + .Map(dest => dest.Id, src => id); + }); + + // Assert + result.Name.ShouldBe("Alice"); + result.Age.ShouldBe(30); + result.Id.ShouldBe(42); + } + + [TestMethod] + public void Adapt_WithSetter_ShouldMapInitOnlyProperties() + { + // Arrange + var source = new SourceDto("Bob", 25); + long id = 99; + + // Act + var result = source.Adapt(setter => + { + setter.Map(dest => dest.Id, src => id); + }); + + // Assert + result.Name.ShouldBe("Bob"); + result.Age.ShouldBe(25); + result.Id.ShouldBe(99); + } + + [TestMethod] + public void Adapt_TemporaryConfig_ShouldNotModifyGlobalSettings() + { + // Arrange + var source = new SourceDto("Charlie", 40); + long id = 123; + + var globalMap = TypeAdapterConfig.GlobalSettings.GetMapFunction(); + + // Act + var result = source.Adapt(setter => + { + setter.Map(dest => dest.Id, src => id); + }); + + // Assert + var original = globalMap(source); // mapping via GlobalSettings + original.Id.ShouldBe(default(long)); // GlobalSettings unaffected + original.Name.ShouldBe("Charlie"); + original.Age.ShouldBe(40); + } + + } +} diff --git a/src/Mapster.Tests/WhenUsingDestinationValue.cs b/src/Mapster.Tests/WhenUsingDestinationValue.cs index 086bf938..f3f7d3a8 100644 --- a/src/Mapster.Tests/WhenUsingDestinationValue.cs +++ b/src/Mapster.Tests/WhenUsingDestinationValue.cs @@ -35,6 +35,32 @@ public void MapUsingDestinationValue() poco.Strings.ShouldBe(strings); } + /// + /// https://github.com/MapsterMapper/Mapster/issues/410 + /// + [TestMethod] + public void MappingToReadonlyPropertyWhenPocoDetectRegression() + { + var studentDto = new StudentDtoOrigin { Name = "Marta" }; + var student = studentDto.Adapt(); // No exception. + + student.Name.ShouldBe("John"); + } + + + public class StudentOrigin + { + [UseDestinationValue] + public string Name { get; } = "John"; // only readonly + } + + public class StudentDtoOrigin + { + + public string Name { get; set; } + } + + public class ContractingParty { public string Name { get; set; } diff --git a/src/Mapster.Tool.Tests/.config/dotnet-tools.json b/src/Mapster.Tool.Tests/.config/dotnet-tools.json index 0c61f2d3..db4df7b6 100644 --- a/src/Mapster.Tool.Tests/.config/dotnet-tools.json +++ b/src/Mapster.Tool.Tests/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "mapster.tool": { - "version": "8.3.0", + "version": "8.4.0", "commands": [ "dotnet-mapster" ] diff --git a/src/Mapster.Tool.Tests/Mapster.Tool.Tests.csproj b/src/Mapster.Tool.Tests/Mapster.Tool.Tests.csproj index 9ddf4c8d..702c02e6 100644 --- a/src/Mapster.Tool.Tests/Mapster.Tool.Tests.csproj +++ b/src/Mapster.Tool.Tests/Mapster.Tool.Tests.csproj @@ -1,23 +1,23 @@ - + - net6.0 + net10.0;net9.0;net8.0 enable enable - + true false - - - - - + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/Mapster.Tool/DeferredDependencyAssemblyLoadContext.cs b/src/Mapster.Tool/DeferredDependencyAssemblyLoadContext.cs new file mode 100644 index 00000000..09132d22 --- /dev/null +++ b/src/Mapster.Tool/DeferredDependencyAssemblyLoadContext.cs @@ -0,0 +1,87 @@ +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.Loader; + +namespace Mapster.Tool +{ + // + // Summary: + // Used for loading an assembly and its dependencies in an isolated assembly load context but deferring the resolution of + // a subset of those assemblies to an already existing Assembly Load Context (likely the AssemblyLoadContext.Default + // context that is used by the runtime by default at startup) + public class DeferredDependencyAssemblyLoadContext : AssemblyLoadContext + { + private readonly AssemblyDependencyResolver resolver; + private readonly ImmutableHashSet deferredDependencyAssemblyNames; + private readonly AssemblyLoadContext deferToContext; + + public DeferredDependencyAssemblyLoadContext( + string assemblyPath, + AssemblyLoadContext deferToContext, + params AssemblyName[] deferredDependencyAssemblyNames + ) + { + // set up a resolver for the dependencies of this non-deferred assembly + resolver = new AssemblyDependencyResolver(assemblyPath); + + // store all of the assembly simple names that should be deferred w/ + // the sharing assembly context loader (and not resolved exclusively in this loader) + this.deferredDependencyAssemblyNames = deferredDependencyAssemblyNames + .Select(an => an.Name!) + .Where(n => n != null) + .ToImmutableHashSet(); + + // store a reference to the assembly load context that assembly resolution will be deferred + // to when on the deferredDependencyAssemblyNames list + this.deferToContext = deferToContext; + + // load the non-deferred assembly in this context to start + Load(GetAssemblyName(assemblyPath)); + } + + protected override Assembly? Load(AssemblyName assemblyName) + { + if (assemblyName.Name == null) + { + return null; + } + + // if the assembly to be loaded is also set to be deferrred (based on constructor) + // then first attempt to load it from the sharing assembly load context + if (deferredDependencyAssemblyNames.Contains(assemblyName.Name)) + { + return deferToContext.LoadFromAssemblyName(assemblyName); + } + + // all other loaded assemblies should be considered dependencies of the + // non-deferred dependency loaded in the constructor and should be loaded + // from its path (the AssemblyDepedencyResolver resolves dependency paths) + string? assemblyPath = resolver.ResolveAssemblyToPath(assemblyName); + if (assemblyPath == null) + { + return null; + } + + return LoadFromAssemblyPath(assemblyPath); + } + + public static Assembly LoadAssemblyFrom( + string assemblyPath, + AssemblyLoadContext deferToContext, + params AssemblyName[] deferredDependencyAssemblyNames + ) + { + DeferredDependencyAssemblyLoadContext loadContext = + new DeferredDependencyAssemblyLoadContext( + assemblyPath, + deferToContext, + deferredDependencyAssemblyNames + ); + return loadContext.LoadFromAssemblyName( + new AssemblyName(Path.GetFileNameWithoutExtension(assemblyPath)) + ); + } + } +} diff --git a/src/Mapster.Tool/Mapster.Tool.csproj b/src/Mapster.Tool/Mapster.Tool.csproj index b04a51c5..8abc6c96 100644 --- a/src/Mapster.Tool/Mapster.Tool.csproj +++ b/src/Mapster.Tool/Mapster.Tool.csproj @@ -2,7 +2,7 @@ Exe - net7.0;net6.0 + net10.0;net9.0;net8.0; true true dotnet-mapster @@ -10,7 +10,7 @@ Mapster;Tool true Mapster.Tool.snk - 8.4.0-pre06 + 10.0.0-pre01 enable @@ -19,8 +19,8 @@ - - + + diff --git a/src/Mapster.Tool/Program.cs b/src/Mapster.Tool/Program.cs index 4066ef52..1347cb13 100644 --- a/src/Mapster.Tool/Program.cs +++ b/src/Mapster.Tool/Program.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Linq.Expressions; using System.Reflection; +using System.Runtime.Loader; using System.Text; using CommandLine; using ExpressionDebugger; @@ -16,7 +17,8 @@ class Program { static void Main(string[] args) { - Parser.Default.ParseArguments(args) + Parser.Default + .ParseArguments(args) .WithParsed(GenerateMappers) .WithParsed(GenerateModels) .WithParsed(GenerateExtensions); @@ -39,9 +41,13 @@ static void Main(string[] args) private static string GetOutput(string baseOutput, string? segment, string typeName) { var fullBasePath = Path.GetFullPath(baseOutput); - return segment == null - ? Path.Combine(fullBasePath, typeName + ".g.cs") - : Path.Combine(fullBasePath, segment.Replace('.', Path.DirectorySeparatorChar), typeName + ".g.cs"); + return segment == null + ? Path.Combine(fullBasePath, typeName + ".g.cs") + : Path.Combine( + fullBasePath, + segment.Replace('.', Path.DirectorySeparatorChar), + typeName + ".g.cs" + ); } private static void WriteFile(string code, string path) @@ -60,7 +66,27 @@ private static void WriteFile(string code, string path) private static void GenerateMappers(MapperOptions opt) { - var assembly = Assembly.LoadFrom(Path.GetFullPath(opt.Assembly)); + // We want loaded assemblies that we're scanning to be isolated from our currently + // running assembly load context in order to avoid type/framework collisions between Mapster assemblies + // and their dependencies and the scanned assemblies and their dependencies + + // However, we also need *some* of those scanned assemblies and thus their types to resolve from our + // currently running AssemblyLoadContext.Default: The Mapster assembly basically. + + // This way when we compare attribute types (such as MapperAttribute) between our running assembly + // and the scanned assembly the two types with the same FullName can be considered equal because + // they both were resolved from AssemblyLoadContext.Default. + + // This isolated Assembly Load Context will be able to resolve the Mapster assembly, but + // the resolved Assembly will be the same one that is in AssemblyLoadContext.Default + // (the runtime assembly load context that our code refers to by default when referencing + // types) + var assembly = DeferredDependencyAssemblyLoadContext.LoadAssemblyFrom( + assemblyPath: Path.GetFullPath(opt.Assembly), + deferToContext: AssemblyLoadContext.Default, + typeof(MapperAttribute).Assembly.GetName(), + typeof(IRegister).Assembly.GetName() + ); var config = TypeAdapterConfig.GlobalSettings; config.SelfContainedCodeGeneration = true; config.Scan(assembly); @@ -78,7 +104,7 @@ private static void GenerateMappers(MapperOptions opt) var segments = GetSegments(type.Namespace, opt.BaseNamespace); var definitions = new TypeDefinitions { - Implements = new[] {type}, + Implements = new[] { type }, Namespace = CreateNamespace(opt.Namespace, segments, type.Namespace), TypeName = attr.Name ?? GetImplName(GetCodeFriendlyTypeName(type)), IsInternal = attr.IsInternal, @@ -88,7 +114,9 @@ private static void GenerateMappers(MapperOptions opt) var path = GetOutput(opt.Output, segments, definitions.TypeName); if (opt.SkipExistingFiles && File.Exists(path)) { - Console.WriteLine($"Skipped: {type.FullName}. Mapper {definitions.TypeName} already exists."); + Console.WriteLine( + $"Skipped: {type.FullName}. Mapper {definitions.TypeName} already exists." + ); continue; } @@ -110,8 +138,11 @@ private static void GenerateMappers(MapperOptions opt) var funcArgs = propArgs.GetGenericArguments(); var tuple = new TypeTuple(funcArgs[0], funcArgs[1]); var expr = config.CreateMapExpression(tuple, MapType.Projection); - translator.VisitLambda(expr, ExpressionTranslator.LambdaType.PublicLambda, - prop.Name); + translator.VisitLambda( + expr, + ExpressionTranslator.LambdaType.PublicLambda, + prop.Name + ); } } @@ -127,16 +158,21 @@ private static void GenerateMappers(MapperOptions opt) if (methodArgs.Length < 1 || methodArgs.Length > 2) continue; var tuple = new TypeTuple(methodArgs[0].ParameterType, method.ReturnType); - var expr = config.CreateMapExpression(tuple, - methodArgs.Length == 1 ? MapType.Map : MapType.MapToTarget); - translator.VisitLambda(expr, ExpressionTranslator.LambdaType.PublicMethod, - method.Name); + var expr = config.CreateMapExpression( + tuple, + methodArgs.Length == 1 ? MapType.Map : MapType.MapToTarget + ); + translator.VisitLambda( + expr, + ExpressionTranslator.LambdaType.PublicMethod, + method.Name + ); } } - var code = opt.GenerateNullableDirective ? - $"#nullable enable{Environment.NewLine}{translator}" : - translator.ToString(); + var code = opt.GenerateNullableDirective + ? $"#nullable enable{Environment.NewLine}{translator}" + : translator.ToString(); WriteFile(code, path); } } @@ -150,7 +186,12 @@ private static string GetImplName(string name) private static void GenerateModels(ModelOptions opt) { - var assembly = Assembly.LoadFrom(Path.GetFullPath(opt.Assembly)); + var assembly = DeferredDependencyAssemblyLoadContext.LoadAssemblyFrom( + assemblyPath: Path.GetFullPath(opt.Assembly), + deferToContext: AssemblyLoadContext.Default, + typeof(MapperAttribute).Assembly.GetName(), + typeof(IRegister).Assembly.GetName() + ); var codeGenConfig = new CodeGenerationConfig(); codeGenConfig.Scan(assembly); @@ -165,7 +206,11 @@ private static void GenerateModels(ModelOptions opt) foreach (var type in types) { var builders = type.GetAdaptAttributeBuilders(codeGenConfig) - .Where(it => !string.IsNullOrEmpty(it.Attribute.Name) && it.Attribute.Name != "[name]") + .Where( + it => + !string.IsNullOrEmpty(it.Attribute.Name) + && it.Attribute.Name != "[name]" + ) .ToList(); if (builders.Count == 0) continue; @@ -182,10 +227,13 @@ private static void GenerateModels(ModelOptions opt) { var nilCtxAttr = type.GetCustomAttributesData() .FirstOrDefault(it => it.AttributeType.Name == "NullableContextAttribute"); - return nilCtxAttr?.ConstructorArguments.Count == 1 && nilCtxAttr.ConstructorArguments[0].Value is byte b - ? (byte?) b + return + nilCtxAttr?.ConstructorArguments.Count == 1 + && nilCtxAttr.ConstructorArguments[0].Value is byte b + ? (byte?)b : null; } + private static void CreateModel(ModelOptions opt, Type type, AdaptAttributeBuilder builder) { var segments = GetSegments(type.Namespace, opt.BaseNamespace); @@ -202,7 +250,9 @@ private static void CreateModel(ModelOptions opt, Type type, AdaptAttributeBuild var path = GetOutput(opt.Output, segments, definitions.TypeName); if (opt.SkipExistingFiles && File.Exists(path)) { - Console.WriteLine($"Skipped: {type.FullName}. Model {definitions.TypeName} already exists."); + Console.WriteLine( + $"Skipped: {type.FullName}. Model {definitions.TypeName} already exists." + ); return; } @@ -210,33 +260,43 @@ private static void CreateModel(ModelOptions opt, Type type, AdaptAttributeBuild var isAdaptTo = attr is AdaptToAttribute; var isTwoWays = attr is AdaptTwoWaysAttribute; var side = isAdaptTo ? MemberSide.Source : MemberSide.Destination; - var properties = type.GetFieldsAndProperties().Where(it => - !it.SafeGetCustomAttributes().OfType() - .Any(it2 => isTwoWays || it2.Side == null || it2.Side == side)); + var properties = type.GetFieldsAndProperties() + .Where( + it => + !it.SafeGetCustomAttributes() + .OfType() + .Any(it2 => isTwoWays || it2.Side == null || it2.Side == side) + ); if (attr.IgnoreAttributes != null) { - properties = properties.Where(it => - !it.SafeGetCustomAttributes() - .Select(it2 => it2.GetType()) - .Intersect(attr.IgnoreAttributes) - .Any()); + properties = properties.Where( + it => + !it.SafeGetCustomAttributes() + .Select(it2 => it2.GetType()) + .Intersect(attr.IgnoreAttributes) + .Any() + ); } if (attr.IgnoreNoAttributes != null) { - properties = properties.Where(it => - it.SafeGetCustomAttributes() - .Select(it2 => it2.GetType()) - .Intersect(attr.IgnoreNoAttributes) - .Any()); + properties = properties.Where( + it => + it.SafeGetCustomAttributes() + .Select(it2 => it2.GetType()) + .Intersect(attr.IgnoreNoAttributes) + .Any() + ); } if (attr.IgnoreNamespaces != null) { foreach (var ns in attr.IgnoreNamespaces) { - properties = properties.Where(it => getPropType(it).Namespace?.StartsWith(ns) != true); + properties = properties.Where( + it => getPropType(it).Namespace?.StartsWith(ns) != true + ); } } @@ -248,47 +308,74 @@ private static void CreateModel(ModelOptions opt, Type type, AdaptAttributeBuild var setting = propSettings?.GetValueOrDefault(member.Name); if (setting?.Ignore == true) continue; - + var adaptMember = member.GetCustomAttribute(); if (!isTwoWays && adaptMember?.Side != null && adaptMember.Side != side) adaptMember = null; - var propType = setting?.MapFunc?.ReturnType ?? - setting?.TargetPropertyType ?? - GetPropertyType(member, getPropType(member), attr.GetType(), opt.Namespace, builder); - var nilAttr = member.GetCustomAttributesData() + var propType = + setting?.MapFunc?.ReturnType + ?? setting?.TargetPropertyType + ?? GetPropertyType( + member, + getPropType(member), + attr.GetType(), + opt.Namespace, + builder + ); + var nilAttr = member + .GetCustomAttributesData() .FirstOrDefault(it => it.AttributeType.Name == "NullableAttribute"); - var nilAttrArg = nilAttr?.ConstructorArguments.Count == 1 ? nilAttr.ConstructorArguments[0].Value : null; - translator.Properties.Add(new PropertyDefinitions - { - Name = setting?.TargetPropertyName ?? adaptMember?.Name ?? member.Name, - Type = isNullable ? propType.MakeNullable() : propType, - IsReadOnly = isReadOnly, - NullableContext = nilAttrArg is byte b ? (byte?)b : null, - Nullable = nilAttrArg is byte[] bytes ? bytes : null, - }); + var nilAttrArg = + nilAttr?.ConstructorArguments.Count == 1 + ? nilAttr.ConstructorArguments[0].Value + : null; + translator.Properties.Add( + new PropertyDefinitions + { + Name = setting?.TargetPropertyName ?? adaptMember?.Name ?? member.Name, + Type = isNullable ? propType.MakeNullable() : propType, + IsReadOnly = isReadOnly, + NullableContext = nilAttrArg is byte b ? (byte?)b : null, + Nullable = nilAttrArg is byte[] bytes ? bytes : null, + } + ); } - var code = opt.GenerateNullableDirective ? - $"#nullable enable{Environment.NewLine}{translator}" : - translator.ToString(); + var code = opt.GenerateNullableDirective + ? $"#nullable enable{Environment.NewLine}{translator}" + : translator.ToString(); WriteFile(code, path); static Type getPropType(MemberInfo mem) { - return mem is PropertyInfo p ? p.PropertyType : ((FieldInfo) mem).FieldType; + return mem is PropertyInfo p ? p.PropertyType : ((FieldInfo)mem).FieldType; } } - private static readonly Dictionary _mockTypes = new Dictionary(); - private static Type GetPropertyType(MemberInfo member, Type propType, Type attrType, string? ns, AdaptAttributeBuilder builder) + private static readonly Dictionary _mockTypes = + new Dictionary(); + + private static Type GetPropertyType( + MemberInfo member, + Type propType, + Type attrType, + string? ns, + AdaptAttributeBuilder builder + ) { - var navAttr = member.SafeGetCustomAttributes() + var navAttr = member + .SafeGetCustomAttributes() .OfType() .FirstOrDefault(it => it.ForAttributes?.Contains(attrType) != false); if (navAttr != null) return navAttr.Type; - if (propType.IsCollection() && propType.IsCollectionCompatible() && propType.IsGenericType && propType.GetGenericArguments().Length == 1) + if ( + propType.IsCollection() + && propType.IsCollectionCompatible() + && propType.IsGenericType + && propType.GetGenericArguments().Length == 1 + ) { var elementType = propType.GetGenericArguments()[0]; var newType = GetPropertyType(member, elementType, attrType, ns, builder); @@ -305,14 +392,16 @@ private static Type GetPropertyType(MemberInfo member, Type propType, Type attrT return alterType; var propTypeAttrs = propType.SafeGetCustomAttributes(); - navAttr = propTypeAttrs.OfType() + navAttr = propTypeAttrs + .OfType() .FirstOrDefault(it => it.ForAttributes?.Contains(attrType) != false); if (navAttr != null) return navAttr.Type; var adaptAttr = builder.TypeSettings.ContainsKey(propType) - ? (BaseAdaptAttribute?) builder.Attribute - : propTypeAttrs.OfType() + ? (BaseAdaptAttribute?)builder.Attribute + : propTypeAttrs + .OfType() .FirstOrDefault(it => it.GetType() == attrType); if (adaptAttr == null) return propType; @@ -330,7 +419,7 @@ private static Type GetPropertyType(MemberInfo member, Type propType, Type attrT private static Type? GetFromType(Type type, BaseAdaptAttribute attr, HashSet types) { - if (!(attr is AdaptFromAttribute) && !(attr is AdaptTwoWaysAttribute)) + if (!(attr is AdaptFromAttribute) && !(attr is AdaptTwoWaysAttribute)) return null; var fromType = attr.Type; @@ -345,7 +434,7 @@ private static Type GetPropertyType(MemberInfo member, Type propType, Type attrT private static Type? GetToType(Type type, BaseAdaptAttribute attr, HashSet types) { - if (!(attr is AdaptToAttribute)) + if (!(attr is AdaptToAttribute)) return null; var toType = attr.Type; @@ -358,19 +447,25 @@ private static Type GetPropertyType(MemberInfo member, Type propType, Type attrT return toType; } - private static void ApplySettings(TypeAdapterSetter setter, BaseAdaptAttribute attr, Dictionary settings) + private static void ApplySettings( + TypeAdapterSetter setter, + BaseAdaptAttribute attr, + Dictionary settings + ) { setter.ApplyAdaptAttribute(attr); foreach (var (name, setting) in settings) { if (setting.MapFunc != null) { - setter.Settings.Resolvers.Add(new InvokerModel - { - DestinationMemberName = setting.TargetPropertyName ?? name, - SourceMemberName = name, - Invoker = setting.MapFunc, - }); + setter.Settings.Resolvers.Add( + new InvokerModel + { + DestinationMemberName = setting.TargetPropertyName ?? name, + SourceMemberName = name, + Invoker = setting.MapFunc, + } + ); } else if (setting.TargetPropertyName != null) { @@ -381,14 +476,19 @@ private static void ApplySettings(TypeAdapterSetter setter, BaseAdaptAttribute a private static void GenerateExtensions(ExtensionOptions opt) { - var assembly = Assembly.LoadFrom(Path.GetFullPath(opt.Assembly)); + var assembly = DeferredDependencyAssemblyLoadContext.LoadAssemblyFrom( + assemblyPath: Path.GetFullPath(opt.Assembly), + deferToContext: AssemblyLoadContext.Default, + typeof(MapperAttribute).Assembly.GetName(), + typeof(IRegister).Assembly.GetName() + ); var config = TypeAdapterConfig.GlobalSettings; config.SelfContainedCodeGeneration = true; config.Scan(assembly); var codeGenConfig = new CodeGenerationConfig(); codeGenConfig.Scan(assembly); - var assemblies = new HashSet {assembly}; + var assemblies = new HashSet { assembly }; foreach (var builder in codeGenConfig.AdaptAttributeBuilders) { foreach (var setting in builder.TypeSettings) @@ -399,7 +499,8 @@ private static void GenerateExtensions(ExtensionOptions opt) var types = assemblies.SelectMany(it => it.GetLoadableTypes()).ToHashSet(); // assemblies defines open generic only, so we have to add specialised types used in mappings - foreach (var (key, _) in config.RuleMap) types.Add(key.Source); + foreach (var (key, _) in config.RuleMap) + types.Add(key.Source); var configDict = new Dictionary(); foreach (var builder in codeGenConfig.AdaptAttributeBuilders) { @@ -423,8 +524,9 @@ private static void GenerateExtensions(ExtensionOptions opt) { var mapperAttr = type.GetGenerateMapperAttributes(codeGenConfig).FirstOrDefault(); var ruleMaps = config.RuleMap - .Where(it => it.Key.Source == type && - it.Value.Settings.GenerateMapper is MapType) + .Where( + it => it.Key.Source == type && it.Value.Settings.GenerateMapper is MapType + ) .ToList(); if (mapperAttr == null && ruleMaps.Count == 0) continue; @@ -452,7 +554,9 @@ private static void GenerateExtensions(ExtensionOptions opt) var path = GetOutput(opt.Output, segments, definitions.TypeName); if (opt.SkipExistingFiles && File.Exists(path)) { - Console.WriteLine($"Skipped: {type.FullName}. Extension class {definitions.TypeName} already exists."); + Console.WriteLine( + $"Skipped: {type.FullName}. Extension class {definitions.TypeName} already exists." + ); continue; } @@ -466,67 +570,109 @@ private static void GenerateExtensions(ExtensionOptions opt) if (fromType != null) { var tuple = new TypeTuple(fromType, type); - var mapType = attr.MapType == 0 ? MapType.Map | MapType.MapToTarget : attr.MapType; - GenerateExtensionMethods(mapType, cloned, tuple, translator, type, mapperAttr.IsHelperClass); + var mapType = + attr.MapType == 0 ? MapType.Map | MapType.MapToTarget : attr.MapType; + GenerateExtensionMethods( + mapType, + cloned, + tuple, + translator, + type, + mapperAttr.IsHelperClass + ); } var toType = GetToType(type, attr, types); if (toType != null && (!(attr is AdaptTwoWaysAttribute) || type != toType)) { var tuple = new TypeTuple(type, toType); - var mapType = attr.MapType == 0 - ? MapType.Map | MapType.MapToTarget - : attr.MapType; - GenerateExtensionMethods(mapType, cloned, tuple, translator, type, mapperAttr.IsHelperClass); + var mapType = + attr.MapType == 0 ? MapType.Map | MapType.MapToTarget : attr.MapType; + GenerateExtensionMethods( + mapType, + cloned, + tuple, + translator, + type, + mapperAttr.IsHelperClass + ); } } foreach (var (tuple, rule) in ruleMaps) { - var mapType = (MapType) rule.Settings.GenerateMapper!; - GenerateExtensionMethods(mapType, config, tuple, translator, type, mapperAttr.IsHelperClass); + var mapType = (MapType)rule.Settings.GenerateMapper!; + GenerateExtensionMethods( + mapType, + config, + tuple, + translator, + type, + mapperAttr.IsHelperClass + ); } - var code = opt.GenerateNullableDirective ? - $"#nullable enable{Environment.NewLine}{translator}" : - translator.ToString(); + var code = opt.GenerateNullableDirective + ? $"#nullable enable{Environment.NewLine}{translator}" + : translator.ToString(); WriteFile(code, path); } } - private static void GenerateExtensionMethods(MapType mapType, TypeAdapterConfig config, TypeTuple tuple, - ExpressionTranslator translator, Type entityType, bool isHelperClass) + private static void GenerateExtensionMethods( + MapType mapType, + TypeAdapterConfig config, + TypeTuple tuple, + ExpressionTranslator translator, + Type entityType, + bool isHelperClass + ) { //add type name to prevent duplication translator.Translate(entityType); var destName = GetCodeFriendlyTypeName(tuple.Destination); - var name = tuple.Destination.Name == entityType.Name - ? destName - : destName.Replace(entityType.Name, ""); + var name = + tuple.Destination.Name == entityType.Name + ? destName + : destName.Replace(entityType.Name, ""); if ((mapType & MapType.Map) > 0) { var expr = config.CreateMapExpression(tuple, MapType.Map); - translator.VisitLambda(expr, isHelperClass ? ExpressionTranslator.LambdaType.PublicMethod : ExpressionTranslator.LambdaType.ExtensionMethod, - "AdaptTo" + name); + translator.VisitLambda( + expr, + isHelperClass + ? ExpressionTranslator.LambdaType.PublicMethod + : ExpressionTranslator.LambdaType.ExtensionMethod, + "AdaptTo" + name + ); } if ((mapType & MapType.MapToTarget) > 0) { var expr2 = config.CreateMapExpression(tuple, MapType.MapToTarget); - translator.VisitLambda(expr2, isHelperClass ? ExpressionTranslator.LambdaType.PublicMethod : ExpressionTranslator.LambdaType.ExtensionMethod, - "AdaptTo"); + translator.VisitLambda( + expr2, + isHelperClass + ? ExpressionTranslator.LambdaType.PublicMethod + : ExpressionTranslator.LambdaType.ExtensionMethod, + "AdaptTo" + ); } if ((mapType & MapType.Projection) > 0) { var proj = config.CreateMapExpression(tuple, MapType.Projection); - translator.VisitLambda(proj, ExpressionTranslator.LambdaType.PublicLambda, - "ProjectTo" + name); + translator.VisitLambda( + proj, + ExpressionTranslator.LambdaType.PublicLambda, + "ProjectTo" + name + ); } } - private static string GetCodeFriendlyTypeName(Type type) => GetCodeFriendlyTypeName(new StringBuilder(), type).ToString(); + private static string GetCodeFriendlyTypeName(Type type) => + GetCodeFriendlyTypeName(new StringBuilder(), type).ToString(); private static StringBuilder GetCodeFriendlyTypeName(StringBuilder sb, Type type) { @@ -544,7 +690,8 @@ private static StringBuilder GetCodeFriendlyTypeName(StringBuilder sb, Type type var name = type.Name; var i = name.IndexOf('`'); - if (i>0) name = name.Remove(i); + if (i > 0) + name = name.Remove(i); name = name switch { "SByte" => "Sbyte", @@ -559,8 +706,9 @@ private static StringBuilder GetCodeFriendlyTypeName(StringBuilder sb, Type type _ => name, }; - if (!string.IsNullOrEmpty(name)) sb.Append(name); + if (!string.IsNullOrEmpty(name)) + sb.Append(name); return sb; } } -} \ No newline at end of file +} diff --git a/src/Mapster.sln b/src/Mapster.sln index befd616f..57696e45 100644 --- a/src/Mapster.sln +++ b/src/Mapster.sln @@ -63,6 +63,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TemplateTest", "TemplateTes EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mapster.Tool.Tests", "Mapster.Tool.Tests\Mapster.Tool.Tests.csproj", "{E64E9CEB-8FB2-4012-BBA8-4C2B99FD54C1}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Benchmark.Development", "Benchmark.Development\Benchmark.Development.csproj", "{5F29425E-DFC6-48C2-945A-FC5D91260C07}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -161,6 +163,10 @@ Global {E64E9CEB-8FB2-4012-BBA8-4C2B99FD54C1}.Debug|Any CPU.Build.0 = Debug|Any CPU {E64E9CEB-8FB2-4012-BBA8-4C2B99FD54C1}.Release|Any CPU.ActiveCfg = Release|Any CPU {E64E9CEB-8FB2-4012-BBA8-4C2B99FD54C1}.Release|Any CPU.Build.0 = Release|Any CPU + {5F29425E-DFC6-48C2-945A-FC5D91260C07}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5F29425E-DFC6-48C2-945A-FC5D91260C07}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5F29425E-DFC6-48C2-945A-FC5D91260C07}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5F29425E-DFC6-48C2-945A-FC5D91260C07}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Mapster/Adapters/ArrayAdapter.cs b/src/Mapster/Adapters/ArrayAdapter.cs index b2f4eaed..37f4c5e6 100644 --- a/src/Mapster/Adapters/ArrayAdapter.cs +++ b/src/Mapster/Adapters/ArrayAdapter.cs @@ -70,7 +70,7 @@ protected override Expression CreateBlockExpression(Expression source, Expressio return CreateArraySet(source, destination, arg); } - protected override Expression CreateInlineExpression(Expression source, CompileArgument arg) + protected override Expression CreateInlineExpression(Expression source, CompileArgument arg, bool IsRequiredOnly = false) { if (arg.DestinationType.GetTypeInfo().IsAssignableFrom(source.Type.GetTypeInfo())) return source; diff --git a/src/Mapster/Adapters/BaseAdapter.cs b/src/Mapster/Adapters/BaseAdapter.cs index b7b6fc67..a3ffb648 100644 --- a/src/Mapster/Adapters/BaseAdapter.cs +++ b/src/Mapster/Adapters/BaseAdapter.cs @@ -102,11 +102,18 @@ protected virtual Expression CreateExpressionBody(Expression source, Expression? arg.Context.Depth++; } - if (CanInline(source, destination, arg)) + if(arg.Settings.MapToTargetPrimitive == true) { - var exp = CreateInlineExpressionBody(source, arg); - if (exp != null) - return exp.To(arg.DestinationType, true); + // skip inline mapping + } + else + { + if (CanInline(source, destination, arg)) + { + var exp = CreateInlineExpressionBody(source, arg); + if (exp != null) + return exp.To(arg.DestinationType, true); + } } if (arg.Context.Running.Count > 1 && @@ -143,44 +150,55 @@ protected Expression CreateBlockExpressionBody(Expression source, Expression? de var blocks = new List(); var label = Expression.Label(arg.DestinationType); - //var drvdSource = source as TDerivedSource + //var drvdSource = _source as TDerivedSource //if (drvdSource != null) // return adapt(drvdSource); foreach (var tuple in arg.Settings.Includes) { + TypeTuple itemTuple = tuple; + + if (tuple.Source.IsOpenGenericType() && tuple.Destination.IsOpenGenericType()) + { + var genericArg = source.Type.GetGenericArguments(); + itemTuple = new TypeTuple(tuple.Source.MakeGenericType(genericArg), tuple.Destination.MakeGenericType(genericArg)); + } + //same type, no redirect to prevent endless loop - if (tuple.Source == arg.SourceType) + if (itemTuple.Source == arg.SourceType) continue; //type is not compatible, no redirect - if (!arg.SourceType.GetTypeInfo().IsAssignableFrom(tuple.Source.GetTypeInfo())) + if (!arg.SourceType.GetTypeInfo().IsAssignableFrom(itemTuple.Source.GetTypeInfo())) continue; - var drvdSource = Expression.Variable(tuple.Source); + var drvdSource = Expression.Variable(itemTuple.Source); vars.Add(drvdSource); var drvdSourceAssign = Expression.Assign( drvdSource, - Expression.TypeAs(source, tuple.Source)); + Expression.TypeAs(source, itemTuple.Source)); blocks.Add(drvdSourceAssign); - var cond = Expression.NotEqual(drvdSource, Expression.Constant(null, tuple.Source)); + var cond = Expression.NotEqual(drvdSource, Expression.Constant(null, itemTuple.Source)); ParameterExpression? drvdDest = null; if (destination != null) { - drvdDest = Expression.Variable(tuple.Destination); + drvdDest = Expression.Variable(itemTuple.Destination); vars.Add(drvdDest); var drvdDestAssign = Expression.Assign( drvdDest, - Expression.TypeAs(destination, tuple.Destination)); + Expression.TypeAs(destination, itemTuple.Destination)); blocks.Add(drvdDestAssign); - cond = Expression.AndAlso( - cond, - Expression.NotEqual(drvdDest, Expression.Constant(null, tuple.Destination))); + + // fix by https://github.com/MapsterMapper/Mapster/issues/794 + // This can be removed if it does not cause any other bugs. + // cond = Expression.AndAlso( + // cond, + // Expression.NotEqual(drvdDest, Expression.Constant(null, tuple.Destination))); } - var adaptExpr = CreateAdaptExpressionCore(drvdSource, tuple.Destination, arg, destination: drvdDest); + var adaptExpr = CreateAdaptExpressionCore(drvdSource, itemTuple.Destination, arg, destination: drvdDest); var adapt = Expression.Return(label, adaptExpr); var ifExpr = Expression.IfThen(cond, adapt); blocks.Add(ifExpr); @@ -195,7 +213,17 @@ protected Expression CreateBlockExpressionBody(Expression source, Expression? de vars.Add(src); transformedSource = src; } - var set = CreateInstantiationExpression(transformedSource, destination, arg); + + Expression? set; + var requiremembers = arg.DestinationType.GetProperties() + .Where(x => x.GetCustomAttributes() + .Any(y => y.GetType() == typeof(System.Runtime.CompilerServices.RequiredMemberAttribute))); + + if (requiremembers.Count() != 0) + set = CreateInlineExpression(source, arg, true); + else + set = CreateInstantiationExpression(transformedSource, destination, arg); + if (destination != null && (UseTargetValue || arg.UseDestinationValue) && arg.GetConstructUsing()?.Parameters.Count != 2) { if (destination.CanBeNull()) @@ -218,7 +246,7 @@ protected Expression CreateBlockExpressionBody(Expression source, Expression? de else { //TDestination result; - //if (source == null) + //if (_source == null) // return default(TDestination); if (source.CanBeNull()) { @@ -237,15 +265,15 @@ protected Expression CreateBlockExpressionBody(Expression source, Expression? de assignActions.Add(Expression.Assign(transformedSource, transform)); assignActions.Add(assign); - //before(source, result, destination); + //before(_source, result, destination); var beforeMappings = arg.Settings.BeforeMappingFactories.Select(it => InvokeMapping(it, source, result, destination, arg, true)).Reverse(); assignActions.AddRange(beforeMappings); - //result.prop = adapt(source.prop); + //result.prop = adapt(_source.prop); var mapping = CreateBlockExpression(transformedSource, result, arg); var settingActions = new List {mapping}; - //after(source, result, destination); + //after(_source, result, destination); var afterMappings = arg.Settings.AfterMappingFactories.Select(it => InvokeMapping(it, source, result, destination, arg, false)).Reverse(); settingActions.AddRange(afterMappings); @@ -254,13 +282,13 @@ protected Expression CreateBlockExpressionBody(Expression source, Expression? de //using (var scope = new MapContextScope()) { // var references = scope.Context.Reference; - // var key = new ReferenceTuple(source, typeof(TDestination)); + // var key = new ReferenceTuple(_source, typeof(TDestination)); // if (references.TryGetValue(key, out var cache)) // return (TDestination)cache; // // var result = new TDestination(); - // references[source] = (object)result; - // result.prop = adapt(source.prop); + // references[_source] = (object)result; + // result.prop = adapt(_source.prop); // return result; //} @@ -348,7 +376,7 @@ private static Expression InvokeMapping( protected Expression? CreateInlineExpressionBody(Expression source, CompileArgument arg) { - //source == null ? default(TDestination) : adapt(source) + //_source == null ? default(TDestination) : adapt(_source) var exp = CreateInlineExpression(source, arg); if (exp == null) @@ -362,7 +390,8 @@ private static Expression InvokeMapping( } protected abstract Expression CreateBlockExpression(Expression source, Expression destination, CompileArgument arg); - protected abstract Expression? CreateInlineExpression(Expression source, CompileArgument arg); + protected abstract Expression? CreateInlineExpression(Expression source, CompileArgument arg, bool IsRequiredOnly = false); + protected Expression CreateInstantiationExpression(Expression source, CompileArgument arg) { @@ -450,23 +479,45 @@ protected Expression CreateAdaptExpression(Expression source, Type destinationTy } internal Expression CreateAdaptExpression(Expression source, Type destinationType, CompileArgument arg, MemberMapping? mapping, Expression? destination = null) { - if (source.Type == destinationType && arg.MapType == MapType.Projection) - return source; + Expression _source; + + if (arg.MapType != MapType.Projection) + _source = source.NullableEnumExtractor(); // Extraction Nullable Enum + else + _source = source; + + if (_source.Type == destinationType && arg.MapType == MapType.Projection) + return _source; - //adapt(source); + //adapt(_source); var notUsingDestinationValue = mapping is not { UseDestinationValue: true }; - var exp = source.Type == destinationType && arg.Settings.ShallowCopyForSameType == true && notUsingDestinationValue && - !arg.Context.Config.HasRuleFor(source.Type, destinationType) - ? source - : CreateAdaptExpressionCore(source, destinationType, arg, mapping, destination); + var exp = _source.Type == destinationType && arg.Settings.ShallowCopyForSameType == true && notUsingDestinationValue && + !arg.Context.Config.HasRuleFor(_source.Type, destinationType) + ? _source + : CreateAdaptExpressionCore(_source, destinationType, arg, mapping, destination); - //transform(adapt(source)); + //transform(adapt(_source)); if (notUsingDestinationValue) { var transform = arg.Settings.DestinationTransforms.Find(it => it.Condition(exp.Type)); if (transform != null) exp = transform.TransformFunc(exp.Type).Apply(arg.MapType, exp); } + else + { + if (exp.NodeType != ExpressionType.Invoke) + { + var argExt = new CompileArgument + { + DestinationType = arg.DestinationType, + SourceType = arg.DestinationType, + MapType = MapType.MapToTarget, + Context = arg.Context, + }; + + return CreateAdaptExpressionCore(exp, destinationType, argExt, mapping, destination).To(destinationType); + } + } return exp.To(destinationType); } diff --git a/src/Mapster/Adapters/BaseClassAdapter.cs b/src/Mapster/Adapters/BaseClassAdapter.cs index faa490ec..a50c4b73 100644 --- a/src/Mapster/Adapters/BaseClassAdapter.cs +++ b/src/Mapster/Adapters/BaseClassAdapter.cs @@ -1,10 +1,10 @@ -using System; +using Mapster.Models; +using Mapster.Utils; +using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Reflection; -using Mapster.Models; -using Mapster.Utils; namespace Mapster.Adapters { @@ -15,12 +15,16 @@ internal abstract class BaseClassAdapter : BaseAdapter #region Build the Adapter Model - protected ClassMapping CreateClassConverter(Expression source, ClassModel classModel, CompileArgument arg, Expression? destination = null) + protected ClassMapping CreateClassConverter(Expression source, ClassModel classModel, CompileArgument arg, Expression? destination = null, bool ctorMapping = false, ClassModel recordRestorMemberModel = null) { var destinationMembers = classModel.Members; var unmappedDestinationMembers = new List(); var properties = new List(); + arg.ConstructorMapping = ctorMapping; + if (arg.Settings.IgnoreNonMapped == true) + IgnoreNonMapped(classModel,arg); + var sources = new List {source}; sources.AddRange( arg.Settings.ExtraSources.Select(src => @@ -29,7 +33,7 @@ src is LambdaExpression lambda : ExpressionEx.PropertyOrFieldPath(source, (string)src))); foreach (var destinationMember in destinationMembers) { - if (ProcessIgnores(arg, destinationMember, out var ignore)) + if (ProcessIgnores(arg, destinationMember, out var ignore) && !ctorMapping) continue; var resolvers = arg.Settings.ValueAccessingStrategies.AsEnumerable(); @@ -40,6 +44,63 @@ from src in sources select fn(src, destinationMember, arg)) .FirstOrDefault(result => result != null); + if (arg.MapType == MapType.Projection && getter != null) + { + var s = new TopLevelMemberNameVisitor(); + + s.Visit(getter); + + var match = arg.Settings.ProjectToTypeResolvers.GetValueOrDefault(s.MemeberName); + + if (match != null) + { + arg.Settings.Resolvers.Add(new InvokerModel + { + Condition = null, + DestinationMemberName = destinationMember.Name, + Invoker = (LambdaExpression)match.Operand, + SourceMemberName = null, + IsChildPath = false + + }); + } + + getter = (from fn in resolvers + from src in sources + select fn(src, destinationMember, arg)) + .FirstOrDefault(result => result != null); + } + + + if (arg.MapType == MapType.Projection) + { + + var checkgetter = (from fn in resolvers.Where(ValueAccessingStrategy.CustomResolvers.Contains) + from src in sources + select fn(src, destinationMember, arg)) + .FirstOrDefault(result => result != null); + + if (checkgetter == null) + { + Type destinationType; + + if (destinationMember.Type.IsNullable()) + destinationType = destinationMember.Type.GetGenericArguments()[0]; + else + destinationType = destinationMember.Type; + + if (arg.Settings.ProjectToTypeMapConfig == Enums.ProjectToTypeAutoMapping.OnlyPrimitiveTypes + && destinationType.IsMapsterPrimitive() == false) + continue; + + if (arg.Settings.ProjectToTypeMapConfig == Enums.ProjectToTypeAutoMapping.WithoutCollections + && destinationType.IsCollectionCompatible() == true) + continue; + } + + } + + var nextIgnore = arg.Settings.Ignore.Next((ParameterExpression)source, (ParameterExpression?)destination, destinationMember.Name); var nextResolvers = arg.Settings.Resolvers.Next(arg.Settings.Ignore, (ParameterExpression)source, destinationMember.Name) .ToList(); @@ -54,6 +115,20 @@ select fn(src, destinationMember, arg)) Destination = (ParameterExpression?)destination, UseDestinationValue = arg.MapType != MapType.Projection && destinationMember.UseDestinationValue(arg), }; + if(getter == null && !arg.DestinationType.IsRecordType() + && destinationMember.Info is PropertyInfo propinfo) + { + if (propinfo.GetCustomAttributes() + .Any(y => y.GetType() == typeof(System.Runtime.CompilerServices.RequiredMemberAttribute))) + { + getter = destinationMember.Type.CreateDefault(); + } + } + + if (arg.MapType == MapType.MapToTarget && getter == null && arg.DestinationType.IsRecordType()) + { + getter = TryRestoreRecordMember(destinationMember, recordRestorMemberModel, destination) ?? getter; + } if (getter != null) { propertyModel.Getter = arg.MapType == MapType.Projection @@ -80,7 +155,8 @@ select fn(src, destinationMember, arg)) { if (classModel.BreakOnUnmatched) return null!; - unmappedDestinationMembers.Add(destinationMember.Name); + if(!arg.Settings.Ignore.Any(x=>x.Key == destinationMember.Name)) // Don't mark a constructor parameter if it was explicitly ignored + unmappedDestinationMembers.Add(destinationMember.Name); } properties.Add(propertyModel); @@ -128,7 +204,7 @@ protected static bool ProcessIgnores( && ignore.Condition == null; } - protected Expression CreateInstantiationExpression(Expression source, ClassMapping classConverter, CompileArgument arg) + protected Expression CreateInstantiationExpression(Expression source, ClassMapping classConverter, CompileArgument arg, Expression? destination, ClassModel recordRestorParamModel = null) { var members = classConverter.Members; @@ -144,10 +220,24 @@ protected Expression CreateInstantiationExpression(Expression source, ClassMappi if (member.Getter == null) { getter = defaultConst; + + if (arg.MapType == MapType.MapToTarget && arg.DestinationType.IsRecordType()) + getter = TryRestoreRecordMember(member.DestinationMember,recordRestorParamModel,destination) ?? getter; } else { - getter = CreateAdaptExpression(member.Getter, member.DestinationMember.Type, arg, member); + + if (member.Getter.CanBeNull() && member.DestinationMember.Type.IsAbstractOrNotPublicCtor() + && member.Ignore.Condition == null) + { + var compareNull = Expression.Equal(member.Getter, Expression.Constant(null, member.Getter.Type)); + getter = Expression.Condition(ExpressionEx.Not(compareNull), + CreateAdaptExpression(member.Getter, member.DestinationMember.Type, arg, member), + defaultConst); + } + else + getter = CreateAdaptExpression(member.Getter, member.DestinationMember.Type, arg, member); + if (member.Ignore.Condition != null) { var body = member.Ignore.IsChildPath @@ -156,6 +246,14 @@ protected Expression CreateInstantiationExpression(Expression source, ClassMappi var condition = ExpressionEx.Not(body); getter = Expression.Condition(condition, getter, defaultConst); } + else + if (arg.Settings.Ignore.Any(x => x.Key == member.DestinationMember.Name)) + { + getter = defaultConst; + + if (arg.MapType == MapType.MapToTarget && arg.DestinationType.IsRecordType()) + getter = TryRestoreRecordMember(member.DestinationMember, recordRestorParamModel, destination) ?? getter; + } } arguments.Add(getter); } @@ -181,6 +279,63 @@ protected virtual ClassModel GetSetterModel(CompileArgument arg) }; } + protected void IgnoreNonMapped (ClassModel classModel, CompileArgument arg) + { + var notMappingToIgnore = classModel.Members + .ExceptBy(arg.Settings.Resolvers.Select(x => x.DestinationMemberName), + y => y.Name); + + foreach (var item in notMappingToIgnore) + { + arg.Settings.Ignore.TryAdd(item.Name, new IgnoreDictionary.IgnoreItem()); + } + } + + protected virtual ClassModel GetOnlyRequiredPropertySetterModel(CompileArgument arg) + { + return new ClassModel + { + Members = arg.DestinationType.GetFieldsAndProperties(true) + .Where(x => x.GetType() == typeof(PropertyModel)) + .Where(y => ((PropertyInfo)y.Info).GetCustomAttributes() + .Any(y => y.GetType() == typeof(System.Runtime.CompilerServices.RequiredMemberAttribute))) + }; + } + + protected Expression? TryRestoreRecordMember(IMemberModelEx member, ClassModel? restorRecordModel, Expression? destination) + { + if (restorRecordModel != null && destination != null) + { + var find = restorRecordModel.Members + .Where(x => x.Name == member.Name).FirstOrDefault(); + + if (find != null) + { + var compareNull = Expression.Equal(destination, Expression.Constant(null, destination.Type)); + return Expression.Condition(compareNull, member.Type.CreateDefault(), Expression.MakeMemberAccess(destination, (MemberInfo)find.Info)); + } + + } + + return null; + } + + protected static Expression SetValueTypeAutoPropertyByReflection(MemberMapping member, Expression adapt, ClassModel checkmodel) + { + var modDesinationMemeberName = $"<{member.DestinationMember.Name}>k__BackingField"; + if (checkmodel.Members.Any(x => x.Name == modDesinationMemeberName) == false) // Property is not autoproperty + return Expression.Empty(); + var typeofExpression = Expression.Constant(member.Destination!.Type); + var getPropertyMethod = typeof(Type).GetMethod("GetField", new[] { typeof(string), typeof(BindingFlags) })!; + var getPropertyExpression = Expression.Call(typeofExpression, getPropertyMethod, + Expression.Constant(modDesinationMemeberName), Expression.Constant(BindingFlags.Instance | BindingFlags.NonPublic)); + var setValueMethod = + typeof(FieldInfo).GetMethod("SetValue", new[] { typeof(object), typeof(object) })!; + var memberAsObject = adapt.To(typeof(object)); + return Expression.Call(getPropertyExpression, setValueMethod, + new[] { member.Destination, memberAsObject }); + } + #endregion } } diff --git a/src/Mapster/Adapters/ClassAdapter.cs b/src/Mapster/Adapters/ClassAdapter.cs index cc803d8d..9e44105d 100644 --- a/src/Mapster/Adapters/ClassAdapter.cs +++ b/src/Mapster/Adapters/ClassAdapter.cs @@ -53,8 +53,8 @@ protected override bool CanInline(Expression source, Expression? destination, Co protected override Expression CreateInstantiationExpression(Expression source, Expression? destination, CompileArgument arg) { //new TDestination(src.Prop1, src.Prop2) - - if (arg.GetConstructUsing() != null || arg.Settings.MapToConstructor == null) + + if (arg.DestinationType.isDefaultCtor() || arg.GetConstructUsing() != null && arg.Settings.MapToConstructor == null) return base.CreateInstantiationExpression(source, destination, arg); ClassMapping? classConverter; @@ -69,19 +69,19 @@ protected override Expression CreateInstantiationExpression(Expression source, E classConverter = destType.GetConstructors() .OrderByDescending(it => it.GetParameters().Length) .Select(it => GetConstructorModel(it, true)) - .Select(it => CreateClassConverter(source, it, arg)) + .Select(it => CreateClassConverter(source, it, arg, ctorMapping:true)) .FirstOrDefault(it => it != null); } else { var model = GetConstructorModel(ctor, false); - classConverter = CreateClassConverter(source, model, arg); + classConverter = CreateClassConverter(source, model, arg, ctorMapping:true); } if (classConverter == null) return base.CreateInstantiationExpression(source, destination, arg); - return CreateInstantiationExpression(source, classConverter, arg); + return CreateInstantiationExpression(source, classConverter, arg, destination); } protected override Expression CreateBlockExpression(Expression source, Expression destination, CompileArgument arg) @@ -104,14 +104,22 @@ protected override Expression CreateBlockExpression(Expression source, Expressio Dictionary, Expression>>? conditions = null; foreach (var member in members) { - if (!member.UseDestinationValue && member.DestinationMember.SetterModifier == AccessModifier.None) - continue; - var destMember = arg.MapType == MapType.MapToTarget || member.UseDestinationValue ? member.DestinationMember.GetExpression(destination) : null; var adapt = CreateAdaptExpression(member.Getter, member.DestinationMember.Type, arg, member, destMember); + + if (!member.UseDestinationValue && member.DestinationMember.SetterModifier == AccessModifier.None) + { + if (member.DestinationMember is PropertyModel && arg.MapType == MapType.MapToTarget) + adapt = SetValueTypeAutoPropertyByReflection(member, adapt, classModel); + else + continue; + if (adapt == Expression.Empty()) + continue; + } + if (!member.UseDestinationValue) { if (arg.Settings.IgnoreNullValues == true && member.Getter.CanBeNull()) @@ -132,10 +140,14 @@ protected override Expression CreateBlockExpression(Expression source, Expressio //Todo Try catch block should be removed after pull request approved try { - var destinationPropertyInfo = (PropertyInfo)member.DestinationMember.Info!; - adapt = destinationPropertyInfo.IsInitOnly() - ? SetValueByReflection(destination, (MemberExpression)adapt, arg.DestinationType) - : member.DestinationMember.SetExpression(destination, adapt); + if (member.DestinationMember.SetterModifier != AccessModifier.None) + { + var destinationPropertyInfo = (PropertyInfo)member.DestinationMember.Info!; + adapt = destinationPropertyInfo.IsInitOnly() + ? SetValueByReflection(member, (MemberExpression)adapt) + : member.DestinationMember.SetExpression(destination, adapt); + } + } catch (Exception e) { @@ -178,22 +190,20 @@ protected override Expression CreateBlockExpression(Expression source, Expressio return lines.Count > 0 ? (Expression)Expression.Block(lines) : Expression.Empty(); } - private static Expression SetValueByReflection(Expression destination, MemberExpression adapt, - Type destinationType) + private static Expression SetValueByReflection(MemberMapping member, MemberExpression adapt) { - var memberName = adapt.Member.Name; - var typeofExpression = Expression.Constant(destinationType); - var getPropertyMethod = typeof(Type).GetMethod("GetProperty", new[] { typeof(string) })!; + var typeofExpression = Expression.Constant(member.Destination!.Type); + var getPropertyMethod = typeof(Type).GetMethod("GetProperty", new[] { typeof(string), typeof(Type) })!; var getPropertyExpression = Expression.Call(typeofExpression, getPropertyMethod, - Expression.Constant(memberName)); + new[] { Expression.Constant(member.DestinationMember.Name), Expression.Constant(member.DestinationMember.Type) }); var setValueMethod = typeof(PropertyInfo).GetMethod("SetValue", new[] { typeof(object), typeof(object) })!; var memberAsObject = adapt.To(typeof(object)); return Expression.Call(getPropertyExpression, setValueMethod, - new[] { destination, memberAsObject }); + new[] { member.Destination, memberAsObject }); } - protected override Expression? CreateInlineExpression(Expression source, CompileArgument arg) + protected override Expression? CreateInlineExpression(Expression source, CompileArgument arg, bool IsRequiredOnly = false) { //new TDestination { // Prop1 = convert(src.Prop1), @@ -203,9 +213,19 @@ private static Expression SetValueByReflection(Expression destination, MemberExp var exp = CreateInstantiationExpression(source, arg); var memberInit = exp as MemberInitExpression; var newInstance = memberInit?.NewExpression ?? (NewExpression)exp; - var contructorMembers = newInstance.Arguments.OfType().Select(me => me.Member).ToArray(); - var classModel = GetSetterModel(arg); - var classConverter = CreateClassConverter(source, classModel, arg); + var contructorMembers = newInstance.GetAllMemberExpressionsMemberInfo().ToArray(); + ClassModel? classModel; + ClassMapping? classConverter; + if (IsRequiredOnly) + { + classModel = GetOnlyRequiredPropertySetterModel(arg); + classConverter = CreateClassConverter(source, classModel, arg, ctorMapping: true); + } + else + { + classModel = GetSetterModel(arg); + classConverter = CreateClassConverter(source, classModel, arg); + } var members = classConverter.Members; var lines = new List(); diff --git a/src/Mapster/Adapters/CollectionAdapter.cs b/src/Mapster/Adapters/CollectionAdapter.cs index 861b1383..01189614 100644 --- a/src/Mapster/Adapters/CollectionAdapter.cs +++ b/src/Mapster/Adapters/CollectionAdapter.cs @@ -115,7 +115,7 @@ protected override Expression CreateBlockExpression(Expression source, Expressio return Expression.Block(actions); } - protected override Expression CreateInlineExpression(Expression source, CompileArgument arg) + protected override Expression CreateInlineExpression(Expression source, CompileArgument arg, bool IsRequiredOnly = false) { if (arg.DestinationType.GetTypeInfo().IsAssignableFrom(source.Type.GetTypeInfo()) && (arg.Settings.ShallowCopyForSameType == true || arg.MapType == MapType.Projection)) diff --git a/src/Mapster/Adapters/DelegateAdapter.cs b/src/Mapster/Adapters/DelegateAdapter.cs index 0c16e315..2eb33ab3 100644 --- a/src/Mapster/Adapters/DelegateAdapter.cs +++ b/src/Mapster/Adapters/DelegateAdapter.cs @@ -30,7 +30,7 @@ protected override Expression CreateBlockExpression(Expression source, Expressio return Expression.Empty(); } - protected override Expression CreateInlineExpression(Expression source, CompileArgument arg) + protected override Expression CreateInlineExpression(Expression source, CompileArgument arg, bool IsRequiredOnly = false) { return CreateInstantiationExpression(source, arg); } diff --git a/src/Mapster/Adapters/DictionaryAdapter.cs b/src/Mapster/Adapters/DictionaryAdapter.cs index 25af8e3b..1f5ef77e 100644 --- a/src/Mapster/Adapters/DictionaryAdapter.cs +++ b/src/Mapster/Adapters/DictionaryAdapter.cs @@ -181,7 +181,7 @@ private Expression CreateSetFromKvp(Expression kvp, Expression key, Expression d return destSetFn(destination, key, value); } - protected override Expression CreateInlineExpression(Expression source, CompileArgument arg) + protected override Expression CreateInlineExpression(Expression source, CompileArgument arg, bool IsRequiredOnly = false) { //new TDestination { // { "Prop1", convert(src.Prop1) }, @@ -219,7 +219,7 @@ protected override Expression CreateInlineExpression(Expression source, CompileA protected override ClassModel GetSetterModel(CompileArgument arg) { //get member name from map - var destNames = arg.GetDestinationNames().AsEnumerable(); + var destNames = arg.GetDestinationNames(false).AsEnumerable(); //get member name from properties if (arg.SourceType.GetDictionaryType() == null) diff --git a/src/Mapster/Adapters/MultiDimensionalArrayAdapter.cs b/src/Mapster/Adapters/MultiDimensionalArrayAdapter.cs index 01fa6931..8c44d04a 100644 --- a/src/Mapster/Adapters/MultiDimensionalArrayAdapter.cs +++ b/src/Mapster/Adapters/MultiDimensionalArrayAdapter.cs @@ -91,7 +91,7 @@ protected override Expression CreateBlockExpression(Expression source, Expressio return CreateArraySet(source, destination, arg); } - protected override Expression CreateInlineExpression(Expression source, CompileArgument arg) + protected override Expression CreateInlineExpression(Expression source, CompileArgument arg, bool IsRequiredOnly = false) { throw new NotImplementedException(); } diff --git a/src/Mapster/Adapters/ObjectAdapter.cs b/src/Mapster/Adapters/ObjectAdapter.cs index 71817886..3140549a 100644 --- a/src/Mapster/Adapters/ObjectAdapter.cs +++ b/src/Mapster/Adapters/ObjectAdapter.cs @@ -27,7 +27,7 @@ protected override Expression CreateBlockExpression(Expression source, Expressio return Expression.Empty(); } - protected override Expression CreateInlineExpression(Expression source, CompileArgument arg) + protected override Expression CreateInlineExpression(Expression source, CompileArgument arg, bool IsRequiredOnly = false) { return CreateInstantiationExpression(source, arg); } diff --git a/src/Mapster/Adapters/PrimitiveAdapter.cs b/src/Mapster/Adapters/PrimitiveAdapter.cs index 2a407a89..c2d411db 100644 --- a/src/Mapster/Adapters/PrimitiveAdapter.cs +++ b/src/Mapster/Adapters/PrimitiveAdapter.cs @@ -18,6 +18,26 @@ protected override bool CanMap(PreCompileArgument arg) protected override Expression CreateExpressionBody(Expression source, Expression? destination, CompileArgument arg) { + if (arg.SourceType != arg.DestinationType && arg.Context.Config.RequireExplicitMappingPrimitive && !arg.ExplicitMapping) + throw new InvalidOperationException("Implicit mapping is not allowed (check GlobalSettings.RequireExplicitMapping) and no configuration exists"); + + if (arg.Settings.MapToTargetPrimitive == true) + { + Expression dest; + + if (destination == null) + { + dest = arg.DestinationType.CreateDefault(); + } + else + dest = destination; + + var customConvert = arg.Context.Config.CreateMapToTargetInvokeExpressionBody(source.Type, arg.DestinationType, source, dest); + + arg.MapType = MapType.MapToTarget; + return customConvert; + } + Expression convert = source; var sourceType = arg.SourceType; var destinationType = arg.DestinationType; @@ -74,7 +94,7 @@ protected override Expression CreateBlockExpression(Expression source, Expressio throw new NotImplementedException(); } - protected override Expression CreateInlineExpression(Expression source, CompileArgument arg) + protected override Expression CreateInlineExpression(Expression source, CompileArgument arg, bool IsRequiredOnly = false) { throw new NotImplementedException(); } diff --git a/src/Mapster/Adapters/ReadOnlyInterfaceAdapter.cs b/src/Mapster/Adapters/ReadOnlyInterfaceAdapter.cs new file mode 100644 index 00000000..ea4aa6e1 --- /dev/null +++ b/src/Mapster/Adapters/ReadOnlyInterfaceAdapter.cs @@ -0,0 +1,68 @@ +using Mapster.Utils; +using System.Linq; +using System.Linq.Expressions; + +namespace Mapster.Adapters +{ + internal class ReadOnlyInterfaceAdapter : ClassAdapter + { + protected override int Score => -148; + + protected override bool CanMap(PreCompileArgument arg) + { + return arg.DestinationType.IsInterface; + } + + protected override bool CanInline(Expression source, Expression? destination, CompileArgument arg) + { + if (base.CanInline(source, destination, arg)) + return true; + else + return false; + + } + + protected override Expression CreateInstantiationExpression(Expression source, Expression? destination, CompileArgument arg) + { + var destintionType = arg.DestinationType; + var props = destintionType.GetFieldsAndProperties().ToList(); + + //interface with readonly props + if (props.Any(p => p.SetterModifier != AccessModifier.Public)) + { + if (arg.GetConstructUsing() != null) + return base.CreateInstantiationExpression(source, destination, arg); + + var destType = DynamicTypeGenerator.GetTypeForInterface(arg.DestinationType, arg.Settings.Includes.Count > 0); + if (destType == null) + return base.CreateInstantiationExpression(source, destination, arg); + var ctor = destType.GetConstructors()[0]; + var classModel = GetConstructorModel(ctor, false); + var classConverter = CreateClassConverter(source, classModel, arg, ctorMapping:true); + return CreateInstantiationExpression(source, classConverter, arg, destination); + } + else + return base.CreateInstantiationExpression(source,destination, arg); + } + + protected override Expression CreateExpressionBody(Expression source, Expression? destination, CompileArgument arg) + { + if (source.Type.IsInterface) + { + if (!arg.DestinationType.IsAssignableFrom(arg.SourceType)) + return base.CreateExpressionBody(source, destination, arg); + + if (arg.MapType != MapType.MapToTarget) + return Expression.Convert(source, arg.DestinationType); + + if (arg.MapType == MapType.MapToTarget) + return source; + + return base.CreateExpressionBody(source, destination, arg); + } + + return base.CreateExpressionBody(source, destination, arg); + } + + } +} diff --git a/src/Mapster/Adapters/RecordTypeAdapter.cs b/src/Mapster/Adapters/RecordTypeAdapter.cs index e5812e16..06a3c421 100644 --- a/src/Mapster/Adapters/RecordTypeAdapter.cs +++ b/src/Mapster/Adapters/RecordTypeAdapter.cs @@ -1,46 +1,300 @@ -using System.Linq.Expressions; -using System.Reflection; +using Mapster.Models; using Mapster.Utils; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using static Mapster.IgnoreDictionary; namespace Mapster.Adapters { - internal class RecordTypeAdapter : BaseClassAdapter + internal class RecordTypeAdapter : ClassAdapter { + private ClassMapping? ClassConverterContext; protected override int Score => -149; protected override bool UseTargetValue => false; + private List SkipIgnoreNullValuesMemberMap = new List(); + protected override bool CanMap(PreCompileArgument arg) { return arg.DestinationType.IsRecordType(); } + protected override bool CanInline(Expression source, Expression? destination, CompileArgument arg) + { + return false; + } + + protected override Expression CreateInlineExpression(Expression source, CompileArgument arg, bool IsRequiredOnly = false) + { + return base.CreateInstantiationExpression(source, arg); + } protected override Expression CreateInstantiationExpression(Expression source, Expression? destination, CompileArgument arg) { //new TDestination(src.Prop1, src.Prop2) - if (arg.GetConstructUsing() != null) - return base.CreateInstantiationExpression(source, destination, arg); - - var destType = arg.DestinationType.GetTypeInfo().IsInterface - ? DynamicTypeGenerator.GetTypeForInterface(arg.DestinationType, arg.Settings.Includes.Count > 0) - : arg.DestinationType; - if (destType == null) - return base.CreateInstantiationExpression(source, destination, arg); - var ctor = destType.GetConstructors()[0]; - var classModel = GetConstructorModel(ctor, false); - var classConverter = CreateClassConverter(source, classModel, arg); - return CreateInstantiationExpression(source, classConverter, arg); + SkipIgnoreNullValuesMemberMap.Clear(); + Expression installExpr; + + if (arg.GetConstructUsing() != null || arg.DestinationType == null) + installExpr = base.CreateInstantiationExpression(source, destination, arg); + else + { + var ctor = arg.DestinationType.GetConstructors() + .OrderByDescending(it => it.GetParameters().Length).ToArray().FirstOrDefault(); // Will be used public constructor with the maximum number of parameters + var classModel = GetConstructorModel(ctor, false); + var restorParamModel = GetSetterModel(arg); + var classConverter = CreateClassConverter(source, classModel, arg, ctorMapping: true); + installExpr = CreateInstantiationExpression(source, classConverter, arg, destination, restorParamModel); + } + + + return RecordInlineExpression(source, destination, arg, installExpr); // Activator field when not include in public ctor } + + private Expression? RecordInlineExpression(Expression source, Expression? destination, CompileArgument arg, Expression installExpr) + { + //new TDestination { + // Prop1 = convert(src.Prop1), + // Prop2 = convert(src.Prop2), + //} - protected override Expression CreateBlockExpression(Expression source, Expression destination, CompileArgument arg) + var exp = installExpr; + var memberInit = exp as MemberInitExpression; + var newInstance = memberInit?.NewExpression ?? (NewExpression)exp; + var contructorMembers = newInstance.Constructor?.GetParameters().ToList() ?? new(); + var classModel = GetSetterModel(arg); + var classConverter = CreateClassConverter(source, classModel, arg, destination: destination, recordRestorMemberModel: classModel); + var members = classConverter.Members; + + ClassConverterContext = classConverter; + + var lines = new List(); + if (memberInit != null) + lines.AddRange(memberInit.Bindings); + foreach (var member in members) + { + + if (!arg.Settings.Resolvers.Any(r => r.DestinationMemberName == member.DestinationMember.Name) + && contructorMembers.Any(x => string.Equals(x.Name, member.DestinationMember.Name, StringComparison.InvariantCultureIgnoreCase))) + continue; + + if (member.DestinationMember.SetterModifier == AccessModifier.None) + continue; + + var adapt = CreateAdaptExpression(member.Getter, member.DestinationMember.Type, arg, member); + + if (arg.Settings.IgnoreNullValues == true && member.Getter.CanBeNull()) // add IgnoreNullValues support + { + if (arg.MapType != MapType.MapToTarget) + { + SkipIgnoreNullValuesMemberMap.Add(member); + continue; + } + + if (adapt is ConditionalExpression condEx) + { + if (condEx.Test is BinaryExpression { NodeType: ExpressionType.Equal } binEx && + binEx.Left == member.Getter && + binEx.Right is ConstantExpression { Value: null }) + adapt = condEx.IfFalse; + } + var destinationCompareNull = Expression.Equal(destination, Expression.Constant(null, destination.Type)); + var sourceCondition = Expression.NotEqual(member.Getter, Expression.Constant(null, member.Getter.Type)); + var destinationCanbeNull = Expression.Condition(destinationCompareNull, member.DestinationMember.Type.CreateDefault(), member.DestinationMember.GetExpression(destination)); + adapt = Expression.Condition(sourceCondition, adapt, destinationCanbeNull); + } + + + + + //special null property check for projection + //if we don't set null to property, EF will create empty object + //except collection type & complex type which cannot be null + if (arg.MapType == MapType.Projection + && member.Getter.Type != member.DestinationMember.Type + && !member.Getter.Type.IsCollection() + && !member.DestinationMember.Type.IsCollection() + && member.Getter.Type.GetTypeInfo().GetCustomAttributesData().All(attr => attr.GetAttributeType().Name != "ComplexTypeAttribute")) + { + adapt = member.Getter.NotNullReturn(adapt); + } + var bind = Expression.Bind((MemberInfo)member.DestinationMember.Info!, adapt); + lines.Add(bind); + } + + if (arg.MapType == MapType.MapToTarget) + lines.AddRange(RecordIngnoredWithoutConditonRestore(destination, arg, contructorMembers, classModel)); + + return Expression.MemberInit(newInstance, lines); + } + + private List RecordIngnoredWithoutConditonRestore(Expression? destination, CompileArgument arg, List contructorMembers, ClassModel restorPropertyModel) { - return Expression.Empty(); + var members = restorPropertyModel.Members + .Where(x => arg.Settings.Ignore.Any(y => y.Key == x.Name)); + + var lines = new List(); + + + foreach (var member in members) + { + if (destination == null) + continue; + + IgnoreItem ignore; + ProcessIgnores(arg, member, out ignore); + + if (member.SetterModifier == AccessModifier.None || + ignore.Condition != null || + contructorMembers.Any(x => string.Equals(x.Name, member.Name, StringComparison.InvariantCultureIgnoreCase))) + continue; + + lines.Add(Expression.Bind((MemberInfo)member.Info, Expression.MakeMemberAccess(destination, (MemberInfo)member.Info))); + } + + return lines; } - protected override Expression CreateInlineExpression(Expression source, CompileArgument arg) + protected override Expression CreateBlockExpression(Expression source, Expression destination, CompileArgument arg) { - return CreateInstantiationExpression(source, arg); + // Mapping property Without setter when UseDestinationValue == true + + var result = destination; + var classModel = GetSetterModel(arg); + var classConverter = CreateClassConverter(source, classModel, arg, result); + var members = classConverter.Members; + + var lines = new List(); + + if (arg.MapType != MapType.MapToTarget) + { + foreach (var member in SkipIgnoreNullValuesMemberMap) + { + + var adapt = CreateAdaptExpression(member.Getter, member.DestinationMember.Type, arg, member); + + if (adapt is ConditionalExpression condEx) + { + if (condEx.Test is BinaryExpression { NodeType: ExpressionType.Equal } binEx && + binEx.Left == member.Getter && + binEx.Right is ConstantExpression { Value: null }) + adapt = condEx.IfFalse; + } + adapt = member.DestinationMember.SetExpression(destination, adapt); + var sourceCondition = Expression.NotEqual(member.Getter, Expression.Constant(null, member.Getter.Type)); + + + lines.Add(Expression.IfThen(sourceCondition, adapt)); + } + } + + + foreach (var member in members) + { + if (member.DestinationMember.SetterModifier == AccessModifier.None && member.UseDestinationValue) + { + + if (member.DestinationMember is PropertyModel && member.DestinationMember.Type.IsValueType + || member.DestinationMember.Type.IsMapsterPrimitive() + || member.DestinationMember.Type.IsRecordType()) + { + + Expression adapt; + if (member.DestinationMember.Type.IsRecordType()) + adapt = arg.Context.Config.CreateMapInvokeExpressionBody(member.Getter.Type, member.DestinationMember.Type, member.Getter); + else + adapt = CreateAdaptExpression(member.Getter, member.DestinationMember.Type, arg, member, result); + + var blocks = Expression.Block(SetValueTypeAutoPropertyByReflection(member, adapt, classModel)); + var lambda = Expression.Lambda(blocks, parameters: new[] { (ParameterExpression)source, (ParameterExpression)destination }); + + if (arg.Settings.IgnoreNullValues == true && member.Getter.CanBeNull()) + { + + if (arg.MapType != MapType.MapToTarget) + { + var condition = Expression.NotEqual(member.Getter, Expression.Constant(null, member.Getter.Type)); + lines.Add(Expression.IfThen(condition, Expression.Invoke(lambda, source, destination))); + continue; + } + + if (arg.MapType == MapType.MapToTarget) + { + var var2Param = ClassConverterContext.Members.Where(x => x.DestinationMember.Name == member.DestinationMember.Name).FirstOrDefault(); + + Expression destMemberVar2 = var2Param.DestinationMember.GetExpression(var2Param.Destination); + var ParamLambdaVar2 = destMemberVar2; + if(member.DestinationMember.Type.IsRecordType()) + ParamLambdaVar2 = arg.Context.Config.CreateMapInvokeExpressionBody(member.Getter.Type, member.DestinationMember.Type, destMemberVar2); + + var blocksVar2 = Expression.Block(SetValueTypeAutoPropertyByReflection(member, ParamLambdaVar2, classModel)); + var lambdaVar2 = Expression.Lambda(blocksVar2, parameters: new[] { (ParameterExpression)var2Param.Destination, (ParameterExpression)destination }); + var adaptVar2 = Expression.Invoke(lambdaVar2, var2Param.Destination, destination); + + + Expression conditionVar2; + if (destMemberVar2.CanBeNull()) + { + var complexcheck = Expression.AndAlso(Expression.NotEqual(var2Param.Destination, Expression.Constant(null, var2Param.Destination.Type)), // if(var2 != null && var2.Prop != null) + Expression.NotEqual(destMemberVar2, Expression.Constant(null, var2Param.Getter.Type))); + conditionVar2 = Expression.IfThen(complexcheck, adaptVar2); + } + else + conditionVar2 = Expression.IfThen(Expression.NotEqual(var2Param.Destination, Expression.Constant(null, var2Param.Destination.Type)), adaptVar2); + + var condition = Expression.NotEqual(member.Getter, Expression.Constant(null, member.Getter.Type)); + lines.Add(Expression.IfThenElse(condition, Expression.Invoke(lambda, source, destination), conditionVar2)); + continue; + } + } + + lines.Add(Expression.Invoke(lambda, source, destination)); + } + else + { + var destMember = member.DestinationMember.GetExpression(destination); + var adapt = CreateAdaptExpression(member.Getter, member.DestinationMember.Type, arg, member, destMember); + + if (arg.Settings.IgnoreNullValues == true && member.Getter.CanBeNull()) + { + if (arg.MapType != MapType.MapToTarget) + { + var condition = Expression.NotEqual(member.Getter, Expression.Constant(null, member.Getter.Type)); + lines.Add(Expression.IfThen(condition, adapt)); + continue; + } + if (arg.MapType == MapType.MapToTarget) + { + var var2Param = ClassConverterContext.Members.Where(x => x.DestinationMember.Name == member.DestinationMember.Name).FirstOrDefault(); + + var destMemberVar2 = var2Param.DestinationMember.GetExpression(var2Param.Destination); + var adaptVar2 = CreateAdaptExpression(destMemberVar2, member.DestinationMember.Type, arg, var2Param, destMember); + + var complexcheck = Expression.AndAlso(Expression.NotEqual(var2Param.Destination, Expression.Constant(null, var2Param.Destination.Type)), // if(var2 != null && var2.Prop != null) + Expression.NotEqual(destMemberVar2, Expression.Constant(null, var2Param.Getter.Type))); + var conditionVar2 = Expression.IfThen(complexcheck, adaptVar2); + + var condition = Expression.NotEqual(member.Getter, Expression.Constant(null, member.Getter.Type)); + lines.Add(Expression.IfThenElse(condition, adapt, conditionVar2)); + continue; + } + + + } + + lines.Add(adapt); + } + + } + } + + return lines.Count > 0 ? (Expression)Expression.Block(lines) : Expression.Empty(); } + + } } diff --git a/src/Mapster/Compile/CompileArgument.cs b/src/Mapster/Compile/CompileArgument.cs index 2e15637a..e9015449 100644 --- a/src/Mapster/Compile/CompileArgument.cs +++ b/src/Mapster/Compile/CompileArgument.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; -using Mapster.Utils; namespace Mapster { @@ -15,21 +14,24 @@ public class CompileArgument public TypeAdapterSettings Settings { get; set; } public CompileContext Context { get; set; } public bool UseDestinationValue { get; set; } + public bool? ConstructorMapping { get; set; } private HashSet? _srcNames; internal HashSet GetSourceNames() { return _srcNames ??= (from it in Settings.Resolvers - where it.SourceMemberName != null - select it.SourceMemberName!.Split('.').First()).ToHashSet(); + where it.SourceMemberName != null + select it.SourceMemberName!.Split('.').First()).ToHashSet(); } private HashSet? _destNames; - internal HashSet GetDestinationNames() + internal HashSet GetDestinationNames(bool split = true) { return _destNames ??= (from it in Settings.Resolvers - where it.DestinationMemberName != null - select it.DestinationMemberName.Split('.').First()).ToHashSet(); + where it.DestinationMemberName != null + select split + ? it.DestinationMemberName.Split('.').First() + : it.DestinationMemberName).ToHashSet(); } private bool _fetchConstructUsing; diff --git a/src/Mapster/Directory.Build.props b/src/Mapster/Directory.Build.props new file mode 100644 index 00000000..184391ef --- /dev/null +++ b/src/Mapster/Directory.Build.props @@ -0,0 +1,14 @@ + + + true + Mapster.snk + + + true + false + + + true + false + + \ No newline at end of file diff --git a/src/Mapster/Mapster.csproj b/src/Mapster/Mapster.csproj index 81283f31..bc6f2dd1 100644 --- a/src/Mapster/Mapster.csproj +++ b/src/Mapster/Mapster.csproj @@ -4,11 +4,11 @@ A fast, fun and stimulating object to object mapper. Kind of like AutoMapper, just simpler and way, way faster. Copyright (c) 2016 Chaowlert Chaisrichalermpol, Eric Swann chaowlert;eric_swann - net7.0;net6.0 + net10.0;net9.0;net8.0; Mapster A fast, fun and stimulating object to object mapper. Kind of like AutoMapper, just simpler and way, way faster. Mapster - Mapper;AutoMapper;Fast;Mapping + Mapster;Mapper;AutoMapper;Fast;Mapping icon.png https://cloud.githubusercontent.com/assets/5763993/26522718/d16f3e42-4330-11e7-9b78-f8c7402624e7.png https://github.com/MapsterMapper/Mapster @@ -16,7 +16,7 @@ true Mapster - 7.4.0-pre06 + 10.0.0-pre01 enable 1701;1702;8618 diff --git a/src/Mapster/Settings/SettingStore.cs b/src/Mapster/Settings/SettingStore.cs index e89c776e..821b8569 100644 --- a/src/Mapster/Settings/SettingStore.cs +++ b/src/Mapster/Settings/SettingStore.cs @@ -25,6 +25,12 @@ public void Set(string key, object? value) _objectStore[key] = value; } + + public T GetEnum(string key, Func initializer) where T : System.Enum + { + return (T)_objectStore.GetOrAdd(key, _ => initializer()); + } + public bool? Get(string key) { return _booleanStore.TryGetValue(key, out var value) ? value : null; diff --git a/src/Mapster/Settings/ValueAccessingStrategy.cs b/src/Mapster/Settings/ValueAccessingStrategy.cs index ea47d9ad..fd13407d 100644 --- a/src/Mapster/Settings/ValueAccessingStrategy.cs +++ b/src/Mapster/Settings/ValueAccessingStrategy.cs @@ -73,10 +73,10 @@ public static class ValueAccessingStrategy { var members = source.Type.GetFieldsAndProperties(true); var strategy = arg.Settings.NameMatchingStrategy; - var destinationMemberName = destinationMember.GetMemberName(MemberSide.Destination, arg.Settings.GetMemberNames, strategy.DestinationMemberNameConverter); + var destinationMemberName = destinationMember.GetMemberName(MemberSide.Destination, arg.Settings.GetMemberNames, strategy.DestinationMemberNameConverter, arg); return members .Where(member => member.ShouldMapMember(arg, MemberSide.Source)) - .Where(member => member.GetMemberName(MemberSide.Source, arg.Settings.GetMemberNames, strategy.SourceMemberNameConverter) == destinationMemberName) + .Where(member => member.GetMemberName(MemberSide.Source, arg.Settings.GetMemberNames, strategy.SourceMemberNameConverter, arg) == destinationMemberName) .Select(member => member.GetExpression(source)) .FirstOrDefault(); } @@ -86,7 +86,7 @@ public static class ValueAccessingStrategy if (arg.MapType == MapType.Projection) return null; var strategy = arg.Settings.NameMatchingStrategy; - var destinationMemberName = "Get" + destinationMember.GetMemberName(MemberSide.Destination, arg.Settings.GetMemberNames, strategy.DestinationMemberNameConverter); + var destinationMemberName = "Get" + destinationMember.GetMemberName(MemberSide.Destination, arg.Settings.GetMemberNames, strategy.DestinationMemberNameConverter, arg); var getMethod = Array.Find(source.Type.GetMethods(BindingFlags.Public | BindingFlags.Instance), m => strategy.SourceMemberNameConverter(m.Name) == destinationMemberName && m.GetParameters().Length == 0); if (getMethod == null) return null; @@ -98,7 +98,7 @@ public static class ValueAccessingStrategy private static Expression? FlattenMemberFn(Expression source, IMemberModel destinationMember, CompileArgument arg) { var strategy = arg.Settings.NameMatchingStrategy; - var destinationMemberName = destinationMember.GetMemberName(MemberSide.Destination, arg.Settings.GetMemberNames, strategy.DestinationMemberNameConverter); + var destinationMemberName = destinationMember.GetMemberName(MemberSide.Destination, arg.Settings.GetMemberNames, strategy.DestinationMemberNameConverter, arg); return GetDeepFlattening(source, destinationMemberName, arg); } @@ -111,7 +111,7 @@ public static class ValueAccessingStrategy if (!member.ShouldMapMember(arg, MemberSide.Source)) continue; - var sourceMemberName = member.GetMemberName(MemberSide.Source, arg.Settings.GetMemberNames, strategy.SourceMemberNameConverter); + var sourceMemberName = member.GetMemberName(MemberSide.Source, arg.Settings.GetMemberNames, strategy.SourceMemberNameConverter, arg); if (string.Equals(propertyName, sourceMemberName)) return member.GetExpression(source); @@ -132,14 +132,14 @@ public static class ValueAccessingStrategy internal static IEnumerable FindUnflatteningPairs(Expression source, IMemberModel destinationMember, CompileArgument arg) { var strategy = arg.Settings.NameMatchingStrategy; - var destinationMemberName = destinationMember.GetMemberName(MemberSide.Destination, arg.Settings.GetMemberNames, strategy.DestinationMemberNameConverter); + var destinationMemberName = destinationMember.GetMemberName(MemberSide.Destination, arg.Settings.GetMemberNames, strategy.DestinationMemberNameConverter, arg); var members = source.Type.GetFieldsAndProperties(true); foreach (var member in members) { if (!member.ShouldMapMember(arg, MemberSide.Source)) continue; - var sourceMemberName = member.GetMemberName(MemberSide.Source, arg.Settings.GetMemberNames, strategy.SourceMemberNameConverter); + var sourceMemberName = member.GetMemberName(MemberSide.Source, arg.Settings.GetMemberNames, strategy.SourceMemberNameConverter, arg); if (!sourceMemberName.StartsWith(destinationMemberName) || sourceMemberName == destinationMemberName) continue; foreach (var prop in GetDeepUnflattening(destinationMember, sourceMemberName.Substring(destinationMemberName.Length).TrimStart('_'), arg)) @@ -161,7 +161,7 @@ private static IEnumerable GetDeepUnflattening(IMemberModel destinationM { if (!member.ShouldMapMember(arg, MemberSide.Destination)) continue; - var destMemberName = member.GetMemberName(MemberSide.Destination, arg.Settings.GetMemberNames, strategy.DestinationMemberNameConverter); + var destMemberName = member.GetMemberName(MemberSide.Destination, arg.Settings.GetMemberNames, strategy.DestinationMemberNameConverter, arg); var propertyType = member.Type; if (string.Equals(propertyName, destMemberName)) { @@ -185,7 +185,7 @@ private static IEnumerable GetDeepUnflattening(IMemberModel destinationM return null; var strategy = arg.Settings.NameMatchingStrategy; - var destinationMemberName = destinationMember.GetMemberName(MemberSide.Destination, arg.Settings.GetMemberNames, strategy.DestinationMemberNameConverter); + var destinationMemberName = destinationMember.GetMemberName(MemberSide.Destination, arg.Settings.GetMemberNames, strategy.DestinationMemberNameConverter, arg); var key = Expression.Constant(destinationMemberName); var args = dictType.GetGenericArguments(); if (strategy.SourceMemberNameConverter != MapsterHelper.Identity) diff --git a/src/Mapster/TypeAdapter.cs b/src/Mapster/TypeAdapter.cs index 2e1db2f1..218045bf 100644 --- a/src/Mapster/TypeAdapter.cs +++ b/src/Mapster/TypeAdapter.cs @@ -1,5 +1,9 @@ -using System; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Reflection; +using Mapster.Models; namespace Mapster { @@ -21,7 +25,8 @@ public static ITypeAdapterBuilder BuildAdapter(this TSource so /// Destination type. /// Source object to adapt. /// Adapted destination type. - public static TDestination Adapt(this object? source) + [return: NotNullIfNotNull(nameof(source))] + public static TDestination? Adapt(this object? source) { return Adapt(source, TypeAdapterConfig.GlobalSettings); } @@ -33,14 +38,15 @@ public static TDestination Adapt(this object? source) /// Source object to adapt. /// Configuration /// Adapted destination type. - public static TDestination Adapt(this object? source, TypeAdapterConfig config) + [return: NotNullIfNotNull(nameof(source))] + public static TDestination? Adapt(this object? source, TypeAdapterConfig config) { // ReSharper disable once ConditionIsAlwaysTrueOrFalse if (source == null) - return default!; + return default; var type = source.GetType(); var fn = config.GetDynamicMapFunction(type); - return fn(source); + return fn(source)!; } /// @@ -93,10 +99,38 @@ public static TDestination Adapt(this TSource source, TDe /// Adapted destination type. public static TDestination Adapt(this TSource source, TDestination destination, TypeAdapterConfig config) { + var sourceType = source?.GetType(); + var destinationType = destination?.GetType(); + + if (sourceType == typeof(object)) // Infinity loop in ObjectAdapter if Runtime Type of source is Object + return destination; + + if (typeof(TSource) == typeof(object) || typeof(TDestination) == typeof(object)) + if(sourceType != null && destinationType != null) + return UpdateFuncFromPackedinObject(source, destination, config, sourceType, destinationType); + var fn = config.GetMapToTargetFunction(); return fn(source, destination); } + private static TDestination UpdateFuncFromPackedinObject(TSource source, TDestination destination, TypeAdapterConfig config, Type sourceType, Type destinationType) + { + dynamic del = config.GetMapToTargetFunction(sourceType, destinationType); + + + if (sourceType.GetTypeInfo().IsVisible && destinationType.GetTypeInfo().IsVisible) + { + dynamic objfn = del; + return objfn((dynamic)source, (dynamic)destination); + } + else + { + //NOTE: if type is non-public, we cannot use dynamic + //DynamicInvoke is slow, but works with non-public + return (TDestination)del.DynamicInvoke(source, destination); + } + } + /// /// Adapt the source object to the destination type. /// @@ -106,6 +140,12 @@ public static TDestination Adapt(this TSource source, TDe /// Adapted destination type. public static object? Adapt(this object source, Type sourceType, Type destinationType) { + if (source != null && + sourceType.IsOpenGenericType() && destinationType.IsOpenGenericType()) + { + var arg = source.GetType().GetGenericArguments(); + return Adapt(source, sourceType.MakeGenericType(arg), destinationType.MakeGenericType(arg), TypeAdapterConfig.GlobalSettings); + } return Adapt(source, sourceType, destinationType, TypeAdapterConfig.GlobalSettings); } @@ -170,6 +210,96 @@ public static TDestination Adapt(this TSource source, TDe return del.DynamicInvoke(source, destination); } } + + /// + /// Validate properties and Adapt the source object to the destination type. + /// + /// Source type. + /// Destination type. + /// Source object to adapt. + /// Adapted destination type. + public static TDestination ValidateAndAdapt(this TSource source) + { + var sourceType = typeof(TSource); + var selectorType = typeof(TDestination); + + var sourceProperties = new HashSet(sourceType.GetProperties().Select(p => p.Name)); + var selectorProperties = new HashSet(selectorType.GetProperties().Select(p=> p.Name)); + + foreach (var selectorProperty in selectorProperties) + { + if (sourceProperties.Contains(selectorProperty)) continue; + throw new Exception($"Property {selectorProperty} does not exist in {sourceType.Name} and is not configured in Mapster"); + } + return source.Adapt(); + } + + /// + /// Validate properties with configuration and Adapt the source object to the destination type. + /// + /// Source type. + /// Destination type. + /// Source object to adapt. + /// Configuration + /// Adapted destination type. + public static TDestination ValidateAndAdapt(this TSource source, TypeAdapterConfig config) + { + var sourceType = typeof(TSource); + var selectorType = typeof(TDestination); + + var sourceProperties = new HashSet(sourceType.GetProperties().Select(p => p.Name)); + var selectorProperties = new HashSet(selectorType.GetProperties().Select(p=> p.Name)); + + // Get the rule map for the current types + var ruleMap = config.RuleMap; + var typeTuple = new TypeTuple(sourceType, selectorType); + ruleMap.TryGetValue(typeTuple, out var rule); + + foreach (var selectorProperty in selectorProperties) + { + if (sourceProperties.Contains(selectorProperty)) continue; + // Check whether the adapter config has a config for the property + if (rule != null && rule.Settings.Resolvers.Any(r => r.DestinationMemberName.Equals(selectorProperty))) continue; + throw new Exception($"Property {selectorProperty} does not exist in {sourceType.Name} and is not configured in Mapster"); + } + return source.Adapt(config); + } + + + /// + /// Adapt the source object to a destination type using a temporary configuration. + /// A new TypeAdapterConfig is created for this call, ensuring GlobalSettings remain unchanged. + /// Safe for init-only properties and record types. + /// + /// Destination type. + /// Source object to adapt. + /// Action to customize the temporary config. + /// Adapted destination object of type TDestination. + public static TDestination Adapt(this object? source, Action configAction) + { + var config = TypeAdapterConfig.GlobalSettings.Clone(); + configAction(config); + return source.Adapt(config); + } + + /// + /// Adapt the source object from TSource to TDestination using a dedicated TypeAdapterSetter. + /// A temporary TypeAdapterConfig is created and configured via the setter. + /// Safe for init-only properties and record types, without modifying GlobalSettings. + /// + /// Source type. + /// Destination type. + /// Source object to adapt. + /// Action to customize the TypeAdapterSetter. + /// Adapted destination object of type TDestination. + public static TDestination Adapt(this object? source, Action> configAction) + { + var config = TypeAdapterConfig.GlobalSettings.Clone(); + var setter = config.ForType(); + configAction(setter); + setter.Settings.Resolvers.Reverse(); + return source.Adapt(config); + } } [System.Diagnostics.CodeAnalysis.SuppressMessage("Minor Code Smell", "S1104:Fields should not have public accessibility", Justification = "")] diff --git a/src/Mapster/TypeAdapterConfig.cs b/src/Mapster/TypeAdapterConfig.cs index f5ebbb3a..1e6abade 100644 --- a/src/Mapster/TypeAdapterConfig.cs +++ b/src/Mapster/TypeAdapterConfig.cs @@ -23,6 +23,7 @@ private static List CreateRuleTemplate() new PrimitiveAdapter().CreateRule(), //-200 new ClassAdapter().CreateRule(), //-150 new RecordTypeAdapter().CreateRule(), //-149 + new ReadOnlyInterfaceAdapter().CreateRule(), // -148 new CollectionAdapter().CreateRule(), //-125 new DictionaryAdapter().CreateRule(), //-124 new ArrayAdapter().CreateRule(), //-123 @@ -81,6 +82,7 @@ private static List CreateRuleTemplate() public bool RequireDestinationMemberSource { get; set; } public bool RequireExplicitMapping { get; set; } + public bool RequireExplicitMappingPrimitive { get; set; } public bool AllowImplicitDestinationInheritance { get; set; } public bool AllowImplicitSourceInheritance { get; set; } = true; public bool SelfContainedCodeGeneration { get; set; } @@ -227,6 +229,10 @@ private TypeAdapterSettings GetSettings(TypeTuple key) Rules.LockAdd(r); return r; }); + + rule.Settings.SourceType = key.Source; + rule.Settings.DestinationType = key.Destination; + return rule.Settings; } @@ -494,7 +500,7 @@ private LambdaExpression CreateMapInvokeExpression(Type sourceType, Type destina internal Expression CreateMapInvokeExpressionBody(Type sourceType, Type destinationType, Expression p) { - if (RequireExplicitMapping) + if (RequireExplicitMapping || RequireExplicitMappingPrimitive) { var key = new TypeTuple(sourceType, destinationType); _mapDict[key] = Compiler(CreateMapExpression(key, MapType.Map)); @@ -517,7 +523,7 @@ internal Expression CreateMapInvokeExpressionBody(Type sourceType, Type destinat internal Expression CreateMapToTargetInvokeExpressionBody(Type sourceType, Type destinationType, Expression p1, Expression p2) { - if (RequireExplicitMapping) + if (RequireExplicitMapping || RequireExplicitMappingPrimitive) { var key = new TypeTuple(sourceType, destinationType); _mapToTargetDict[key] = Compiler(CreateMapExpression(key, MapType.MapToTarget)); diff --git a/src/Mapster/TypeAdapterSetter.cs b/src/Mapster/TypeAdapterSetter.cs index 9215f375..e08ef312 100644 --- a/src/Mapster/TypeAdapterSetter.cs +++ b/src/Mapster/TypeAdapterSetter.cs @@ -1,13 +1,14 @@ -using System; +using Mapster.Adapters; +using Mapster.Models; +using Mapster.Utils; +using System; using System.Linq; using System.Linq.Expressions; using System.Reflection; -using Mapster.Adapters; -using Mapster.Models; -using Mapster.Utils; namespace Mapster { + [AdaptWith(AdaptDirectives.DestinationAsRecord)] public class TypeAdapterSetter { protected const string SourceParameterName = "source"; @@ -268,10 +269,29 @@ public static TSetter UseDestinationValue(this TSetter setter, Func(this TSetter setter, Type sourceType, Type destType) where TSetter : TypeAdapterSetter + public static TSetter Include(this TSetter setter, Type sourceType, Type destType) where TSetter : TypeAdapterSetter { setter.CheckCompiled(); + Type baseSourceType = setter.Settings.SourceType ?? typeof(void); + Type baseDestinationType = setter.Settings.DestinationType ?? typeof(void); + + if (baseSourceType.IsOpenGenericType() && baseDestinationType.IsOpenGenericType()) + { + if (!sourceType.IsAssignableToGenericType(baseSourceType)) + throw new InvalidCastException("In order to use inherits, TSource must be inherited from TBaseSource."); + if (!destType.IsAssignableToGenericType(baseDestinationType)) + throw new InvalidCastException("In order to use inherits, TDestination must be inherited from TBaseDestination."); + } + else + { + if (!baseSourceType.GetTypeInfo().IsAssignableFrom(sourceType.GetTypeInfo())) + throw new InvalidCastException("In order to use inherits, TSource must be inherited from TBaseSource."); + + if (!baseDestinationType.GetTypeInfo().IsAssignableFrom(destType.GetTypeInfo())) + throw new InvalidCastException("In order to use inherits, TDestination must be inherited from TBaseDestination."); + } + setter.Config.Rules.LockAdd(new TypeAdapterRule { Priority = arg => @@ -285,6 +305,36 @@ internal static TSetter Include(this TSetter setter, Type sourceType, T return setter; } + public static TSetter Inherits(this TSetter setter, Type baseSourceType, Type baseDestinationType) where TSetter : TypeAdapterSetter + { + setter.CheckCompiled(); + + Type derivedSourceType = setter.Settings.SourceType ?? typeof(void); + Type derivedDestinationType = setter.Settings.DestinationType ?? typeof(void); + + if(baseSourceType.IsOpenGenericType() && baseDestinationType.IsOpenGenericType()) + { + if (!derivedSourceType.IsAssignableToGenericType(baseSourceType)) + throw new InvalidCastException("In order to use inherits, TSource must be inherited from TBaseSource."); + if (!derivedDestinationType.IsAssignableToGenericType(baseDestinationType)) + throw new InvalidCastException("In order to use inherits, TDestination must be inherited from TBaseDestination."); + } + else + { + if (!baseSourceType.GetTypeInfo().IsAssignableFrom(derivedSourceType.GetTypeInfo())) + throw new InvalidCastException("In order to use inherits, TSource must be inherited from TBaseSource."); + + if (!baseDestinationType.GetTypeInfo().IsAssignableFrom(derivedDestinationType.GetTypeInfo())) + throw new InvalidCastException("In order to use inherits, TDestination must be inherited from TBaseDestination."); + } + + if (setter.Config.RuleMap.TryGetValue(new TypeTuple(baseSourceType, baseDestinationType), out var rule)) + { + setter.Settings.Apply(rule.Settings); + } + return setter; + } + public static TSetter ApplyAdaptAttribute(this TSetter setter, BaseAdaptAttribute attr) where TSetter : TypeAdapterSetter { if (attr.IgnoreAttributes != null) @@ -634,6 +684,11 @@ public TypeAdapterSetter MapToTargetWith(Expression Inherits Fork(Action action) diff --git a/src/Mapster/TypeAdapterSettings.cs b/src/Mapster/TypeAdapterSettings.cs index 7af0f52b..15c666a2 100644 --- a/src/Mapster/TypeAdapterSettings.cs +++ b/src/Mapster/TypeAdapterSettings.cs @@ -1,4 +1,5 @@ -using Mapster.Models; +using Mapster.Enums; +using Mapster.Models; using System; using System.Collections.Generic; using System.Linq.Expressions; @@ -6,8 +7,19 @@ namespace Mapster { + [AdaptWith(AdaptDirectives.DestinationAsRecord)] public class TypeAdapterSettings : SettingStore { + public Type? SourceType + { + get => Get(nameof(SourceType)); + set => Set(nameof(SourceType), value); + } + public Type? DestinationType + { + get => Get(nameof(DestinationType)); + set => Set(nameof(DestinationType), value); + } public IgnoreDictionary Ignore { get => Get(nameof(Ignore), () => new IgnoreDictionary()); @@ -83,6 +95,34 @@ public object? GenerateMapper set => Set(nameof(GenerateMapper), value); } + /// + /// Not implemented + /// + public bool? MapToPrimitive + { + get => Get(nameof(MapToPrimitive)); + set => Set(nameof(MapToPrimitive), value); + } + + public bool? MapToTargetPrimitive + { + get => Get(nameof(MapToTargetPrimitive)); + set => Set(nameof(MapToTargetPrimitive), value); + } + + public ProjectToTypeAutoMapping ProjectToTypeMapConfig + { + get => GetEnum(nameof(ProjectToTypeMapConfig), ()=> default(ProjectToTypeAutoMapping)); + set => Set(nameof(ProjectToTypeMapConfig), value); + } + + public Dictionary ProjectToTypeResolvers + { + get => Get(nameof(ProjectToTypeResolvers), () => new Dictionary()); + set => Set(nameof(ProjectToTypeResolvers), value); + } + + public List> ShouldMapMember { get => Get(nameof(ShouldMapMember), () => new List>()); diff --git a/src/Mapster/Utils/ExpressionEx.cs b/src/Mapster/Utils/ExpressionEx.cs index 031b29d7..f8011b5b 100644 --- a/src/Mapster/Utils/ExpressionEx.cs +++ b/src/Mapster/Utils/ExpressionEx.cs @@ -2,7 +2,9 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Linq.Expressions; using System.Reflection; namespace Mapster.Utils @@ -19,32 +21,112 @@ public static Expression Assign(Expression left, Expression right) public static Expression PropertyOrFieldPath(Expression expr, string path) { - var props = path.Split('.'); - return props.Aggregate(expr, PropertyOrField); + Expression current = expr; + string[] props = path.Split('.'); + + for (int i = 0; i < props.Length; i++) + { + if (IsDictionaryKey(current, props[i], out Expression? next)) + { + current = next; + continue; + } + + if (IsPropertyOrField(current, props[i], out next)) + { + current = next; + continue; + } + + // For dynamically built types, it is possible to have periods in the property name. + // Rejoin an incrementing number of parts with periods to try and find a property match. + if (IsPropertyOrFieldPathWithPeriods(current, props[i..], out next, out int combinationLength)) + { + current = next; + i += combinationLength - 1; + continue; + } + + throw new ArgumentException($"'{props[i]}' is not a member of type '{current.Type.FullName}'", nameof(path)); + } + + return current; } - private static Expression PropertyOrField(Expression expr, string prop) + private static bool IsPropertyOrFieldPathWithPeriods(Expression expr, string[] path, [NotNullWhen(true)] out Expression? propExpr, out int combinationLength) + { + if (path.Length < 2) + { + propExpr = null; + combinationLength = 0; + return false; + } + + for (int count = 2; count <= path.Length; count++) + { + string prop = string.Join('.', path[..count]); + if (IsPropertyOrField(expr, prop, out propExpr)) + { + combinationLength = count; + return true; + } + } + + propExpr = null; + combinationLength = 0; + return false; + } + + private static bool IsDictionaryKey(Expression expr, string prop, [NotNullWhen(true)] out Expression? propExpr) { var type = expr.Type; var dictType = type.GetDictionaryType(); - if (dictType?.GetGenericArguments()[0] == typeof(string)) + + if (dictType?.GetGenericArguments()[0] != typeof(string)) { + propExpr = null; + return false; + } + var method = typeof(MapsterHelper).GetMethods() .First(m => m.Name == nameof(MapsterHelper.GetValueOrDefault) && m.GetParameters()[0].ParameterType.Name == dictType.Name) .MakeGenericMethod(dictType.GetGenericArguments()); - return Expression.Call(method, expr.To(type), Expression.Constant(prop)); + propExpr = Expression.Call(method, expr.To(type), Expression.Constant(prop)); + return true; } + private static bool IsPropertyOrField(Expression expr, string prop, [NotNullWhen(true)] out Expression? propExpr) + { + Type type = expr.Type; + if (type.GetTypeInfo().IsInterface) { var allTypes = type.GetAllInterfaces(); var flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; var interfaceType = allTypes.FirstOrDefault(it => it.GetProperty(prop, flags) != null || it.GetField(prop, flags) != null); if (interfaceType != null) + { expr = Expression.Convert(expr, interfaceType); + type = expr.Type; } - return Expression.PropertyOrField(expr, prop); + } + + MemberInfo? propertyOrField = type + .GetMember( + prop, + MemberTypes.Field | MemberTypes.Property, + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.FlattenHierarchy) + .FirstOrDefault(); + + propExpr = propertyOrField?.MemberType switch + { + MemberTypes.Property => Expression.Property(expr, (PropertyInfo)propertyOrField), + MemberTypes.Field => Expression.Field(expr, (FieldInfo)propertyOrField), + _ => null + }; + + return propExpr != null; } private static bool IsReferenceAssignableFrom(this Type destType, Type srcType) @@ -301,6 +383,30 @@ public static Expression NotNullReturn(this Expression exp, Expression value) value); } + /// + /// Unpack Enum Nullable TSource value + /// + /// + /// + public static Expression NullableEnumExtractor(this Expression param) + { + var _SourceType = param.Type; + + if (_SourceType.IsNullable()) + { + var _genericType = param.Type.GetGenericArguments()[0]; + + if (_genericType.IsEnum) + { + var ExtractionExpression = Expression.Convert(param, typeof(object)); + return ExtractionExpression; + } + + return param; + } + + return param; + } public static Expression ApplyNullPropagation(this Expression getter) { var current = getter; diff --git a/src/Mapster/Utils/MemberExpressionExtractor.cs b/src/Mapster/Utils/MemberExpressionExtractor.cs new file mode 100644 index 00000000..f333ad79 --- /dev/null +++ b/src/Mapster/Utils/MemberExpressionExtractor.cs @@ -0,0 +1,152 @@ +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Reflection; + +public static class MemberExpressionExtractor +{ + public static IEnumerable GetAllMemberExpressionsMemberInfo(this Expression expression) + { + var result = new List(); + CollectMemberInfos(expression, result); + return result; + } + + private static void CollectMemberInfos(Expression expression, ICollection results) + { + if (expression == null) return; + + if (expression is MemberExpression memberExpression) + { + results.Add(memberExpression.Member); + } + else if (expression is BinaryExpression binaryExpression && binaryExpression.NodeType == ExpressionType.Assign) + { + ProcessBinaryAssign(binaryExpression, results); + } + + foreach (var subExpression in expression.GetSubExpressions()) + { + CollectMemberInfos(subExpression, results); + } + } + + private static void ProcessBinaryAssign(BinaryExpression assignExpression, ICollection results) + { + if (assignExpression == null) return; + + if (assignExpression.Left is MemberExpression leftMember) + { + results.Add(leftMember.Member); + } + + CollectMemberInfos(assignExpression.Right, results); + } + + private static IEnumerable GetSubExpressions(this Expression expression) + { + if (expression == null) yield break; + + switch (expression.NodeType) + { + case ExpressionType.MemberAccess: + yield return ((MemberExpression)expression).Expression; + break; + + case ExpressionType.Call: + foreach (var arg in ((MethodCallExpression)expression).Arguments) + yield return arg; + yield return ((MethodCallExpression)expression).Object; + break; + + case ExpressionType.Lambda: + yield return ((LambdaExpression)expression).Body; + break; + + case ExpressionType.Block: + foreach (var blockExpr in ((BlockExpression)expression).Expressions) + yield return blockExpr; + break; + + case ExpressionType.Conditional: + yield return ((ConditionalExpression)expression).Test; + yield return ((ConditionalExpression)expression).IfTrue; + yield return ((ConditionalExpression)expression).IfFalse; + break; + + case ExpressionType.NewArrayInit or ExpressionType.NewArrayBounds: + foreach (var arrayItem in ((NewArrayExpression)expression).Expressions) + yield return arrayItem; + break; + + case ExpressionType.New: + foreach (var arg in ((NewExpression)expression).Arguments) + yield return arg; + break; + + case ExpressionType.Invoke: + yield return ((InvocationExpression)expression).Expression; + break; + + case ExpressionType.Assign: + yield return ((BinaryExpression)expression).Left; + yield return ((BinaryExpression)expression).Right; + break; + + case ExpressionType.Add: + case ExpressionType.Subtract: + case ExpressionType.Multiply: + case ExpressionType.Divide: + case ExpressionType.Equal: + case ExpressionType.NotEqual: + case ExpressionType.GreaterThan: + case ExpressionType.LessThan: + case ExpressionType.AndAlso: + case ExpressionType.OrElse: + yield return ((BinaryExpression)expression).Left; + yield return ((BinaryExpression)expression).Right; + break; + + case ExpressionType.Not: + case ExpressionType.Negate: + case ExpressionType.Convert: + case ExpressionType.Increment: + case ExpressionType.Decrement: + case ExpressionType.Quote: + case ExpressionType.TypeAs: + case ExpressionType.OnesComplement: + yield return ((UnaryExpression)expression).Operand; + break; + + case ExpressionType.TypeIs: + yield return ((TypeBinaryExpression)expression).Expression; + break; + + case ExpressionType.Coalesce: + yield return ((BinaryExpression)expression).Left; + yield return ((BinaryExpression)expression).Conversion; + yield return ((BinaryExpression)expression).Right; + break; + + case ExpressionType.Index: + yield return ((IndexExpression)expression).Object; + foreach (var indexArg in ((IndexExpression)expression).Arguments) + yield return indexArg; + break; + + case ExpressionType.Loop: + yield return ((LoopExpression)expression).Body; + break; + + case ExpressionType.Try: + yield return ((TryExpression)expression).Body; + foreach (var handler in ((TryExpression)expression).Handlers) + yield return handler.Body; + yield return ((TryExpression)expression).Finally; + break; + + + default: + break; + } + } +} \ No newline at end of file diff --git a/src/Mapster/Utils/NullableExpressionVisitor.cs b/src/Mapster/Utils/NullableExpressionVisitor.cs index ed642996..70d25162 100644 --- a/src/Mapster/Utils/NullableExpressionVisitor.cs +++ b/src/Mapster/Utils/NullableExpressionVisitor.cs @@ -127,7 +127,7 @@ protected override Expression VisitConstant(ConstantExpression node) protected override Expression VisitMember(MemberExpression node) { - CanBeNull = node.Member.GetCustomAttributesData().All(IsNullable); + CanBeNull = node.Type.IsClass || node.Member.GetCustomAttributesData().All(IsNullable); return node; } } diff --git a/src/Mapster/Utils/ReflectionUtils.cs b/src/Mapster/Utils/ReflectionUtils.cs index 5959daa5..9abe09f8 100644 --- a/src/Mapster/Utils/ReflectionUtils.cs +++ b/src/Mapster/Utils/ReflectionUtils.cs @@ -1,11 +1,11 @@ -using System; +using Mapster.Models; +using Mapster.Utils; +using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Reflection; -using Mapster.Models; -using Mapster.Utils; // ReSharper disable once CheckNamespace namespace Mapster @@ -36,6 +36,11 @@ public static Type GetTypeInfo(this Type type) } #endif + public static bool IsMapsterPrimitive(this Type type) + { + return _primitiveTypes.TryGetValue(type, out var primitiveType) || type == typeof(string) || type.IsEnum; + } + public static bool IsNullable(this Type type) { return type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); @@ -56,6 +61,12 @@ public static bool IsPoco(this Type type) if (type.IsConvertible()) return false; + if (type == typeof(Type) || type.BaseType == typeof(MulticastDelegate)) + return false; + + if (type.IsClass && type.GetProperties().Count() != 0) + return true; + return type.GetFieldsAndProperties().Any(it => (it.SetterModifier & (AccessModifier.Public | AccessModifier.NonPublic)) != 0); } @@ -64,23 +75,44 @@ public static IEnumerable GetFieldsAndProperties(this Type type, var bindingFlags = BindingFlags.Instance | BindingFlags.Public; if (includeNonPublic) bindingFlags |= BindingFlags.NonPublic; - + + var currentTypeMembers = type.FindMembers(MemberTypes.Property | MemberTypes.Field, + BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, + (x, y) => true, type.FullName); + if (type.GetTypeInfo().IsInterface) { var allInterfaces = GetAllInterfaces(type); - return allInterfaces.SelectMany(GetPropertiesFunc); + return allInterfaces.SelectMany(x => GetPropertiesFunc(x, currentTypeMembers)); } - return GetPropertiesFunc(type).Concat(GetFieldsFunc(type)); + return GetPropertiesFunc(type, currentTypeMembers).Concat(GetFieldsFunc(type, currentTypeMembers)); - IEnumerable GetPropertiesFunc(Type t) => t.GetProperties(bindingFlags) - .Where(x => x.GetIndexParameters().Length == 0) + IEnumerable GetPropertiesFunc(Type t, MemberInfo[] currentTypeMembers) => t.GetProperties(bindingFlags) + .Where(x => x.GetIndexParameters().Length == 0).DropHiddenMembers(currentTypeMembers) .Select(CreateModel); - IEnumerable GetFieldsFunc(Type t) => t.GetFields(bindingFlags) + IEnumerable GetFieldsFunc(Type t, MemberInfo[] overlapMembers) => + t.GetFields(bindingFlags).DropHiddenMembers(overlapMembers) .Select(CreateModel); } + public static IEnumerable DropHiddenMembers(this IEnumerable allMembers, ICollection currentTypeMembers) where T : MemberInfo + { + var compareMemberNames = allMembers.IntersectBy(currentTypeMembers.Select(x => x.Name), x => x.Name).Select(x => x.Name); + + foreach (var member in allMembers) + { + if (compareMemberNames.Contains(member.Name)) + { + if (currentTypeMembers.First(x => x.Name == member.Name).MetadataToken == member.MetadataToken) + yield return member; + } + else + yield return member; + } + } + // GetProperties(), GetFields(), GetMethods() do not return properties/methods from parent interfaces, // so we need to process every one of them separately. public static IEnumerable GetAllInterfaces(this Type interfaceType) @@ -168,29 +200,20 @@ public static bool IsRecordType(this Type type) if (type.IsConvertible()) return false; - var props = type.GetFieldsAndProperties().ToList(); + if(RecordTypeIdentityHelper.IsDirectiveTagret(type)) // added Support work from custom Attribute + return true; + + #region SupportingСurrentBehavior for Config Clone and Fork - //interface with readonly props - if (type.GetTypeInfo().IsInterface && - props.Any(p => p.SetterModifier != AccessModifier.Public)) + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(KeyValuePair<,>)) return true; - //1 constructor - var ctors = type.GetConstructors().ToList(); - if (ctors.Count != 1) - return false; + #endregion SupportingСurrentBehavior for Config Clone and Fork - //ctor must not empty - var ctorParams = ctors[0].GetParameters(); - if (ctorParams.Length == 0) - return false; + if(RecordTypeIdentityHelper.IsRecordType(type)) + return true; - //all parameters should match getter - return props.All(prop => - { - var name = prop.Name.ToPascalCase(); - return ctorParams.Any(p => p.ParameterType == prop.Type && p.Name?.ToPascalCase() == name); - }); + return false; } public static bool IsConvertible(this Type type) @@ -315,11 +338,14 @@ public static bool UseDestinationValue(this IMemberModel member, CompileArgument return predicates.Any(predicate => predicate(member)); } - public static string GetMemberName(this IMemberModel member, MemberSide side, List> getMemberNames, Func nameConverter) + public static string GetMemberName(this IMemberModel member, MemberSide side, List> getMemberNames, Func nameConverter, CompileArgument arg) { var memberName = getMemberNames.Select(func => func(member, side)) - .FirstOrDefault(name => name != null) - ?? member.Name; + .FirstOrDefault(name => name != null); + if (memberName == null && arg.ConstructorMapping == true) + memberName = member.Name.ToPascalCase(); + if (memberName == null) + memberName = member.Name; return nameConverter(memberName); } @@ -384,5 +410,40 @@ public static bool IsInitOnly(this PropertyInfo propertyInfo) var isExternalInitType = typeof(System.Runtime.CompilerServices.IsExternalInit); return setMethod.ReturnParameter.GetRequiredCustomModifiers().Contains(isExternalInitType); } + + public static bool IsAssignableToGenericType(this Type derivedType, Type genericType) + { + + if (derivedType.IsGenericType && derivedType.BaseType.GUID == genericType.GUID) + return true; + + Type baseType = derivedType.BaseType; + if (baseType == null) return false; + + return IsAssignableToGenericType(baseType, genericType); + } + public static bool IsOpenGenericType(this Type type) + { + if(type.IsGenericType) + return type.GetGenericArguments().All(x=>x.GUID == Guid.Empty); + + return false; + } + + public static bool IsAbstractOrNotPublicCtor(this Type type) + { + if(type.IsAbstract) + return true; + + if (type.GetConstructors().All(x => !x.IsPublic)) + return true; + + return false; + } + + public static bool isDefaultCtor(this Type type) + { + return type.GetConstructor(new Type[] { }) is not null ? true : false; + } } } \ No newline at end of file diff --git a/src/Sample.AspNetCore/Sample.AspNetCore.csproj b/src/Sample.AspNetCore/Sample.AspNetCore.csproj index c5761156..a681b6fd 100644 --- a/src/Sample.AspNetCore/Sample.AspNetCore.csproj +++ b/src/Sample.AspNetCore/Sample.AspNetCore.csproj @@ -1,15 +1,15 @@  - net7.0;net6.0 + net10.0;net9.0;net8.0; - - - - + + + + diff --git a/src/Sample.CodeGen/Sample.CodeGen.csproj b/src/Sample.CodeGen/Sample.CodeGen.csproj index 27485d31..78092145 100644 --- a/src/Sample.CodeGen/Sample.CodeGen.csproj +++ b/src/Sample.CodeGen/Sample.CodeGen.csproj @@ -1,16 +1,16 @@  - net7.0;net6.0 + net10.0;net9.0;net8.0; enable - - - - - + + + + + diff --git a/src/TemplateTest/TemplateTest.csproj b/src/TemplateTest/TemplateTest.csproj index 2f6524f3..ae4dff09 100644 --- a/src/TemplateTest/TemplateTest.csproj +++ b/src/TemplateTest/TemplateTest.csproj @@ -1,17 +1,20 @@ - + - net7.0;net6.0 + net10.0;net9.0;net8.0; enable - + true false - - - - + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive +