diff --git a/.readthedocs.yml b/.readthedocs.yml index af59f269aa..3a7e5af642 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1 +1,13 @@ -requirements_file: docs/requirements.txt +version: "2" + +build: + os: "ubuntu-22.04" + tools: + python: "3.10" + +python: + install: + - requirements: docs/requirements.txt + +sphinx: + configuration: docs/source/conf.py \ No newline at end of file diff --git a/AutoMapper.sln b/AutoMapper.sln index 92bcf049c6..057f765ad4 100644 --- a/AutoMapper.sln +++ b/AutoMapper.sln @@ -26,6 +26,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoMapper.UnitTests", "src EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoMapper.IntegrationTests", "src\IntegrationTests\AutoMapper.IntegrationTests.csproj", "{24B47F4C-0035-4F29-AAD9-4C47E1AAD98E}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoMapper.DI.Tests", "src\AutoMapper.DI.Tests\AutoMapper.DI.Tests.csproj", "{BEBD620A-8BAA-463F-BE0F-8319AD3C1644}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestApp", "src\TestApp\TestApp.csproj", "{35CED3AE-B825-4703-992D-A58B5BE646DC}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -96,6 +100,38 @@ Global {24B47F4C-0035-4F29-AAD9-4C47E1AAD98E}.Release|x64.Build.0 = Release|Any CPU {24B47F4C-0035-4F29-AAD9-4C47E1AAD98E}.Release|x86.ActiveCfg = Release|Any CPU {24B47F4C-0035-4F29-AAD9-4C47E1AAD98E}.Release|x86.Build.0 = Release|Any CPU + {BEBD620A-8BAA-463F-BE0F-8319AD3C1644}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BEBD620A-8BAA-463F-BE0F-8319AD3C1644}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BEBD620A-8BAA-463F-BE0F-8319AD3C1644}.Debug|ARM.ActiveCfg = Debug|Any CPU + {BEBD620A-8BAA-463F-BE0F-8319AD3C1644}.Debug|ARM.Build.0 = Debug|Any CPU + {BEBD620A-8BAA-463F-BE0F-8319AD3C1644}.Debug|x64.ActiveCfg = Debug|Any CPU + {BEBD620A-8BAA-463F-BE0F-8319AD3C1644}.Debug|x64.Build.0 = Debug|Any CPU + {BEBD620A-8BAA-463F-BE0F-8319AD3C1644}.Debug|x86.ActiveCfg = Debug|Any CPU + {BEBD620A-8BAA-463F-BE0F-8319AD3C1644}.Debug|x86.Build.0 = Debug|Any CPU + {BEBD620A-8BAA-463F-BE0F-8319AD3C1644}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BEBD620A-8BAA-463F-BE0F-8319AD3C1644}.Release|Any CPU.Build.0 = Release|Any CPU + {BEBD620A-8BAA-463F-BE0F-8319AD3C1644}.Release|ARM.ActiveCfg = Release|Any CPU + {BEBD620A-8BAA-463F-BE0F-8319AD3C1644}.Release|ARM.Build.0 = Release|Any CPU + {BEBD620A-8BAA-463F-BE0F-8319AD3C1644}.Release|x64.ActiveCfg = Release|Any CPU + {BEBD620A-8BAA-463F-BE0F-8319AD3C1644}.Release|x64.Build.0 = Release|Any CPU + {BEBD620A-8BAA-463F-BE0F-8319AD3C1644}.Release|x86.ActiveCfg = Release|Any CPU + {BEBD620A-8BAA-463F-BE0F-8319AD3C1644}.Release|x86.Build.0 = Release|Any CPU + {35CED3AE-B825-4703-992D-A58B5BE646DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {35CED3AE-B825-4703-992D-A58B5BE646DC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {35CED3AE-B825-4703-992D-A58B5BE646DC}.Debug|ARM.ActiveCfg = Debug|Any CPU + {35CED3AE-B825-4703-992D-A58B5BE646DC}.Debug|ARM.Build.0 = Debug|Any CPU + {35CED3AE-B825-4703-992D-A58B5BE646DC}.Debug|x64.ActiveCfg = Debug|Any CPU + {35CED3AE-B825-4703-992D-A58B5BE646DC}.Debug|x64.Build.0 = Debug|Any CPU + {35CED3AE-B825-4703-992D-A58B5BE646DC}.Debug|x86.ActiveCfg = Debug|Any CPU + {35CED3AE-B825-4703-992D-A58B5BE646DC}.Debug|x86.Build.0 = Debug|Any CPU + {35CED3AE-B825-4703-992D-A58B5BE646DC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {35CED3AE-B825-4703-992D-A58B5BE646DC}.Release|Any CPU.Build.0 = Release|Any CPU + {35CED3AE-B825-4703-992D-A58B5BE646DC}.Release|ARM.ActiveCfg = Release|Any CPU + {35CED3AE-B825-4703-992D-A58B5BE646DC}.Release|ARM.Build.0 = Release|Any CPU + {35CED3AE-B825-4703-992D-A58B5BE646DC}.Release|x64.ActiveCfg = Release|Any CPU + {35CED3AE-B825-4703-992D-A58B5BE646DC}.Release|x64.Build.0 = Release|Any CPU + {35CED3AE-B825-4703-992D-A58B5BE646DC}.Release|x86.ActiveCfg = Release|Any CPU + {35CED3AE-B825-4703-992D-A58B5BE646DC}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Build.ps1 b/Build.ps1 index f3c142e64f..f78e52ab9b 100644 --- a/Build.ps1 +++ b/Build.ps1 @@ -26,6 +26,6 @@ $artifacts = ".\artifacts" if(Test-Path $artifacts) { Remove-Item $artifacts -Force -Recurse } -exec { & dotnet test -c Release --results-directory $artifacts -l trx --verbosity=normal } +exec { & dotnet test -c Release --results-directory $artifacts -l trx } exec { & dotnet pack .\src\AutoMapper\AutoMapper.csproj -c Release -o $artifacts --no-build } diff --git a/README.md b/README.md index 9b288ebad0..a14c843b7c 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ [![CI](https://github.com/automapper/automapper/workflows/CI/badge.svg)](https://github.com/AutoMapper/AutoMapper/actions?query=workflow%3ACI) [![NuGet](http://img.shields.io/nuget/vpre/AutoMapper.svg?label=NuGet)](https://www.nuget.org/packages/AutoMapper/) [![MyGet (dev)](https://img.shields.io/myget/automapperdev/vpre/AutoMapper.svg?label=MyGet)](https://myget.org/feed/automapperdev/package/nuget/AutoMapper) +[![Documentation Status](https://readthedocs.org/projects/automapper/badge/?version=stable)](https://docs.automapper.org/en/stable/?badge=stable) + ### What is AutoMapper? @@ -10,7 +12,6 @@ AutoMapper is a simple little library built to solve a deceptively complex probl This is the main repository for AutoMapper, but there's more: -* [Microsoft DI Extensions](https://github.com/AutoMapper/AutoMapper.Extensions.Microsoft.DependencyInjection) * [Collection Extensions](https://github.com/AutoMapper/AutoMapper.Collection) * [Expression Mapping](https://github.com/AutoMapper/AutoMapper.Extensions.ExpressionMapping) * [EF6 Extensions](https://github.com/AutoMapper/AutoMapper.EF6) diff --git a/docs/Makefile b/docs/Makefile index 2f136bfb63..269cadcf83 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,12 +1,12 @@ # Minimal makefile for Sphinx documentation # -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = python -msphinx -SPHINXPROJ = AutoMapper -SOURCEDIR = . -BUILDDIR = _build +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build # Put it first so that "make" without argument is like "make help". help: diff --git a/docs/conf.py b/docs/conf.py deleted file mode 100644 index cc465859ff..0000000000 --- a/docs/conf.py +++ /dev/null @@ -1,180 +0,0 @@ -# -*- coding: utf-8 -*- -# -# AutoMapper documentation build configuration file, created by -# sphinx-quickstart on Thu Oct 05 09:44:33 2017. -# -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# -# import os -# import sys -# sys.path.insert(0, os.path.abspath('.')) - -from recommonmark.parser import CommonMarkParser - -# -- General configuration ------------------------------------------------ - -# If your documentation needs a minimal Sphinx version, state it here. -# -# needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = [] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -source_parsers = { - '.md': CommonMarkParser -} - -# The suffix(es) of source filenames. -# You can specify multiple suffix as a list of string: -# -# source_suffix = ['.rst', '.md'] -source_suffix = ['.rst', '.md'] - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = u'AutoMapper' -copyright = u'2017, Jimmy Bogard' -author = u'Jimmy Bogard' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = u'' -# The full version, including alpha/beta/rc tags. -release = u'' - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# -# This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. -language = None - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This patterns also effect to html_static_path and html_extra_path -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# If true, `todo` and `todoList` produce output, else they produce nothing. -todo_include_todos = False - - -# -- Options for HTML output ---------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# -html_theme = 'sphinx_rtd_theme' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -# -# html_theme_options = {} - -html_theme_options = { - 'logo_only': True, - 'display_version': False -} - -html_logo = 'img/logo.png' - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - -# Custom sidebar templates, must be a dictionary that maps document names -# to template names. -# -# This is required for the alabaster theme -# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars -html_sidebars = { - '**': [ - 'about.html', - 'navigation.html', - 'relations.html', # needs 'show_related': True theme option to display - 'searchbox.html', - 'donate.html', - ] -} - - -# -- Options for HTMLHelp output ------------------------------------------ - -# Output file base name for HTML help builder. -htmlhelp_basename = 'AutoMapperdoc' - - -# -- Options for LaTeX output --------------------------------------------- - -latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - # - # 'papersize': 'letterpaper', - - # The font size ('10pt', '11pt' or '12pt'). - # - # 'pointsize': '10pt', - - # Additional stuff for the LaTeX preamble. - # - # 'preamble': '', - - # Latex figure (float) alignment - # - # 'figure_align': 'htbp', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - (master_doc, 'AutoMapper.tex', u'AutoMapper Documentation', - u'Jimmy Bogard', 'manual'), -] - - -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'automapper', u'AutoMapper Documentation', - [author], 1) -] - - -# -- Options for Texinfo output ------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - (master_doc, 'AutoMapper', u'AutoMapper Documentation', - author, 'AutoMapper', 'One line description of project.', - 'Miscellaneous'), -] diff --git a/docs/make.bat b/docs/make.bat index 1568929e2d..53941892e6 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -5,32 +5,31 @@ pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=python -msphinx + set SPHINXBUILD=sphinx-build ) -set SOURCEDIR=. -set BUILDDIR=_build -set SPHINXPROJ=AutoMapper +set SOURCEDIR=source +set BUILDDIR=build if "%1" == "" goto help %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. - echo.The Sphinx module was not found. Make sure you have Sphinx installed, - echo.then set the SPHINXBUILD environment variable to point to the full - echo.path of the 'sphinx-build' executable. Alternatively you may add the - echo.Sphinx directory to PATH. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% goto end :help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% :end -popd +popd \ No newline at end of file diff --git a/docs/requirements.txt b/docs/requirements.txt index 4da41f9b71..30d45e6795 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1 +1,3 @@ -sphinx >= 1.6.0 +sphinx==7.1.2 +sphinx-rtd-theme==1.3.0rc1 +myst_parser==2.0.0 \ No newline at end of file diff --git a/docs/10.0-Upgrade-Guide.md b/docs/source/10.0-Upgrade-Guide.md similarity index 100% rename from docs/10.0-Upgrade-Guide.md rename to docs/source/10.0-Upgrade-Guide.md diff --git a/docs/11.0-Upgrade-Guide.md b/docs/source/11.0-Upgrade-Guide.md similarity index 100% rename from docs/11.0-Upgrade-Guide.md rename to docs/source/11.0-Upgrade-Guide.md diff --git a/docs/12.0-Upgrade-Guide.md b/docs/source/12.0-Upgrade-Guide.md similarity index 100% rename from docs/12.0-Upgrade-Guide.md rename to docs/source/12.0-Upgrade-Guide.md diff --git a/docs/source/13.0-Upgrade-Guide.md b/docs/source/13.0-Upgrade-Guide.md new file mode 100644 index 0000000000..6c13bd5439 --- /dev/null +++ b/docs/source/13.0-Upgrade-Guide.md @@ -0,0 +1,27 @@ +# 13.0 Upgrade Guide + +[Release notes](https://github.com/AutoMapper/AutoMapper/releases/tag/v13.0.0). + +## AutoMapper now targets .Net 6 + +## `AddAutoMapper` is part of the core package and the DI package is discontinued + +## `IMapper` has nullable annotations + +Besides the build-time impact, there is also a behaviour change. Non-generic `Map` overloads require now either a destination type or a non-null destination object. + +## `AllowAdditiveTypeMapCreation` was removed + +Be sure to call `CreateMap` once for a source type, destination type pair. If you want to reuse configuration, use mapping inheritance. + +## ProjectTo runtime polymorphic mapping with Include/IncludeBase + +We consider this an off the beaten path feature and we don't expose it through `CreateProjection`. You can use [an extension method](https://github.com/AutoMapper/AutoMapper/search?l=C%23&q=Advanced) or `CreateMap`. + +## `Context.State` similar to `Context.Items` + +The same pattern the framework uses to pass state to delegates. Note that `State` and `Items` are mutually exclusive per `Map` call. + +## Custom Equals/GetHashCode for source objects + +To avoid broken implementations, we no longer call those when checking for identical source objects, we hard code to checking object references. \ No newline at end of file diff --git a/docs/5.0-Upgrade-Guide.md b/docs/source/5.0-Upgrade-Guide.md similarity index 100% rename from docs/5.0-Upgrade-Guide.md rename to docs/source/5.0-Upgrade-Guide.md diff --git a/docs/8.0-Upgrade-Guide.md b/docs/source/8.0-Upgrade-Guide.md similarity index 100% rename from docs/8.0-Upgrade-Guide.md rename to docs/source/8.0-Upgrade-Guide.md diff --git a/docs/8.1.1-Upgrade-Guide.md b/docs/source/8.1.1-Upgrade-Guide.md similarity index 100% rename from docs/8.1.1-Upgrade-Guide.md rename to docs/source/8.1.1-Upgrade-Guide.md diff --git a/docs/9.0-Upgrade-Guide.md b/docs/source/9.0-Upgrade-Guide.md similarity index 100% rename from docs/9.0-Upgrade-Guide.md rename to docs/source/9.0-Upgrade-Guide.md diff --git a/docs/API-Changes.md b/docs/source/API-Changes.md similarity index 100% rename from docs/API-Changes.md rename to docs/source/API-Changes.md diff --git a/docs/Attribute-mapping.md b/docs/source/Attribute-mapping.md similarity index 100% rename from docs/Attribute-mapping.md rename to docs/source/Attribute-mapping.md diff --git a/docs/Before-and-after-map-actions.md b/docs/source/Before-and-after-map-actions.md similarity index 85% rename from docs/Before-and-after-map-actions.md rename to docs/source/Before-and-after-map-actions.md index 91822a656e..103691cfc8 100644 --- a/docs/Before-and-after-map-actions.md +++ b/docs/source/Before-and-after-map-actions.md @@ -42,10 +42,10 @@ var configuration = new MapperConfiguration(cfg => { }); ``` -### Asp.Net Core and `AutoMapper.Extensions.Microsoft.DependencyInjection` -If you are using Asp.Net Core and the `AutoMapper.Extensions.Microsoft.DependencyInjection` package, this is also a good way of using Dependency Injection. You can't inject dependencies into `Profile` classes, but you can do it in `IMappingAction` implementations. +### Dependency Injection +You can't inject dependencies into `Profile` classes, but you can do it in `IMappingAction` implementations. -The following example shows how to connect an `IMappingAction` accessing the current `HttpContext` to a `Profile` after map action, leveraging Dependency Injection: +The following example shows how to connect an `IMappingAction` accessing the current `HttpContext` to a `Profile` after map action, leveraging dependency injection: ``` csharp public class SetTraceIdentifierAction : IMappingAction @@ -84,6 +84,4 @@ public class Startup } //.. } -``` - -*See `AutoMapper.Extensions.Microsoft.DependencyInjection` for more info.* +``` \ No newline at end of file diff --git a/docs/Conditional-mapping.md b/docs/source/Conditional-mapping.md similarity index 100% rename from docs/Conditional-mapping.md rename to docs/source/Conditional-mapping.md diff --git a/docs/Configuration-validation.md b/docs/source/Configuration-validation.md similarity index 98% rename from docs/Configuration-validation.md rename to docs/source/Configuration-validation.md index 2f0efc7386..fb99659cce 100644 --- a/docs/Configuration-validation.md +++ b/docs/source/Configuration-validation.md @@ -50,7 +50,7 @@ var configuration = new MapperConfiguration(cfg => ); ``` -To skip validation altogether for this map, use `MemberList.None`. +To skip validation altogether for this map, use `MemberList.None`. That's the default for `ReverseMap`. ## Custom validations diff --git a/docs/Configuration.md b/docs/source/Configuration.md similarity index 100% rename from docs/Configuration.md rename to docs/source/Configuration.md diff --git a/docs/Construction.md b/docs/source/Construction.md similarity index 100% rename from docs/Construction.md rename to docs/source/Construction.md diff --git a/docs/Custom-type-converters.md b/docs/source/Custom-type-converters.md similarity index 100% rename from docs/Custom-type-converters.md rename to docs/source/Custom-type-converters.md diff --git a/docs/Custom-value-resolvers.md b/docs/source/Custom-value-resolvers.md similarity index 98% rename from docs/Custom-value-resolvers.md rename to docs/source/Custom-value-resolvers.md index 58a603d42d..b109b638d7 100644 --- a/docs/Custom-value-resolvers.md +++ b/docs/source/Custom-value-resolvers.md @@ -127,6 +127,7 @@ This is how to setup the mapping for this custom resolver cfg.CreateMap() .ForMember(dest => dest.Foo, opt => opt.MapFrom((src, dest, destMember, context) => context.Items["Foo"])); ``` +Starting with version 13.0, you can use `context.State` instead, in a similar way. Note that `State` and `Items` are mutually exclusive per `Map` call. ### ForPath diff --git a/docs/Dependency-injection.md b/docs/source/Dependency-injection.md similarity index 96% rename from docs/Dependency-injection.md rename to docs/source/Dependency-injection.md index 7b52e4ee44..cda75a2c8c 100644 --- a/docs/Dependency-injection.md +++ b/docs/source/Dependency-injection.md @@ -6,6 +6,8 @@ There is a [NuGet package](https://www.nuget.org/packages/AutoMapper.Extensions.Microsoft.DependencyInjection/) to be used with the default injection mechanism described [here](https://github.com/AutoMapper/AutoMapper.Extensions.Microsoft.DependencyInjection) and used in [this project](https://github.com/jbogard/ContosoUniversityCore/blob/master/src/ContosoUniversityCore/Startup.cs). +Starting with version 13.0, `AddAutoMapper` is part of the core package and the DI package is discontinued. + You define the configuration using [profiles](Configuration.html#profile-instances). And then you let AutoMapper know in what assemblies are those profiles defined by calling the `IServiceCollection` extension method `AddAutoMapper` at startup: ```c# services.AddAutoMapper(profileAssembly1, profileAssembly2 /*, ...*/); diff --git a/docs/Dynamic-and-ExpandoObject-Mapping.md b/docs/source/Dynamic-and-ExpandoObject-Mapping.md similarity index 100% rename from docs/Dynamic-and-ExpandoObject-Mapping.md rename to docs/source/Dynamic-and-ExpandoObject-Mapping.md diff --git a/docs/Enum-Mapping.md b/docs/source/Enum-Mapping.md similarity index 100% rename from docs/Enum-Mapping.md rename to docs/source/Enum-Mapping.md diff --git a/docs/Expression-Translation-(UseAsDataSource).md b/docs/source/Expression-Translation-(UseAsDataSource).md similarity index 100% rename from docs/Expression-Translation-(UseAsDataSource).md rename to docs/source/Expression-Translation-(UseAsDataSource).md diff --git a/docs/Flattening.md b/docs/source/Flattening.md similarity index 100% rename from docs/Flattening.md rename to docs/source/Flattening.md diff --git a/docs/Getting-started.md b/docs/source/Getting-started.md similarity index 100% rename from docs/Getting-started.md rename to docs/source/Getting-started.md diff --git a/docs/Lists-and-arrays.md b/docs/source/Lists-and-arrays.md similarity index 100% rename from docs/Lists-and-arrays.md rename to docs/source/Lists-and-arrays.md diff --git a/docs/Mapping-inheritance.md b/docs/source/Mapping-inheritance.md similarity index 100% rename from docs/Mapping-inheritance.md rename to docs/source/Mapping-inheritance.md diff --git a/docs/Nested-mappings.md b/docs/source/Nested-mappings.md similarity index 96% rename from docs/Nested-mappings.md rename to docs/source/Nested-mappings.md index 237d2cba44..54caa54ead 100644 --- a/docs/Nested-mappings.md +++ b/docs/source/Nested-mappings.md @@ -1,60 +1,60 @@ -# Nested Mappings - -As the mapping engine executes the mapping, it can use one of a variety of methods to resolve a destination member value. One of these methods is to use another type map, where the source member type and destination member type are also configured in the mapping configuration. This allows us to not only flatten our source types, but create complex destination types as well. For example, our source type might contain another complex type: - -```c# -public class OuterSource -{ - public int Value { get; set; } - public InnerSource Inner { get; set; } -} - -public class InnerSource -{ - public int OtherValue { get; set; } -} -``` - -We _could_ simply flatten the OuterSource.Inner.OtherValue to one InnerOtherValue property, but we might also want to create a corresponding complex type for the Inner property: - -```c# -public class OuterDest -{ - public int Value { get; set; } - public InnerDest Inner { get; set; } -} - -public class InnerDest -{ - public int OtherValue { get; set; } -} -``` - -In that case, we would need to configure the additional source/destination type mappings: - -```c# -var config = new MapperConfiguration(cfg => { - cfg.CreateMap(); - cfg.CreateMap(); -}); -config.AssertConfigurationIsValid(); - -var source = new OuterSource - { - Value = 5, - Inner = new InnerSource {OtherValue = 15} - }; -var mapper = config.CreateMapper(); -var dest = mapper.Map(source); - -dest.Value.ShouldEqual(5); -dest.Inner.ShouldNotBeNull(); -dest.Inner.OtherValue.ShouldEqual(15); -``` - -A few things to note here: - -* Order of configuring types does not matter -* Call to Map does not need to specify any inner type mappings, only the type map to use for the source value passed in - -With both flattening and nested mappings, we can create a variety of destination shapes to suit whatever our needs may be. +# Nested Mappings + +As the mapping engine executes the mapping, it can use one of a variety of methods to resolve a destination member value. One of these methods is to use another type map, where the source member type and destination member type are also configured in the mapping configuration. This allows us to not only flatten our source types, but create complex destination types as well. For example, our source type might contain another complex type: + +```c# +public class OuterSource +{ + public int Value { get; set; } + public InnerSource Inner { get; set; } +} + +public class InnerSource +{ + public int OtherValue { get; set; } +} +``` + +We _could_ simply flatten the OuterSource.Inner.OtherValue to one InnerOtherValue property, but we might also want to create a corresponding complex type for the Inner property: + +```c# +public class OuterDest +{ + public int Value { get; set; } + public InnerDest Inner { get; set; } +} + +public class InnerDest +{ + public int OtherValue { get; set; } +} +``` + +In that case, we would need to configure the additional source/destination type mappings: + +```c# +var config = new MapperConfiguration(cfg => { + cfg.CreateMap(); + cfg.CreateMap(); +}); +config.AssertConfigurationIsValid(); + +var source = new OuterSource + { + Value = 5, + Inner = new InnerSource {OtherValue = 15} + }; +var mapper = config.CreateMapper(); +var dest = mapper.Map(source); + +dest.Value.ShouldEqual(5); +dest.Inner.ShouldNotBeNull(); +dest.Inner.OtherValue.ShouldEqual(15); +``` + +A few things to note here: + +* Order of configuring types does not matter +* Call to Map does not need to specify any inner type mappings, only the type map to use for the source value passed in + +With both flattening and nested mappings, we can create a variety of destination shapes to suit whatever our needs may be. diff --git a/docs/Null-substitution.md b/docs/source/Null-substitution.md similarity index 100% rename from docs/Null-substitution.md rename to docs/source/Null-substitution.md diff --git a/docs/Open-Generics.md b/docs/source/Open-Generics.md similarity index 100% rename from docs/Open-Generics.md rename to docs/source/Open-Generics.md diff --git a/docs/Projection.md b/docs/source/Projection.md similarity index 100% rename from docs/Projection.md rename to docs/source/Projection.md diff --git a/docs/Queryable-Extensions.md b/docs/source/Queryable-Extensions.md similarity index 93% rename from docs/Queryable-Extensions.md rename to docs/source/Queryable-Extensions.md index 500356d3c2..40a1633336 100644 --- a/docs/Queryable-Extensions.md +++ b/docs/source/Queryable-Extensions.md @@ -1,232 +1,242 @@ -# Queryable Extensions - -When using an ORM such as NHibernate or Entity Framework with AutoMapper's standard `mapper.Map` functions, you may notice that the ORM will query all the fields of all the objects within a graph when AutoMapper is attempting to map the results to a destination type. - -If your ORM exposes `IQueryable`s, you can use AutoMapper's QueryableExtensions helper methods to address this key pain. - -Using Entity Framework for an example, say that you have an entity `OrderLine` with a relationship with an entity `Item`. If you want to map this to an `OrderLineDTO` with the `Item`'s `Name` property, the standard `mapper.Map` call will result in Entity Framework querying the entire `OrderLine` and `Item` table. - -Use this approach instead. - -Given the following entities: - -```c# -public class OrderLine -{ - public int Id { get; set; } - public int OrderId { get; set; } - public Item Item { get; set; } - public decimal Quantity { get; set; } -} - -public class Item -{ - public int Id { get; set; } - public string Name { get; set; } -} -``` - -And the following DTO: - -```c# -public class OrderLineDTO -{ - public int Id { get; set; } - public int OrderId { get; set; } - public string Item { get; set; } - public decimal Quantity { get; set; } -} -``` - -You can use the Queryable Extensions like so: - -```c# -var configuration = new MapperConfiguration(cfg => - cfg.CreateProjection() - .ForMember(dto => dto.Item, conf => conf.MapFrom(ol => ol.Item.Name))); - -public List GetLinesForOrder(int orderId) -{ - using (var context = new orderEntities()) - { - return context.OrderLines.Where(ol => ol.OrderId == orderId) - .ProjectTo(configuration).ToList(); - } -} -``` - -The `.ProjectTo()` will tell AutoMapper's mapping engine to emit a `select` clause to the IQueryable that will inform entity framework that it only needs to query the Name column of the Item table, same as if you manually projected your `IQueryable` to an `OrderLineDTO` with a `Select` clause. - -`ProjectTo` must be the last call in the chain. ORMs work with entities, not DTOs. So apply any filtering and sorting on entities and, as the last step, project to DTOs. - -Note that for this feature to work, all type conversions must be explicitly handled in your Mapping. For example, you can not rely on the `ToString()` override of the `Item` class to inform entity framework to only select from the `Name` column, and any data type changes, such as `Double` to `Decimal` must be explicitly handled as well. - -### The instance API - -Starting with 8.0 there are similar ProjectTo methods on IMapper that feel more natural when you use IMapper with DI. - -### Preventing lazy loading/SELECT N+1 problems - -Because the LINQ projection built by AutoMapper is translated directly to a SQL query by the query provider, the mapping occurs at the SQL/ADO.NET level, and not touching your entities. All data is eagerly fetched and loaded into your DTOs. - -Nested collections use a Select to project child DTOs: - -```c# -from i in db.Instructors -orderby i.LastName -select new InstructorIndexData.InstructorModel -{ - ID = i.ID, - FirstMidName = i.FirstMidName, - LastName = i.LastName, - HireDate = i.HireDate, - OfficeAssignmentLocation = i.OfficeAssignment.Location, - Courses = i.Courses.Select(c => new InstructorIndexData.InstructorCourseModel - { - CourseID = c.CourseID, - CourseTitle = c.Title - }).ToList() -}; -``` - +# Queryable Extensions + +When using an ORM such as NHibernate or Entity Framework with AutoMapper's standard `mapper.Map` functions, you may notice that the ORM will query all the fields of all the objects within a graph when AutoMapper is attempting to map the results to a destination type. + +If your ORM exposes `IQueryable`s, you can use AutoMapper's QueryableExtensions helper methods to address this key pain. + +Using Entity Framework for an example, say that you have an entity `OrderLine` with a relationship with an entity `Item`. If you want to map this to an `OrderLineDTO` with the `Item`'s `Name` property, the standard `mapper.Map` call will result in Entity Framework querying the entire `OrderLine` and `Item` table. + +Use this approach instead. + +Given the following entities: + +```c# +public class OrderLine +{ + public int Id { get; set; } + public int OrderId { get; set; } + public Item Item { get; set; } + public decimal Quantity { get; set; } +} + +public class Item +{ + public int Id { get; set; } + public string Name { get; set; } +} +``` + +And the following DTO: + +```c# +public class OrderLineDTO +{ + public int Id { get; set; } + public int OrderId { get; set; } + public string Item { get; set; } + public decimal Quantity { get; set; } +} +``` + +You can use the Queryable Extensions like so: + +```c# +var configuration = new MapperConfiguration(cfg => + cfg.CreateProjection() + .ForMember(dto => dto.Item, conf => conf.MapFrom(ol => ol.Item.Name))); + +public List GetLinesForOrder(int orderId) +{ + using (var context = new orderEntities()) + { + return context.OrderLines.Where(ol => ol.OrderId == orderId) + .ProjectTo(configuration).ToList(); + } +} +``` + +The `.ProjectTo()` will tell AutoMapper's mapping engine to emit a `select` clause to the IQueryable that will inform entity framework that it only needs to query the Name column of the Item table, same as if you manually projected your `IQueryable` to an `OrderLineDTO` with a `Select` clause. + +### Query Provider Limitations + +`ProjectTo` must be the last call in the LINQ method chain. ORMs work with entities, not DTOs. Apply any filtering and sorting on entities and, as the last step, project to DTOs. Query providers are highly complex and making the `ProjectTo` call last ensures the query provider works as closely as designed to build valid queries against the underlying query target (SQL, Mongo QL etc.). + +Note that for this feature to work, all type conversions must be explicitly handled in your Mapping. For example, you can not rely on the `ToString()` override of the `Item` class to inform entity framework to only select from the `Name` column, and any data type changes, such as `Double` to `Decimal` must be explicitly handled as well. + +### The instance API + +Starting with 8.0 there are similar ProjectTo methods on IMapper that feel more natural when you use IMapper with DI. + +### Preventing lazy loading/SELECT N+1 problems + +Because the LINQ projection built by AutoMapper is translated directly to a SQL query by the query provider, the mapping occurs at the SQL/ADO.NET level, and not touching your entities. All data is eagerly fetched and loaded into your DTOs. + +Nested collections use a Select to project child DTOs: + +```c# +from i in db.Instructors +orderby i.LastName +select new InstructorIndexData.InstructorModel +{ + ID = i.ID, + FirstMidName = i.FirstMidName, + LastName = i.LastName, + HireDate = i.HireDate, + OfficeAssignmentLocation = i.OfficeAssignment.Location, + Courses = i.Courses.Select(c => new InstructorIndexData.InstructorCourseModel + { + CourseID = c.CourseID, + CourseTitle = c.Title + }).ToList() +}; +``` + This map through AutoMapper will result in a SELECT N+1 problem, as each child `Course` will be queried one at a time, unless specified through your ORM to eagerly fetch. With LINQ projection, no special configuration or specification is needed with your ORM. The ORM uses the LINQ projection to build the exact SQL query needed. That means that you don't need to use explicit eager loading (`Include`) with `ProjectTo`. If you need something like filtered `Include`, have the filter in your map: -```c# - - CreateProjection().ForMember(d => d.Collection, o => o.MapFrom(s => s.Collection.Where(i => ...)); -``` - -### Custom projection - -In the case where members names don't line up, or you want to create calculated property, you can use MapFrom (the expression-based overload) to supply a custom expression for a destination member: - -```c# -var configuration = new MapperConfiguration(cfg => cfg.CreateProjection() - .ForMember(d => d.FullName, opt => opt.MapFrom(c => c.FirstName + " " + c.LastName)) - .ForMember(d => d.TotalContacts, opt => opt.MapFrom(c => c.Contacts.Count())); -``` - -AutoMapper passes the supplied expression with the built projection. As long as your query provider can interpret the supplied expression, everything will be passed down all the way to the database. - -If the expression is rejected from your query provider (Entity Framework, NHibernate, etc.), you might need to tweak your expression until you find one that is accepted. - -### Custom Type Conversion - -Occasionally, you need to completely replace a type conversion from a source to a destination type. In normal runtime mapping, this is accomplished via the ConvertUsing method. To perform the analog in LINQ projection, use the ConvertUsing method: - -```c# -cfg.CreateProjection().ConvertUsing(src => new Dest { Value = 10 }); -``` - -The expression-based `ConvertUsing` is slightly more limited than Func-based `ConvertUsing` overloads as only what is allowed in an Expression and the underlying LINQ provider will work. - -### Custom destination type constructors - -If your destination type has a custom constructor but you don't want to override the entire mapping, use the ConstructUsing expression-based method overload: - -```c# -cfg.CreateProjection() - .ConstructUsing(src => new Dest(src.Value + 10)); -``` - -AutoMapper will automatically match up destination constructor parameters to source members based on matching names, so only use this method if AutoMapper can't match up the destination constructor properly, or if you need extra customization during construction. - -### String conversion - -AutoMapper will automatically add `ToString()` when the destination member type is a string and the source member type is not. - -```c# -public class Order { - public OrderTypeEnum OrderType { get; set; } -} -public class OrderDto { - public string OrderType { get; set; } -} -var orders = dbContext.Orders.ProjectTo(configuration).ToList(); -orders[0].OrderType.ShouldEqual("Online"); -``` - -### Explicit expansion - -In some scenarios, such as OData, a generic DTO is returned through an IQueryable controller action. Without explicit instructions, AutoMapper will expand all members in the result. To control which members are expanded during projection, set ExplicitExpansion in the configuration and then pass in the members you want to explicitly expand: - -```c# -dbContext.Orders.ProjectTo(configuration, - dest => dest.Customer, - dest => dest.LineItems); -// or string-based -dbContext.Orders.ProjectTo(configuration, - null, - "Customer", - "LineItems"); -// for collections -dbContext.Orders.ProjectTo(configuration, - null, - dest => dest.LineItems.Select(item => item.Product)); -``` -For more information, see [the tests](https://github.com/AutoMapper/AutoMapper/search?p=1&q=ExplicitExpansion&utf8=%E2%9C%93). - -### Aggregations - -LINQ can support aggregate queries, and AutoMapper supports LINQ extension methods. In the custom projection example, if we renamed the `TotalContacts` property to `ContactsCount`, AutoMapper would match to the `Count()` extension method and the LINQ provider would translate the count into a correlated subquery to aggregate child records. - -AutoMapper can also support complex aggregations and nested restrictions, if the LINQ provider supports it: - -```c# -cfg.CreateProjection() - .ForMember(m => m.EnrollmentsStartingWithA, - opt => opt.MapFrom(c => c.Enrollments.Where(e => e.Student.LastName.StartsWith("A")).Count())); -``` - -This query returns the total number of students, for each course, whose last name starts with the letter 'A'. - -### Parameterization - -Occasionally, projections need runtime parameters for their values. Consider a projection that needs to pull in the current username as part of its data. Instead of using post-mapping code, we can parameterize our MapFrom configuration: - -```c# -string currentUserName = null; -cfg.CreateProjection() - .ForMember(m => m.CurrentUserName, opt => opt.MapFrom(src => currentUserName)); -``` - -When we project, we'll substitute our parameter at runtime: - -```c# -dbContext.Courses.ProjectTo(Config, new { currentUserName = Request.User.Name }); -``` - -This works by capturing the name of the closure's field name in the original expression, then using an anonymous object/dictionary to apply the value to the parameter value before the query is sent to the query provider. - -You may also use a dictionary to build the projection values: - -```c# -dbContext.Courses.ProjectTo(Config, new Dictionary { {"currentUserName", Request.User.Name} }); -``` - -However, using a dictionary will result in hard-coded values in the query instead of a parameterized query, so use with caution. - -### Supported mapping options - -Not all mapping options can be supported, as the expression generated must be interpreted by a LINQ provider. Only what is supported by LINQ providers is supported by AutoMapper: -* MapFrom (Expression-based) -* ConvertUsing (Expression-based) -* Ignore -* NullSubstitute +```c# + + CreateProjection().ForMember(d => d.Collection, o => o.MapFrom(s => s.Collection.Where(i => ...)); +``` + +### Custom projection + +In the case where members names don't line up, or you want to create calculated property, you can use MapFrom (the expression-based overload) to supply a custom expression for a destination member: + +```c# +var configuration = new MapperConfiguration(cfg => cfg.CreateProjection() + .ForMember(d => d.FullName, opt => opt.MapFrom(c => c.FirstName + " " + c.LastName)) + .ForMember(d => d.TotalContacts, opt => opt.MapFrom(c => c.Contacts.Count())); +``` + +AutoMapper passes the supplied expression with the built projection. As long as your query provider can interpret the supplied expression, everything will be passed down all the way to the database. + +If the expression is rejected from your query provider (Entity Framework, NHibernate, etc.), you might need to tweak your expression until you find one that is accepted. + +### Custom Type Conversion + +Occasionally, you need to completely replace a type conversion from a source to a destination type. In normal runtime mapping, this is accomplished via the ConvertUsing method. To perform the analog in LINQ projection, use the ConvertUsing method: + +```c# +cfg.CreateProjection().ConvertUsing(src => new Dest { Value = 10 }); +``` + +The expression-based `ConvertUsing` is slightly more limited than Func-based `ConvertUsing` overloads as only what is allowed in an Expression and the underlying LINQ provider will work. + +### Custom destination type constructors + +If your destination type has a custom constructor but you don't want to override the entire mapping, use the ConstructUsing expression-based method overload: + +```c# +cfg.CreateProjection() + .ConstructUsing(src => new Dest(src.Value + 10)); +``` + +AutoMapper will automatically match up destination constructor parameters to source members based on matching names, so only use this method if AutoMapper can't match up the destination constructor properly, or if you need extra customization during construction. + +### String conversion + +AutoMapper will automatically add `ToString()` when the destination member type is a string and the source member type is not. + +```c# +public class Order { + public OrderTypeEnum OrderType { get; set; } +} +public class OrderDto { + public string OrderType { get; set; } +} +var orders = dbContext.Orders.ProjectTo(configuration).ToList(); +orders[0].OrderType.ShouldEqual("Online"); +``` + +### Explicit expansion + +In some scenarios, such as OData, a generic DTO is returned through an IQueryable controller action. Without explicit instructions, AutoMapper will expand all members in the result. To control which members are expanded during projection, set ExplicitExpansion in the configuration and then pass in the members you want to explicitly expand: + +```c# +dbContext.Orders.ProjectTo(configuration, + dest => dest.Customer, + dest => dest.LineItems); +// or string-based +dbContext.Orders.ProjectTo(configuration, + null, + "Customer", + "LineItems"); +// for collections +dbContext.Orders.ProjectTo(configuration, + null, + dest => dest.LineItems.Select(item => item.Product)); +``` +For more information, see [the tests](https://github.com/AutoMapper/AutoMapper/search?p=1&q=ExplicitExpansion&utf8=%E2%9C%93). + +### Aggregations + +LINQ can support aggregate queries, and AutoMapper supports LINQ extension methods. In the custom projection example, if we renamed the `TotalContacts` property to `ContactsCount`, AutoMapper would match to the `Count()` extension method and the LINQ provider would translate the count into a correlated subquery to aggregate child records. + +AutoMapper can also support complex aggregations and nested restrictions, if the LINQ provider supports it: + +```c# +cfg.CreateProjection() + .ForMember(m => m.EnrollmentsStartingWithA, + opt => opt.MapFrom(c => c.Enrollments.Where(e => e.Student.LastName.StartsWith("A")).Count())); +``` + +This query returns the total number of students, for each course, whose last name starts with the letter 'A'. + +### Parameterization + +Occasionally, projections need runtime parameters for their values. Consider a projection that needs to pull in the current username as part of its data. Instead of using post-mapping code, we can parameterize our MapFrom configuration: + +```c# +string currentUserName = null; +cfg.CreateProjection() + .ForMember(m => m.CurrentUserName, opt => opt.MapFrom(src => currentUserName)); +``` + +When we project, we'll substitute our parameter at runtime: + +```c# +dbContext.Courses.ProjectTo(Config, new { currentUserName = Request.User.Name }); +``` + +This works by capturing the name of the closure's field name in the original expression, then using an anonymous object/dictionary to apply the value to the parameter value before the query is sent to the query provider. + +You may also use a dictionary to build the projection values: + +```c# +dbContext.Courses.ProjectTo(Config, new Dictionary { {"currentUserName", Request.User.Name} }); +``` + +However, using a dictionary will result in hard-coded values in the query instead of a parameterized query, so use with caution. + +### Recursive models + +Ideally, you would avoid models that reference themselves (do some research). But if you must, you need to enable them: + +```c# +configuration.Internal().RecursiveQueriesMaxDepth = someRandomNumber; +``` + +### Supported mapping options + +Not all mapping options can be supported, as the expression generated must be interpreted by a LINQ provider. Only what is supported by LINQ providers is supported by AutoMapper: +* MapFrom (Expression-based) +* ConvertUsing (Expression-based) +* Ignore +* NullSubstitute * Value transformers -* IncludeMembers - -Not supported: -* Condition -* SetMappingOrder -* UseDestinationValue -* MapFrom (Func-based) -* Before/AfterMap -* Custom resolvers -* Custom type converters -* ForPath +* IncludeMembers +* Runtime polymorphic mapping with Include/IncludeBase + +Not supported: +* Condition +* SetMappingOrder +* UseDestinationValue +* MapFrom (Func-based) +* Before/AfterMap +* Custom resolvers +* Custom type converters +* ForPath * Value converters -* Runtime polymorphic mapping with Include/IncludeBase * **Any calculated property on your domain object** \ No newline at end of file diff --git a/docs/Reverse-Mapping-and-Unflattening.md b/docs/source/Reverse-Mapping-and-Unflattening.md similarity index 100% rename from docs/Reverse-Mapping-and-Unflattening.md rename to docs/source/Reverse-Mapping-and-Unflattening.md diff --git a/docs/Setup.md b/docs/source/Setup.md similarity index 100% rename from docs/Setup.md rename to docs/source/Setup.md diff --git a/docs/The-MyGet-build.md b/docs/source/The-MyGet-build.md similarity index 100% rename from docs/The-MyGet-build.md rename to docs/source/The-MyGet-build.md diff --git a/docs/Understanding-your-mapping.md b/docs/source/Understanding-your-mapping.md similarity index 100% rename from docs/Understanding-your-mapping.md rename to docs/source/Understanding-your-mapping.md diff --git a/docs/Value-converters.md b/docs/source/Value-converters.md similarity index 100% rename from docs/Value-converters.md rename to docs/source/Value-converters.md diff --git a/docs/Value-transformers.md b/docs/source/Value-transformers.md similarity index 100% rename from docs/Value-transformers.md rename to docs/source/Value-transformers.md diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000000..f255fabae6 --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,39 @@ +# Configuration file for the Sphinx documentation builder. + +# -- Project information + +project = 'AutoMapper' +copyright = '2024, Jimmy Bogard' +author = 'Jimmy Bogard' + +# -- General configuration + +extensions = [ + 'sphinx.ext.duration', + 'sphinx.ext.doctest', + 'sphinx.ext.autodoc', + 'sphinx.ext.autosummary', + 'sphinx.ext.intersphinx', + 'myst_parser' +] + +intersphinx_mapping = { + 'python': ('https://docs.python.org/3/', None), + 'sphinx': ('https://www.sphinx-doc.org/en/master/', None), +} +intersphinx_disabled_domains = ['std'] + +templates_path = ['_templates'] + +# -- Options for HTML output + +html_theme = 'sphinx_rtd_theme' +html_theme_options = { + 'logo_only': True, + 'display_version': False +} +html_logo = 'img/logo.png' + + +# -- Options for EPUB output +epub_show_urls = 'footnote' \ No newline at end of file diff --git a/docs/img/logo.png b/docs/source/img/logo.png similarity index 100% rename from docs/img/logo.png rename to docs/source/img/logo.png diff --git a/docs/index.rst b/docs/source/index.rst similarity index 93% rename from docs/index.rst rename to docs/source/index.rst index 24f0f4d003..171e4e9381 100644 --- a/docs/index.rst +++ b/docs/source/index.rst @@ -11,10 +11,6 @@ other simple objects, whose design is better suited for serialization, communication, messaging, or simply an anti-corruption layer between the domain and application layer. -AutoMapper supports the following platforms: - -* `.NET Standard 2.1+ `_ - New to AutoMapper? Check out the :doc:`Getting-started` page first. .. _user-docs: @@ -24,7 +20,6 @@ New to AutoMapper? Check out the :doc:`Getting-started` page first. :caption: Overview Getting-started - Quickstart Understanding-your-mapping The-MyGet-build @@ -72,6 +67,7 @@ New to AutoMapper? Check out the :doc:`Getting-started` page first. :caption: Upgrading API-Changes + 13.0-Upgrade-Guide 12.0-Upgrade-Guide 11.0-Upgrade-Guide 10.0-Upgrade-Guide diff --git a/src/AutoMapper.DI.Tests/AppDomainResolutionTests.cs b/src/AutoMapper.DI.Tests/AppDomainResolutionTests.cs new file mode 100644 index 0000000000..d5801cd4d3 --- /dev/null +++ b/src/AutoMapper.DI.Tests/AppDomainResolutionTests.cs @@ -0,0 +1,39 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace AutoMapper.Extensions.Microsoft.DependencyInjection.Tests +{ + using System; + using AutoMapper.Internal; + using Shouldly; + using Xunit; + + public class AppDomainResolutionTests + { + private readonly IServiceProvider _provider; + + public AppDomainResolutionTests() + { + IServiceCollection services = new ServiceCollection(); + services.AddAutoMapper(typeof(AppDomainResolutionTests)); + _provider = services.BuildServiceProvider(); + } + + [Fact] + public void ShouldResolveConfiguration() + { + _provider.GetService().ShouldNotBeNull(); + } + + [Fact] + public void ShouldConfigureProfiles() + { + _provider.GetService().Internal().GetAllTypeMaps().Count.ShouldBe(4); + } + + [Fact] + public void ShouldResolveMapper() + { + _provider.GetService().ShouldNotBeNull(); + } + } +} \ No newline at end of file diff --git a/src/AutoMapper.DI.Tests/AssemblyResolutionTests.cs b/src/AutoMapper.DI.Tests/AssemblyResolutionTests.cs new file mode 100644 index 0000000000..569b56281a --- /dev/null +++ b/src/AutoMapper.DI.Tests/AssemblyResolutionTests.cs @@ -0,0 +1,52 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace AutoMapper.Extensions.Microsoft.DependencyInjection.Tests +{ + using System; + using System.Reflection; + using AutoMapper.Internal; + using Shouldly; + using Xunit; + + public class AssemblyResolutionTests + { + private static readonly IServiceProvider _provider; + + static AssemblyResolutionTests() + { + _provider = BuildServiceProvider(); + } + + private static ServiceProvider BuildServiceProvider() + { + IServiceCollection services = new ServiceCollection(); + services.AddAutoMapper(typeof(Source).GetTypeInfo().Assembly); + var serviceProvider = services.BuildServiceProvider(); + return serviceProvider; + } + + [Fact] + public void ShouldResolveConfiguration() + { + _provider.GetService().ShouldNotBeNull(); + } + + [Fact] + public void ShouldConfigureProfiles() + { + _provider.GetService().Internal().GetAllTypeMaps().Count.ShouldBe(4); + } + + [Fact] + public void ShouldResolveMapper() + { + _provider.GetService().ShouldNotBeNull(); + } + + [Fact] + public void CanRegisterTwiceWithoutProblems() + { + new Action(() => BuildServiceProvider()).ShouldNotThrow(); + } + } +} \ No newline at end of file diff --git a/src/AutoMapper.DI.Tests/AttributeTests.cs b/src/AutoMapper.DI.Tests/AttributeTests.cs new file mode 100644 index 0000000000..bcdbfba670 --- /dev/null +++ b/src/AutoMapper.DI.Tests/AttributeTests.cs @@ -0,0 +1,27 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using Xunit; + +namespace AutoMapper.Extensions.Microsoft.DependencyInjection.Tests +{ + public class AttributeTests + { + [Fact] + public void Should_not_register_static_instance_when_configured() + { + IServiceCollection services = new ServiceCollection(); + services.AddAutoMapper(typeof(Source3)); + + var serviceProvider = services.BuildServiceProvider(); + + var mapper = serviceProvider.GetService(); + + var source = new Source3 {Value = 3}; + + var dest = mapper.Map(source); + + dest.Value.ShouldBe(source.Value); + } + } +} \ No newline at end of file diff --git a/src/AutoMapper.DI.Tests/AutoMapper.DI.Tests.csproj b/src/AutoMapper.DI.Tests/AutoMapper.DI.Tests.csproj new file mode 100644 index 0000000000..6bcbcec357 --- /dev/null +++ b/src/AutoMapper.DI.Tests/AutoMapper.DI.Tests.csproj @@ -0,0 +1,26 @@ + + + + net8.0 + true + AutoMapper.Extensions.Microsoft.DependencyInjection.Tests + AutoMapper.Extensions.Microsoft.DependencyInjection.Tests + true + false + false + false + + + + + + + + + + + + + + + diff --git a/src/AutoMapper.DI.Tests/DependencyTests.cs b/src/AutoMapper.DI.Tests/DependencyTests.cs new file mode 100644 index 0000000000..fe4b35581e --- /dev/null +++ b/src/AutoMapper.DI.Tests/DependencyTests.cs @@ -0,0 +1,40 @@ +namespace AutoMapper.Extensions.Microsoft.DependencyInjection.Tests +{ + using System; + using global::Microsoft.Extensions.DependencyInjection; + using Shouldly; + using Xunit; + + public class DependencyTests + { + private readonly IServiceProvider _provider; + + public DependencyTests() + { + IServiceCollection services = new ServiceCollection(); + services.AddTransient(sp => new FooService(5)); + services.AddAutoMapper(typeof(Source), typeof(Profile)); + _provider = services.BuildServiceProvider(); + + _provider.GetService().AssertConfigurationIsValid(); + } + + [Fact] + public void ShouldResolveWithDependency() + { + var mapper = _provider.GetService(); + var dest = mapper.Map(new Source2()); + + dest.ResolvedValue.ShouldBe(5); + } + + [Fact] + public void ShouldConvertWithDependency() + { + var mapper = _provider.GetService(); + var dest = mapper.Map(new Source2 { ConvertedValue = 5}); + + dest.ConvertedValue.ShouldBe(10); + } + } +} diff --git a/src/AutoMapper.DI.Tests/Integrations/ServiceLifetimeTests.cs b/src/AutoMapper.DI.Tests/Integrations/ServiceLifetimeTests.cs new file mode 100644 index 0000000000..ca0373b294 --- /dev/null +++ b/src/AutoMapper.DI.Tests/Integrations/ServiceLifetimeTests.cs @@ -0,0 +1,64 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Shouldly; +using Xunit; + +namespace AutoMapper.Extensions.Microsoft.DependencyInjection.Tests.Integrations +{ + public class ServiceLifetimeTests + { + internal interface ISingletonService + { + Bar DoTheThing(Foo theObj); + } + + internal class TestSingletonService : ISingletonService + { + private readonly IMapper _mapper; + + public TestSingletonService(IMapper mapper) + { + _mapper = mapper; + } + + public Bar DoTheThing(Foo theObj) + { + var bar = _mapper.Map(theObj); + return bar; + } + } + + internal class Foo + { + public int TheValue { get; set; } + } + + internal class Bar + { + public int TheValue { get; set; } + } + + + [Fact] + public void CanUseDefaultInjectedIMapperInSingletonService() + { + //arrange + var services = new ServiceCollection(); + services.TryAddSingleton(); + services.AddAutoMapper(cfg => cfg.CreateMap().ReverseMap(), GetType().Assembly); + var sp = services.BuildServiceProvider(); + Bar actual; + + //act + using (var scope = sp.CreateScope()) + { + var service = scope.ServiceProvider.GetService(); + actual = service.DoTheThing(new Foo{TheValue = 1}); + } + + //assert + actual.ShouldNotBeNull(); + actual.TheValue.ShouldBe(1); + } + } +} \ No newline at end of file diff --git a/src/AutoMapper.DI.Tests/MultipleRegistrationTests.cs b/src/AutoMapper.DI.Tests/MultipleRegistrationTests.cs new file mode 100644 index 0000000000..5c14981944 --- /dev/null +++ b/src/AutoMapper.DI.Tests/MultipleRegistrationTests.cs @@ -0,0 +1,41 @@ +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using Xunit; + +namespace AutoMapper.Extensions.Microsoft.DependencyInjection.Tests +{ + public class MultipleRegistrationTests + { + [Fact] + public void Can_register_multiple_times() + { + var services = new ServiceCollection(); + + services.AddAutoMapper(cfg => { }); + services.AddAutoMapper(cfg => { }); + services.AddAutoMapper(cfg => { }); + + var serviceProvider = services.BuildServiceProvider(); + + serviceProvider.GetService().ShouldNotBeNull(); + } + + [Fact] + public void Can_register_assembly_multiple_times() + { + var services = new ServiceCollection(); + + services.AddAutoMapper(typeof(MultipleRegistrationTests)); + services.AddAutoMapper(typeof(MultipleRegistrationTests)); + services.AddAutoMapper(typeof(MultipleRegistrationTests)); + services.AddTransient(); + + var serviceProvider = services.BuildServiceProvider(); + + serviceProvider.GetService().ShouldNotBeNull(); + serviceProvider.GetService().ShouldNotBeNull(); + serviceProvider.GetServices().Count().ShouldBe(1); + } + } +} \ No newline at end of file diff --git a/src/AutoMapper.DI.Tests/Profiles.cs b/src/AutoMapper.DI.Tests/Profiles.cs new file mode 100644 index 0000000000..5e9b14e61e --- /dev/null +++ b/src/AutoMapper.DI.Tests/Profiles.cs @@ -0,0 +1,153 @@ +using System; + +namespace AutoMapper.Extensions.Microsoft.DependencyInjection.Tests +{ + public class Source + { + + } + + public class Dest + { + + } + + public class Source2 + { + public int ConvertedValue { get; set; } + } + + public class Dest2 + { + public int ResolvedValue { get; set; } + public int ConvertedValue { get; set; } + } + + public class Source3 + { + public int Value { get; set; } + } + + [AutoMap(typeof(Source3))] + public class Dest3 + { + public int Value { get; set; } + } + + public class Profile1 : Profile + { + public Profile1() + { + CreateMap(); + } + } + + public abstract class AbstractProfile : Profile { } + + internal class Profile2 : Profile + { + public Profile2() + { + CreateMap() + .ForMember(d => d.ResolvedValue, opt => opt.MapFrom()) + .ForMember(d => d.ConvertedValue, opt => opt.ConvertUsing()); + CreateMap(typeof(Enum), typeof(EnumDescriptor<>)).ConvertUsing(typeof(EnumDescriptorTypeConverter<>)); + } + } + + public class DependencyResolver : IValueResolver + { + private readonly ISomeService _service; + + public DependencyResolver(ISomeService service) + { + _service = service; + } + + public int Resolve(object source, object destination, int destMember, ResolutionContext context) + { + return _service.Modify(destMember); + } + } + + public interface ISomeService + { + int Modify(int value); + } + + public class MutableService : ISomeService + { + public int Value { get; set; } + + public int Modify(int value) => value + Value; + } + + public class FooService : ISomeService + { + private readonly int _value; + + public FooService(int value) + { + _value = value; + } + + public int Modify(int value) => value + _value; + } + + internal class FooMappingAction : IMappingAction + { + public void Process(object source, object destination, ResolutionContext context) { } + } + + internal class FooValueResolver: IValueResolver + { + public object Resolve(object source, object destination, object destMember, ResolutionContext context) + { + return null; + } + } + + internal class FooMemberValueResolver : IMemberValueResolver + { + public object Resolve(object source, object destination, object sourceMember, object destMember, ResolutionContext context) + { + return null; + } + } + + internal class FooTypeConverter : ITypeConverter + { + public object Convert(object source, object destination, ResolutionContext context) + { + return null; + } + } + + public class EnumDescriptor where TSource : Enum + { + public int Value { get; set; } + } + + public class EnumDescriptorTypeConverter : ITypeConverter> + where TSource : Enum + { + public EnumDescriptor Convert(Enum source, EnumDescriptor destination, ResolutionContext context) => + new EnumDescriptor{ Value = int.MaxValue }; + } + + internal class FooValueConverter : IValueConverter + { + public int Convert(int sourceMember, ResolutionContext context) + => sourceMember + 1; + } + + internal class DependencyValueConverter : IValueConverter + { + private readonly ISomeService _service; + + public DependencyValueConverter(ISomeService service) => _service = service; + + public int Convert(int sourceMember, ResolutionContext context) + => _service.Modify(sourceMember); + } +} \ No newline at end of file diff --git a/src/AutoMapper.DI.Tests/Properties/AssemblyInfo.cs b/src/AutoMapper.DI.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..c4d34326a6 --- /dev/null +++ b/src/AutoMapper.DI.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,21 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("AutoMapper.Extensions.Microsoft.DependencyInjection.Tests")] +[assembly: AssemblyTrademark("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("a93a7f85-292a-4130-891d-4307d3f60c30")] + +[assembly: Xunit.CollectionBehavior(DisableTestParallelization = true)] diff --git a/src/AutoMapper.DI.Tests/ScopeTests.cs b/src/AutoMapper.DI.Tests/ScopeTests.cs new file mode 100644 index 0000000000..75edc3dc3a --- /dev/null +++ b/src/AutoMapper.DI.Tests/ScopeTests.cs @@ -0,0 +1,75 @@ +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using Xunit; + +namespace AutoMapper.Extensions.Microsoft.DependencyInjection.Tests +{ + public class ScopeTests + { + [Fact] + public void Can_depend_on_scoped_services_as_transient_default() + { + var services = new ServiceCollection(); + services.AddAutoMapper(new [] { typeof(Source).Assembly }); + services.AddScoped(); + + var provider = services.BuildServiceProvider(); + + using (var scope = provider.CreateScope()) + { + var mutableService = (MutableService)scope.ServiceProvider.GetService(); + mutableService.Value = 10; + + var mapper = scope.ServiceProvider.GetService(); + + var dest = mapper.Map(new Source2 {ConvertedValue = 5}); + + dest.ConvertedValue.ShouldBe(15); + } + } + + [Fact] + public void Can_depend_on_scoped_services_as_scoped() + { + var services = new ServiceCollection(); + services.AddAutoMapper(new [] { typeof(Source).Assembly }, ServiceLifetime.Scoped); + services.AddScoped(); + + var provider = services.BuildServiceProvider(); + + using (var scope = provider.CreateScope()) + { + var mutableService = (MutableService)scope.ServiceProvider.GetService(); + mutableService.Value = 10; + + var mapper = scope.ServiceProvider.GetService(); + + var dest = mapper.Map(new Source2 {ConvertedValue = 5}); + + dest.ConvertedValue.ShouldBe(15); + } + } + + [Fact] + public void Cannot_correctly_resolve_scoped_services_as_singleton() + { + var services = new ServiceCollection(); + services.AddAutoMapper(new [] { typeof(Source).Assembly }, ServiceLifetime.Singleton); + services.AddScoped(); + + var provider = services.BuildServiceProvider(); + + using (var scope = provider.CreateScope()) + { + var mutableService = (MutableService)scope.ServiceProvider.GetService(); + mutableService.Value = 10; + + var mapper = scope.ServiceProvider.GetService(); + + var dest = mapper.Map(new Source2 {ConvertedValue = 5}); + + dest.ConvertedValue.ShouldBe(5); + } + } + } +} \ No newline at end of file diff --git a/src/AutoMapper.DI.Tests/ServiceLifetimeTests.cs b/src/AutoMapper.DI.Tests/ServiceLifetimeTests.cs new file mode 100644 index 0000000000..eb61d63767 --- /dev/null +++ b/src/AutoMapper.DI.Tests/ServiceLifetimeTests.cs @@ -0,0 +1,317 @@ +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using Xunit; + +namespace AutoMapper.Extensions.Microsoft.DependencyInjection.Tests +{ + public class ServiceLifetimeTests + { + //Implicitly Transient + [Fact] + public void AddAutoMapperExtensionDefaultWithAssemblySingleDelegateArgCollection() + { + //arrange + var serviceCollection = new ServiceCollection(); + + //act + serviceCollection.AddAutoMapper(cfg => { }, new List()); + var serviceDescriptor = serviceCollection.FirstOrDefault(sd => sd.ServiceType == typeof(IMapper)); + + //assert + serviceDescriptor.ShouldNotBeNull(); + serviceDescriptor.Lifetime.ShouldBe(ServiceLifetime.Transient); + } + + [Fact] + public void AddAutoMapperExtensionDefaultWithAssemblyDoubleDelegateArgCollection() + { + //arrange + var serviceCollection = new ServiceCollection(); + + //act + serviceCollection.AddAutoMapper((sp, cfg) => { }, new List()); + var serviceDescriptor = serviceCollection.FirstOrDefault(sd => sd.ServiceType == typeof(IMapper)); + + //assert + serviceDescriptor.ShouldNotBeNull(); + serviceDescriptor.Lifetime.ShouldBe(ServiceLifetime.Transient); + } + + [Fact] + public void AddAutoMapperExtensionDefaultWithAssemblyCollection() + { + //arrange + var serviceCollection = new ServiceCollection(); + + //act + serviceCollection.AddAutoMapper(new List()); + var serviceDescriptor = serviceCollection.FirstOrDefault(sd => sd.ServiceType == typeof(IMapper)); + + //assert + serviceDescriptor.ShouldNotBeNull(); + serviceDescriptor.Lifetime.ShouldBe(ServiceLifetime.Transient); + } + + [Fact] + public void AddAutoMapperExtensionDefaultSingleDelegateWithProfileTypeCollection() + { + //arrange + var serviceCollection = new ServiceCollection(); + + //act + serviceCollection.AddAutoMapper(cfg => { },new[] {typeof(ServiceLifetimeTests)}); + var serviceDescriptor = serviceCollection.FirstOrDefault(sd => sd.ServiceType == typeof(IMapper)); + + //assert + serviceDescriptor.ShouldNotBeNull(); + serviceDescriptor.Lifetime.ShouldBe(ServiceLifetime.Transient); + } + + [Fact] + public void AddAutoMapperExtensionDefaultDoubleDelegateWithProfileTypeCollection() + { + //arrange + var serviceCollection = new ServiceCollection(); + + //act + serviceCollection.AddAutoMapper((sp, cfg) => { },new[] {typeof(ServiceLifetimeTests)}); + var serviceDescriptor = serviceCollection.FirstOrDefault(sd => sd.ServiceType == typeof(IMapper)); + + //assert + serviceDescriptor.ShouldNotBeNull(); + serviceDescriptor.Lifetime.ShouldBe(ServiceLifetime.Transient); + } + + //Explicitly Singleton + [Fact] + public void AddAutoMapperExtensionSingletonWithAssemblySingleDelegateArgCollection() + { + //arrange + var serviceCollection = new ServiceCollection(); + + //act + serviceCollection.AddAutoMapper(cfg => { }, new List(), ServiceLifetime.Singleton); + var serviceDescriptor = serviceCollection.FirstOrDefault(sd => sd.ServiceType == typeof(IMapper)); + + //assert + serviceDescriptor.ShouldNotBeNull(); + serviceDescriptor.Lifetime.ShouldBe(ServiceLifetime.Singleton); + } + + [Fact] + public void AddAutoMapperExtensionSingletonWithAssemblyDoubleDelegateArgCollection() + { + //arrange + var serviceCollection = new ServiceCollection(); + + //act + serviceCollection.AddAutoMapper((sp, cfg) => { }, new List(), ServiceLifetime.Singleton); + var serviceDescriptor = serviceCollection.FirstOrDefault(sd => sd.ServiceType == typeof(IMapper)); + + //assert + serviceDescriptor.ShouldNotBeNull(); + serviceDescriptor.Lifetime.ShouldBe(ServiceLifetime.Singleton); + } + + [Fact] + public void AddAutoMapperExtensionSingletonWithAssemblyCollection() + { + //arrange + var serviceCollection = new ServiceCollection(); + + //act + serviceCollection.AddAutoMapper(new List(), ServiceLifetime.Singleton); + var serviceDescriptor = serviceCollection.FirstOrDefault(sd => sd.ServiceType == typeof(IMapper)); + + //assert + serviceDescriptor.ShouldNotBeNull(); + serviceDescriptor.Lifetime.ShouldBe(ServiceLifetime.Singleton); + } + + [Fact] + public void AddAutoMapperExtensionSingletonSingleDelegateWithProfileTypeCollection() + { + //arrange + var serviceCollection = new ServiceCollection(); + + //act + serviceCollection.AddAutoMapper(cfg => { },new[] {typeof(ServiceLifetimeTests)}, ServiceLifetime.Singleton); + var serviceDescriptor = serviceCollection.FirstOrDefault(sd => sd.ServiceType == typeof(IMapper)); + + //assert + serviceDescriptor.ShouldNotBeNull(); + serviceDescriptor.Lifetime.ShouldBe(ServiceLifetime.Singleton); + } + + [Fact] + public void AddAutoMapperExtensionSingletonDoubleDelegateWithProfileTypeCollection() + { + //arrange + var serviceCollection = new ServiceCollection(); + + //act + serviceCollection.AddAutoMapper((sp, cfg) => { },new[] {typeof(ServiceLifetimeTests)}, ServiceLifetime.Singleton); + var serviceDescriptor = serviceCollection.FirstOrDefault(sd => sd.ServiceType == typeof(IMapper)); + + //assert + serviceDescriptor.ShouldNotBeNull(); + serviceDescriptor.Lifetime.ShouldBe(ServiceLifetime.Singleton); + } + + //Explicitly Transient + [Fact] + public void AddAutoMapperExtensionTransientWithAssemblySingleDelegateArgCollection() + { + //arrange + var serviceCollection = new ServiceCollection(); + + //act + serviceCollection.AddAutoMapper(cfg => { }, new List(), ServiceLifetime.Transient); + var serviceDescriptor = serviceCollection.FirstOrDefault(sd => sd.ServiceType == typeof(IMapper)); + + //assert + serviceDescriptor.ShouldNotBeNull(); + serviceDescriptor.Lifetime.ShouldBe(ServiceLifetime.Transient); + } + + [Fact] + public void AddAutoMapperExtensionTransientWithAssemblyDoubleDelegateArgCollection() + { + //arrange + var serviceCollection = new ServiceCollection(); + + //act + serviceCollection.AddAutoMapper((sp, cfg) => { }, new List(), ServiceLifetime.Transient); + var serviceDescriptor = serviceCollection.FirstOrDefault(sd => sd.ServiceType == typeof(IMapper)); + + //assert + serviceDescriptor.ShouldNotBeNull(); + serviceDescriptor.Lifetime.ShouldBe(ServiceLifetime.Transient); + } + + [Fact] + public void AddAutoMapperExtensionTransientWithAssemblyCollection() + { + //arrange + var serviceCollection = new ServiceCollection(); + + //act + serviceCollection.AddAutoMapper(new List(), ServiceLifetime.Transient); + var serviceDescriptor = serviceCollection.FirstOrDefault(sd => sd.ServiceType == typeof(IMapper)); + + //assert + serviceDescriptor.ShouldNotBeNull(); + serviceDescriptor.Lifetime.ShouldBe(ServiceLifetime.Transient); + } + + [Fact] + public void AddAutoMapperExtensionTransientSingleDelegateWithProfileTypeCollection() + { + //arrange + var serviceCollection = new ServiceCollection(); + + //act + serviceCollection.AddAutoMapper(cfg => { },new[] {typeof(ServiceLifetimeTests)}, ServiceLifetime.Transient); + var serviceDescriptor = serviceCollection.FirstOrDefault(sd => sd.ServiceType == typeof(IMapper)); + + //assert + serviceDescriptor.ShouldNotBeNull(); + serviceDescriptor.Lifetime.ShouldBe(ServiceLifetime.Transient); + } + + [Fact] + public void AddAutoMapperExtensionTransientDoubleDelegateWithProfileTypeCollection() + { + //arrange + var serviceCollection = new ServiceCollection(); + + //act + serviceCollection.AddAutoMapper((sp, cfg) => { },new[] {typeof(ServiceLifetimeTests)}, ServiceLifetime.Transient); + var serviceDescriptor = serviceCollection.FirstOrDefault(sd => sd.ServiceType == typeof(IMapper)); + + //assert + serviceDescriptor.ShouldNotBeNull(); + serviceDescriptor.Lifetime.ShouldBe(ServiceLifetime.Transient); + } + + //Explicitly Scoped + [Fact] + public void AddAutoMapperExtensionScopedWithAssemblySingleDelegateArgCollection() + { + //arrange + var serviceCollection = new ServiceCollection(); + + //act + serviceCollection.AddAutoMapper(cfg => { }, new List(), ServiceLifetime.Scoped); + var serviceDescriptor = serviceCollection.FirstOrDefault(sd => sd.ServiceType == typeof(IMapper)); + + //assert + serviceDescriptor.ShouldNotBeNull(); + serviceDescriptor.Lifetime.ShouldBe(ServiceLifetime.Scoped); + } + + [Fact] + public void AddAutoMapperExtensionScopedWithAssemblyDoubleDelegateArgCollection() + { + //arrange + var serviceCollection = new ServiceCollection(); + + //act + serviceCollection.AddAutoMapper((sp, cfg) => { }, new List(), ServiceLifetime.Scoped); + var serviceDescriptor = serviceCollection.FirstOrDefault(sd => sd.ServiceType == typeof(IMapper)); + + //assert + serviceDescriptor.ShouldNotBeNull(); + serviceDescriptor.Lifetime.ShouldBe(ServiceLifetime.Scoped); + } + + [Fact] + public void AddAutoMapperExtensionScopedWithAssemblyCollection() + { + //arrange + var serviceCollection = new ServiceCollection(); + + //act + serviceCollection.AddAutoMapper(new List(), ServiceLifetime.Scoped); + var serviceDescriptor = serviceCollection.FirstOrDefault(sd => sd.ServiceType == typeof(IMapper)); + + //assert + serviceDescriptor.ShouldNotBeNull(); + serviceDescriptor.Lifetime.ShouldBe(ServiceLifetime.Scoped); + } + + [Fact] + public void AddAutoMapperExtensionScopedSingleDelegateWithProfileTypeCollection() + { + //arrange + var serviceCollection = new ServiceCollection(); + + //act + serviceCollection.AddAutoMapper(cfg => { },new[] {typeof(ServiceLifetimeTests)}, ServiceLifetime.Scoped); + var serviceDescriptor = serviceCollection.FirstOrDefault(sd => sd.ServiceType == typeof(IMapper)); + + //assert + serviceDescriptor.ShouldNotBeNull(); + serviceDescriptor.Lifetime.ShouldBe(ServiceLifetime.Scoped); + } + + [Fact] + public void AddAutoMapperExtensionScopedDoubleDelegateWithProfileTypeCollection() + { + //arrange + var serviceCollection = new ServiceCollection(); + + //act + serviceCollection.AddAutoMapper((sp, cfg) => { },new[] {typeof(ServiceLifetimeTests)}, ServiceLifetime.Scoped); + var serviceDescriptor = serviceCollection.FirstOrDefault(sd => sd.ServiceType == typeof(IMapper)); + + //assert + serviceDescriptor.ShouldNotBeNull(); + serviceDescriptor.Lifetime.ShouldBe(ServiceLifetime.Scoped); + } + + } +} \ No newline at end of file diff --git a/src/AutoMapper.DI.Tests/TypeResolutionTests.cs b/src/AutoMapper.DI.Tests/TypeResolutionTests.cs new file mode 100644 index 0000000000..78321eda42 --- /dev/null +++ b/src/AutoMapper.DI.Tests/TypeResolutionTests.cs @@ -0,0 +1,76 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace AutoMapper.Extensions.Microsoft.DependencyInjection.Tests +{ + using System; + using AutoMapper.Internal; + using Shouldly; + using Xunit; + + public class TypeResolutionTests + { + private readonly IServiceProvider _provider; + + public TypeResolutionTests() + { + IServiceCollection services = new ServiceCollection(); + services.AddAutoMapper(typeof(Source)); + _provider = services.BuildServiceProvider(); + } + + [Fact] + public void ShouldResolveConfiguration() + { + _provider.GetService().ShouldNotBeNull(); + } + + [Fact] + public void ShouldConfigureProfiles() + { + _provider.GetService().Internal().GetAllTypeMaps().Count.ShouldBe(4); + } + + [Fact] + public void ShouldResolveMapper() + { + _provider.GetService().ShouldNotBeNull(); + } + + [Fact] + public void ShouldResolveMappingAction() + { + _provider.GetService().ShouldNotBeNull(); + } + + [Fact] + public void ShouldResolveValueResolver() + { + _provider.GetService().ShouldNotBeNull(); + } + + [Fact] + public void ShouldResolveMemberValueResolver() + { + _provider.GetService().ShouldNotBeNull(); + } + + [Fact] + public void ShouldResolveTypeConverter() + { + _provider.GetService().ShouldNotBeNull(); + } + + [Fact] + public void ShouldResolveGenericTypeConverter() + { + _provider.GetService>().ShouldNotBeNull(); + _provider.GetService().Map>(ConsoleColor.Green).Value.ShouldBe(int.MaxValue); + } + + [Fact] + public void ShouldResolveValueConverter() + { + _provider.GetService().ShouldNotBeNull(); + } + } +} \ No newline at end of file diff --git a/src/AutoMapper/ApiCompat/PreBuild.ps1 b/src/AutoMapper/ApiCompat/PreBuild.ps1 index 88e35158b4..6bc6b2dbe9 100644 --- a/src/AutoMapper/ApiCompat/PreBuild.ps1 +++ b/src/AutoMapper/ApiCompat/PreBuild.ps1 @@ -10,4 +10,4 @@ if($versionNumbers[1] -eq "0" -AND $versionNumbers[2] -eq "0") $oldVersion = $oldVersion.ToString() +".0.0" echo $oldVersion & ..\..\nuget install AutoMapper -Version $oldVersion -OutputDirectory ..\LastMajorVersionBinary -& copy ..\LastMajorVersionBinary\AutoMapper.$oldVersion\lib\netstandard2.1\AutoMapper.dll ..\LastMajorVersionBinary +& copy ..\LastMajorVersionBinary\AutoMapper.$oldVersion\lib\net6.0\AutoMapper.dll ..\LastMajorVersionBinary diff --git a/src/AutoMapper/ApiCompatBaseline.txt b/src/AutoMapper/ApiCompatBaseline.txt index 90da3f7e58..4c9243b942 100644 --- a/src/AutoMapper/ApiCompatBaseline.txt +++ b/src/AutoMapper/ApiCompatBaseline.txt @@ -1,6 +1,29 @@ Compat issues with assembly AutoMapper: CannotChangeAttribute : Attribute 'System.AttributeUsageAttribute' on 'AutoMapper.AutoMapAttribute' changed from '[AttributeUsageAttribute(1036, AllowMultiple=true)]' in the contract to '[AttributeUsageAttribute(AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Struct, AllowMultiple=true)]' in the implementation. -InterfacesShouldHaveSameMembers : Interface member 'public void AutoMapper.Configuration.ICtorParamConfigurationExpression.ExplicitExpansion()' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.Object AutoMapper.IMappingOperationOptions.State' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.Object AutoMapper.IMappingOperationOptions.State.get()' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public void AutoMapper.IMappingOperationOptions.State.set(System.Object)' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.Collections.Generic.IReadOnlyCollection AutoMapper.IProfileConfiguration.AllPropertyMapActions.get()' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.Collections.Generic.IReadOnlyCollection> AutoMapper.IProfileConfiguration.AllPropertyMapActions.get()' is present in the contract but not in the implementation. +MembersMustExist : Member 'public System.Collections.Generic.IReadOnlyCollection> AutoMapper.IProfileConfiguration.AllPropertyMapActions.get()' does not exist in the implementation but it does exist in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public void AutoMapper.IProjectionMemberConfiguration.ExplicitExpansion()' is present in the contract but not in the implementation. +MembersMustExist : Member 'public void AutoMapper.IProjectionMemberConfiguration.ExplicitExpansion()' does not exist in the implementation but it does exist in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public void AutoMapper.IProjectionMemberConfiguration.ExplicitExpansion(System.Boolean)' is present in the implementation but not in the contract. +CannotSealType : Type 'AutoMapper.Mapper' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract. +CannotSealType : Type 'AutoMapper.MapperConfiguration' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract. +CannotSealType : Type 'AutoMapper.MapperConfigurationExpression' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract. +CannotSealType : Type 'AutoMapper.MappingOperationOptions' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract. +MembersMustExist : Member 'protected void AutoMapper.MappingOperationOptions.AfterMapAction.set(System.Action)' does not exist in the implementation but it does exist in the contract. +MembersMustExist : Member 'protected void AutoMapper.MappingOperationOptions.BeforeMapAction.set(System.Action)' does not exist in the implementation but it does exist in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public void AutoMapper.Configuration.ICtorParamConfigurationExpression.ExplicitExpansion(System.Boolean)' is present in the implementation but not in the contract. +CannotSealType : Type 'AutoMapper.Configuration.MappingExpression' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract. +CannotMakeMemberNonVirtual : Member 'protected void AutoMapper.Configuration.MappingExpression.IgnoreDestinationMember(System.Reflection.MemberInfo, System.Boolean)' is non-virtual in the implementation but is virtual in the contract. +CannotSealType : Type 'AutoMapper.Configuration.MemberConfigurationExpression' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract. +MembersMustExist : Member 'public void AutoMapper.Configuration.MemberConfigurationExpression.ExplicitExpansion()' does not exist in the implementation but it does exist in the contract. +CannotSealType : Type 'AutoMapper.Configuration.PathConfigurationExpression' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract. +MembersMustExist : Member 'protected System.Collections.Generic.List> AutoMapper.Configuration.PathConfigurationExpression.PathMapActions.get()' does not exist in the implementation but it does exist in the contract. +CannotSealType : Type 'AutoMapper.Configuration.SourceMappingExpression' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract. +CannotSealType : Type 'AutoMapper.Configuration.SourceMemberConfig' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract. CannotChangeAttribute : Attribute 'System.AttributeUsageAttribute' on 'AutoMapper.Configuration.Annotations.IgnoreAttribute' changed from '[AttributeUsageAttribute(384)]' in the contract to '[AttributeUsageAttribute(AttributeTargets.Field | AttributeTargets.Property)]' in the implementation. CannotChangeAttribute : Attribute 'System.AttributeUsageAttribute' on 'AutoMapper.Configuration.Annotations.MapAtRuntimeAttribute' changed from '[AttributeUsageAttribute(384)]' in the contract to '[AttributeUsageAttribute(AttributeTargets.Field | AttributeTargets.Property)]' in the implementation. CannotChangeAttribute : Attribute 'System.AttributeUsageAttribute' on 'AutoMapper.Configuration.Annotations.MappingOrderAttribute' changed from '[AttributeUsageAttribute(384)]' in the contract to '[AttributeUsageAttribute(AttributeTargets.Field | AttributeTargets.Property)]' in the implementation. @@ -9,5 +32,10 @@ CannotChangeAttribute : Attribute 'System.AttributeUsageAttribute' on 'AutoMappe CannotChangeAttribute : Attribute 'System.AttributeUsageAttribute' on 'AutoMapper.Configuration.Annotations.UseExistingValueAttribute' changed from '[AttributeUsageAttribute(384)]' in the contract to '[AttributeUsageAttribute(AttributeTargets.Field | AttributeTargets.Property)]' in the implementation. CannotChangeAttribute : Attribute 'System.AttributeUsageAttribute' on 'AutoMapper.Configuration.Annotations.ValueConverterAttribute' changed from '[AttributeUsageAttribute(384)]' in the contract to '[AttributeUsageAttribute(AttributeTargets.Field | AttributeTargets.Property)]' in the implementation. CannotChangeAttribute : Attribute 'System.AttributeUsageAttribute' on 'AutoMapper.Configuration.Annotations.ValueResolverAttribute' changed from '[AttributeUsageAttribute(384)]' in the contract to '[AttributeUsageAttribute(AttributeTargets.Field | AttributeTargets.Property)]' in the implementation. +CannotSealType : Type 'AutoMapper.Configuration.Conventions.PrePostfixName' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract. +CannotSealType : Type 'AutoMapper.Configuration.Conventions.ReplaceName' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract. TypeCannotChangeClassification : Type 'AutoMapper.Execution.TypeMapPlanBuilder' is a 'ref struct' in the implementation but is a 'struct' in the contract. -Total Issues: 11 +CannotSealType : Type 'AutoMapper.QueryableExtensions.MemberVisitor' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract. +CannotMakeMemberNonVirtual : Member 'protected System.Linq.Expressions.Expression AutoMapper.QueryableExtensions.MemberVisitor.VisitMember(System.Linq.Expressions.MemberExpression)' is non-virtual in the implementation but is virtual in the contract. +CannotSealType : Type 'AutoMapper.QueryableExtensions.Impl.MemberProjection' is actually (has the sealed modifier) sealed in the implementation but not sealed in the contract. +Total Issues: 39 diff --git a/src/AutoMapper/AutoMapper.csproj b/src/AutoMapper/AutoMapper.csproj index 41db321e10..1526d2b212 100644 --- a/src/AutoMapper/AutoMapper.csproj +++ b/src/AutoMapper/AutoMapper.csproj @@ -3,7 +3,7 @@ A convention-based object-object mapper. A convention-based object-object mapper. - netstandard2.1 + net6.0 true AutoMapper ..\..\AutoMapper.snk @@ -14,7 +14,7 @@ https://automapper.org README.md - preview + preview.0 v MIT true @@ -38,9 +38,9 @@ - - - + + + diff --git a/src/AutoMapper/AutoMapperMappingException.cs b/src/AutoMapper/AutoMapperMappingException.cs index 2be71cd072..e167e2927d 100644 --- a/src/AutoMapper/AutoMapperMappingException.cs +++ b/src/AutoMapper/AutoMapperMappingException.cs @@ -81,15 +81,13 @@ public DuplicateTypeMapConfigurationException(TypeMapConfigErrors[] errors) { Errors = errors; var builder = new StringBuilder(); - builder.AppendLine("The following type maps were found in multiple profiles:"); + builder.AppendLine("Duplicate CreateMap calls:"); foreach (var error in Errors) { builder.AppendLine($"{error.Types.SourceType.FullName} to {error.Types.DestinationType.FullName} defined in profiles:"); builder.AppendLine(string.Join(Environment.NewLine, error.ProfileNames)); } - builder.AppendLine("This can cause configuration collisions and inconsistent mapping."); - builder.AppendLine("Consolidate the CreateMap calls into one profile, or set the root Internal().AllowAdditiveTypeMapCreation configuration value to 'true'."); - + builder.AppendLine("This can cause configuration collisions and inconsistent mappings. Use a single CreateMap call per type pair."); Message = builder.ToString(); } diff --git a/src/AutoMapper/Configuration/ConfigurationValidator.cs b/src/AutoMapper/Configuration/ConfigurationValidator.cs index ecc5090767..9a9448bb01 100644 --- a/src/AutoMapper/Configuration/ConfigurationValidator.cs +++ b/src/AutoMapper/Configuration/ConfigurationValidator.cs @@ -13,19 +13,16 @@ private void Validate(ValidationContext context) } public void AssertConfigurationExpressionIsValid(IGlobalConfiguration config, IEnumerable typeMaps) { - if (!Expression.AllowAdditiveTypeMapCreation) + var duplicateTypeMapConfigs = Expression.Profiles.Append((Profile)Expression) + .SelectMany(p => p.TypeMapConfigs, (profile, typeMap) => (profile, typeMap)) + .GroupBy(x => x.typeMap.Types) + .Where(g => g.Count() > 1) + .Select(g => (TypePair : g.Key, ProfileNames : g.Select(tmc => tmc.profile.ProfileName).ToArray())) + .Select(g => new DuplicateTypeMapConfigurationException.TypeMapConfigErrors(g.TypePair, g.ProfileNames)) + .ToArray(); + if (duplicateTypeMapConfigs.Any()) { - var duplicateTypeMapConfigs = Expression.Profiles.Append((Profile)Expression) - .SelectMany(p => p.TypeMapConfigs, (profile, typeMap) => (profile, typeMap)) - .GroupBy(x => x.typeMap.Types) - .Where(g => g.Count() > 1) - .Select(g => (TypePair : g.Key, ProfileNames : g.Select(tmc => tmc.profile.ProfileName).ToArray())) - .Select(g => new DuplicateTypeMapConfigurationException.TypeMapConfigErrors(g.TypePair, g.ProfileNames)) - .ToArray(); - if (duplicateTypeMapConfigs.Any()) - { - throw new DuplicateTypeMapConfigurationException(duplicateTypeMapConfigs); - } + throw new DuplicateTypeMapConfigurationException(duplicateTypeMapConfigs); } AssertConfigurationIsValid(config, typeMaps); } @@ -119,7 +116,7 @@ private void CheckPropertyMaps(IGlobalConfiguration config, HashSet typ // when we don't know what the source type is, bail if (sourceType.IsGenericParameter || sourceType == typeof(object)) { - return; + continue; } DryRunTypeMap(config, typeMapsChecked, new(sourceType, memberMap.DestinationType), null, memberMap); } diff --git a/src/AutoMapper/Configuration/Conventions.cs b/src/AutoMapper/Configuration/Conventions.cs index 72c7734a15..191861472f 100644 --- a/src/AutoMapper/Configuration/Conventions.cs +++ b/src/AutoMapper/Configuration/Conventions.cs @@ -5,7 +5,7 @@ public interface ISourceToDestinationNameMapper void Merge(ISourceToDestinationNameMapper other); } [EditorBrowsable(EditorBrowsableState.Never)] -public class MemberConfiguration +public sealed class MemberConfiguration { NameSplitMember _nameSplitMember; public INamingConvention SourceNamingConvention { get; set; } = PascalCaseNamingConvention.Instance; @@ -66,7 +66,7 @@ public void Merge(MemberConfiguration other) } } } -public class PrePostfixName : ISourceToDestinationNameMapper +public sealed class PrePostfixName : ISourceToDestinationNameMapper { public List DestinationPrefixes { get; } = new(); public List DestinationPostfixes { get; } = new(); @@ -89,7 +89,7 @@ public void Merge(ISourceToDestinationNameMapper other) DestinationPostfixes.TryAdd(typedOther.DestinationPostfixes); } } -public class ReplaceName : ISourceToDestinationNameMapper +public sealed class ReplaceName : ISourceToDestinationNameMapper { public List MemberNameReplacers { get; } = new(); public MemberInfo GetSourceMember(TypeDetails sourceTypeDetails, Type destType, Type destMemberType, string nameToSearch) diff --git a/src/AutoMapper/Configuration/CtorParamConfigurationExpression.cs b/src/AutoMapper/Configuration/CtorParamConfigurationExpression.cs index 999e5c529f..4fa6876714 100644 --- a/src/AutoMapper/Configuration/CtorParamConfigurationExpression.cs +++ b/src/AutoMapper/Configuration/CtorParamConfigurationExpression.cs @@ -1,6 +1,4 @@ -using System.Runtime.CompilerServices; -namespace AutoMapper.Configuration; - +namespace AutoMapper.Configuration; public interface ICtorParamConfigurationExpression { /// @@ -11,7 +9,8 @@ public interface ICtorParamConfigurationExpression /// /// Ignore this member for LINQ projections unless explicitly expanded during projection /// - void ExplicitExpansion(); + /// Is explicitExpansion active + void ExplicitExpansion(bool value = true); } public interface ICtorParamConfigurationExpression : ICtorParamConfigurationExpression { @@ -35,7 +34,7 @@ public interface ICtorParameterConfiguration void Configure(TypeMap typeMap); } [EditorBrowsable(EditorBrowsableState.Never)] -public class CtorParamConfigurationExpression : ICtorParamConfigurationExpression, ICtorParameterConfiguration +public sealed class CtorParamConfigurationExpression : ICtorParamConfigurationExpression, ICtorParameterConfiguration { public string CtorParamName { get; } public Type SourceType { get; } @@ -63,7 +62,7 @@ public void MapFrom(string sourceMembersPath) _ctorParamActions.Add(cpm => cpm.MapFrom(sourceMembersPath, sourceMembers)); } - public void ExplicitExpansion() => _ctorParamActions.Add(cpm => cpm.ExplicitExpansion = true); + public void ExplicitExpansion(bool value) => _ctorParamActions.Add(cpm => cpm.ExplicitExpansion = value); public void Configure(TypeMap typeMap) { diff --git a/src/AutoMapper/Configuration/IMappingOperationOptions.cs b/src/AutoMapper/Configuration/IMappingOperationOptions.cs index 8f45ef8c14..471f92b8d8 100644 --- a/src/AutoMapper/Configuration/IMappingOperationOptions.cs +++ b/src/AutoMapper/Configuration/IMappingOperationOptions.cs @@ -1,5 +1,4 @@ namespace AutoMapper; - using StringDictionary = Dictionary; /// /// Options for a single map operation @@ -7,24 +6,26 @@ public interface IMappingOperationOptions { Func ServiceCtor { get; } - /// /// Construct services using this callback. Use this for child/nested containers /// /// void ConstructServicesUsing(Func constructor); - /// - /// Add context items to be accessed at map time inside an or + /// Add state to be accessed at map time inside an or . + /// Mutually exclusive with per Map call. /// - Dictionary Items { get; } - + object State { get; set; } + /// + /// Add context items to be accessed at map time inside an or . + /// Mutually exclusive with per Map call. + /// + StringDictionary Items { get; } /// /// Execute a custom function to the source and/or destination types before member mapping /// /// Callback for the source/destination types void BeforeMap(Action beforeFunction); - /// /// Execute a custom function to the source and/or destination types after member mapping /// @@ -38,21 +39,20 @@ public interface IMappingOperationOptions : IMappingOpera /// /// Callback for the source/destination types void BeforeMap(Action beforeFunction); - /// /// Execute a custom function to the source and/or destination types after member mapping /// /// Callback for the source/destination types void AfterMap(Action afterFunction); } -public class MappingOperationOptions : IMappingOperationOptions +public sealed class MappingOperationOptions : IMappingOperationOptions { - private StringDictionary _items; public MappingOperationOptions(Func serviceCtor) => ServiceCtor = serviceCtor; public Func ServiceCtor { get; private set; } - public Dictionary Items => _items ??= new StringDictionary(); - public Action BeforeMapAction { get; protected set; } - public Action AfterMapAction { get; protected set; } + public StringDictionary Items => (StringDictionary) (State ??= new StringDictionary()); + public object State { get; set; } + public Action BeforeMapAction { get; private set; } + public Action AfterMapAction { get; private set; } public void BeforeMap(Action beforeFunction) => BeforeMapAction = beforeFunction; public void AfterMap(Action afterFunction) => AfterMapAction = afterFunction; public void ConstructServicesUsing(Func constructor) diff --git a/src/AutoMapper/Configuration/IMemberConfigurationExpression.cs b/src/AutoMapper/Configuration/IMemberConfigurationExpression.cs index 5d52a81442..4f5f685249 100644 --- a/src/AutoMapper/Configuration/IMemberConfigurationExpression.cs +++ b/src/AutoMapper/Configuration/IMemberConfigurationExpression.cs @@ -290,7 +290,8 @@ public interface IProjectionMemberConfiguration /// /// Ignore this member for LINQ projections unless explicitly expanded during projection /// - void ExplicitExpansion(); + /// Is explicitExpansion active + void ExplicitExpansion(bool value = true); /// /// Apply a transformation function after any resolved destination member value with the given type /// diff --git a/src/AutoMapper/Configuration/MapperConfiguration.cs b/src/AutoMapper/Configuration/MapperConfiguration.cs index 69a6c37eea..0a1f7115e6 100644 --- a/src/AutoMapper/Configuration/MapperConfiguration.cs +++ b/src/AutoMapper/Configuration/MapperConfiguration.cs @@ -34,7 +34,7 @@ public interface IConfigurationProvider /// void CompileMappings(); } -public class MapperConfiguration : IGlobalConfiguration +public sealed class MapperConfiguration : IGlobalConfiguration { private static readonly MethodInfo MappingError = typeof(MapperConfiguration).GetMethod(nameof(GetMappingError)); private readonly IObjectMapper[] _mappers; @@ -343,6 +343,10 @@ private TypeMap GetTypeMap(TypePair initialTypes) var types = new TypePair(sourceType, destinationType); if (_resolvedMaps.TryGetValue(types, out typeMap)) { + if(typeMap == null) + { + continue; + } return typeMap; } typeMap = FindClosedGenericTypeMapFor(types); diff --git a/src/AutoMapper/Configuration/MapperConfigurationExpression.cs b/src/AutoMapper/Configuration/MapperConfigurationExpression.cs index 5c36306a8d..e4998d49da 100644 --- a/src/AutoMapper/Configuration/MapperConfigurationExpression.cs +++ b/src/AutoMapper/Configuration/MapperConfigurationExpression.cs @@ -85,7 +85,7 @@ public interface IMapperConfigurationExpression : IProfileExpression /// Profile configuration void CreateProfile(string profileName, Action config); } -public class MapperConfigurationExpression : Profile, IGlobalConfigurationExpression +public sealed class MapperConfigurationExpression : Profile, IGlobalConfigurationExpression { private readonly List _profiles = new(); private readonly List _validators = new(); @@ -101,13 +101,6 @@ public class MapperConfigurationExpression : Profile, IGlobalConfigurationExpres /// the validation callback void IGlobalConfigurationExpression.Validator(Validator validator) => _validators.Add(validator ?? throw new ArgumentNullException(nameof(validator))); - - /// - /// Allow the same map to exist in different profiles. - /// The default is to throw an exception, true means the maps are merged. - /// - bool IGlobalConfigurationExpression.AllowAdditiveTypeMapCreation { get; set; } - /// /// How many levels deep should AutoMapper try to inline the execution plan for child classes. /// See the docs for details. diff --git a/src/AutoMapper/Configuration/MappingExpression.cs b/src/AutoMapper/Configuration/MappingExpression.cs index 6bb950e180..427308c0f9 100644 --- a/src/AutoMapper/Configuration/MappingExpression.cs +++ b/src/AutoMapper/Configuration/MappingExpression.cs @@ -1,7 +1,8 @@ namespace AutoMapper.Configuration; -public class MappingExpression : MappingExpressionBase, IMappingExpression +public sealed class MappingExpression : MappingExpressionBase, IMappingExpression { - public MappingExpression(TypePair types, MemberList memberList) : base(memberList, types){} + public MappingExpression(TypePair types, MemberList memberList) : base(memberList, types){} + public MappingExpression(TypeMap typeMap) : this(typeMap.Types, typeMap.ConfiguredMemberList) => Projection = typeMap.Projection; public string[] IncludedMembersNames { get; internal set; } = Array.Empty(); public IMappingExpression ReverseMap() { diff --git a/src/AutoMapper/Configuration/MemberConfigurationExpression.cs b/src/AutoMapper/Configuration/MemberConfigurationExpression.cs index d3edb3d1b0..46006fe492 100644 --- a/src/AutoMapper/Configuration/MemberConfigurationExpression.cs +++ b/src/AutoMapper/Configuration/MemberConfigurationExpression.cs @@ -12,7 +12,7 @@ public class MemberConfigurationExpression : IMe { private MemberInfo[] _sourceMembers; private readonly Type _sourceType; - protected List> PropertyMapActions { get; } = new List>(); + protected List> PropertyMapActions { get; } = new(); public MemberConfigurationExpression(MemberInfo destinationMember, Type sourceType) { DestinationMember = destinationMember; @@ -44,15 +44,15 @@ public void MapFrom(IMemberValueResolver(Func mappingFunction) => - MapFromResult((src, dest, destMember, ctxt) => mappingFunction(src, dest)); + MapFromFunc((src, dest, destMember, ctxt) => mappingFunction(src, dest)); public void MapFrom(Func mappingFunction) => - MapFromResult((src, dest, destMember, ctxt) => mappingFunction(src, dest, destMember)); + MapFromFunc((src, dest, destMember, ctxt) => mappingFunction(src, dest, destMember)); public void MapFrom(Func mappingFunction) => - MapFromResult((src, dest, destMember, ctxt) => mappingFunction(src, dest, destMember, ctxt)); - private void MapFromResult(Expression> expr) => + MapFromFunc((src, dest, destMember, ctxt) => mappingFunction(src, dest, destMember, ctxt)); + private void MapFromFunc(Expression> expr) => SetResolver(new FuncResolver(expr)); - public void MapFrom(Expression> mapExpression) => MapFromUntyped(mapExpression); - internal void MapFromUntyped(LambdaExpression sourceExpression) + public void MapFrom(Expression> mapExpression) => MapFromExpression(mapExpression); + internal void MapFromExpression(LambdaExpression sourceExpression) { SourceExpression = sourceExpression; PropertyMapActions.Add(pm => pm.MapFrom(sourceExpression)); @@ -80,7 +80,7 @@ private void PreConditionCore(Expression pm.PreCondition = expr); public void AddTransform(Expression> transformer) => PropertyMapActions.Add(pm => pm.AddValueTransformation(new ValueTransformerConfiguration(pm.DestinationType, transformer))); - public void ExplicitExpansion() => PropertyMapActions.Add(pm => pm.ExplicitExpansion = true); + public void ExplicitExpansion(bool value) => PropertyMapActions.Add(pm => pm.ExplicitExpansion = value); public void Ignore() => Ignore(ignorePaths: true); public void Ignore(bool ignorePaths) { @@ -166,7 +166,7 @@ public IPropertyMapConfiguration Reverse() } public void DoNotUseDestinationValue() => SetUseDestinationValue(false); } -public class MemberConfigurationExpression : MemberConfigurationExpression, IMemberConfigurationExpression +public sealed class MemberConfigurationExpression : MemberConfigurationExpression, IMemberConfigurationExpression { public MemberConfigurationExpression(MemberInfo destinationMember, Type sourceType) : base(destinationMember, sourceType){} public void MapFrom(Type valueResolverType) => MapFromCore(new(valueResolverType, valueResolverType.GetGenericInterface(typeof(IValueResolver<,,>)))); diff --git a/src/AutoMapper/Configuration/PathConfigurationExpression.cs b/src/AutoMapper/Configuration/PathConfigurationExpression.cs index ecd2e46783..e20514ec46 100644 --- a/src/AutoMapper/Configuration/PathConfigurationExpression.cs +++ b/src/AutoMapper/Configuration/PathConfigurationExpression.cs @@ -21,11 +21,11 @@ public interface IPathConfigurationExpression void Condition(Func, bool> condition); } public readonly record struct ConditionParameters(TSource Source, TDestination Destination, TMember SourceMember, TMember DestinationMember, ResolutionContext Context); -public class PathConfigurationExpression : IPathConfigurationExpression, IPropertyMapConfiguration +public sealed class PathConfigurationExpression : IPathConfigurationExpression, IPropertyMapConfiguration { private readonly LambdaExpression _destinationExpression; private LambdaExpression _sourceExpression; - protected List> PathMapActions { get; } = new List>(); + List> PathMapActions { get; } = new(); public PathConfigurationExpression(LambdaExpression destinationExpression, Stack chain) { _destinationExpression = destinationExpression; @@ -62,7 +62,7 @@ internal static IPropertyMapConfiguration Create(LambdaExpression destination, L if (reversed.MemberPath.Length == 1) { var reversedMemberExpression = new MemberConfigurationExpression(reversed.DestinationMember, typeof(TSource)); - reversedMemberExpression.MapFromUntyped(source); + reversedMemberExpression.MapFromExpression(source); return reversedMemberExpression; } reversed.MapFromUntyped(source); diff --git a/src/AutoMapper/Configuration/Profile.cs b/src/AutoMapper/Configuration/Profile.cs index 499b7fb33e..30b1f51899 100644 --- a/src/AutoMapper/Configuration/Profile.cs +++ b/src/AutoMapper/Configuration/Profile.cs @@ -10,7 +10,7 @@ public interface IProfileConfiguration bool? AllowNullCollections { get; } bool? EnableNullPropagationForQueryMapping { get; } IReadOnlyCollection> AllTypeMapActions { get; } - IReadOnlyCollection> AllPropertyMapActions { get; } + IReadOnlyCollection AllPropertyMapActions { get; } /// /// Source extension methods included for search @@ -60,7 +60,7 @@ public class Profile : IProfileExpressionInternal, IProfileConfiguration private readonly PrePostfixName _prePostfixName = new(); private ReplaceName _replaceName; private readonly MemberConfiguration _memberConfiguration; - private List> _allPropertyMapActions; + private List _allPropertyMapActions; private List> _allTypeMapActions; private List _globalIgnores; private List _openTypeMapConfigs; @@ -81,7 +81,7 @@ protected Profile() bool? IProfileExpressionInternal.FieldMappingEnabled { get; set; } bool? IProfileConfiguration.FieldMappingEnabled => this.Internal().FieldMappingEnabled; bool? IProfileConfiguration.EnableNullPropagationForQueryMapping => this.Internal().EnableNullPropagationForQueryMapping; - IReadOnlyCollection> IProfileConfiguration.AllPropertyMapActions + IReadOnlyCollection IProfileConfiguration.AllPropertyMapActions => _allPropertyMapActions.NullCheck(); IReadOnlyCollection> IProfileConfiguration.AllTypeMapActions => _allTypeMapActions.NullCheck(); IReadOnlyCollection IProfileConfiguration.GlobalIgnores => _globalIgnores.NullCheck(); @@ -122,10 +122,7 @@ void IProfileExpressionInternal.ForAllMaps(Action c void IProfileExpressionInternal.ForAllPropertyMaps(Func condition, Action configuration) { _allPropertyMapActions ??= new(); - _allPropertyMapActions.Add((pm, cfg) => - { - if (condition(pm)) configuration(pm, cfg); - }); + _allPropertyMapActions.Add(new(condition, configuration)); } public IProjectionExpression CreateProjection() => CreateProjection(MemberList.Destination); @@ -180,6 +177,7 @@ public void IncludeSourceExtensionMethods(Type type) { _sourceExtensionMethods ??= new(); _sourceExtensionMethods.AddRange( - type.GetMethods(TypeExtensions.StaticFlags).Where(m => m.Has() && m.GetParameters().Length == 1)); + type.GetMethods(Internal.TypeExtensions.StaticFlags).Where(m => m.Has() && m.GetParameters().Length == 1)); } -} \ No newline at end of file +} +public readonly record struct PropertyMapAction(Func Condition, Action Action); \ No newline at end of file diff --git a/src/AutoMapper/Configuration/SourceMappingExpression.cs b/src/AutoMapper/Configuration/SourceMappingExpression.cs index 80d63ed664..e08b54fd42 100644 --- a/src/AutoMapper/Configuration/SourceMappingExpression.cs +++ b/src/AutoMapper/Configuration/SourceMappingExpression.cs @@ -15,7 +15,7 @@ public interface ISourceMemberConfigurationExpression /// void DoNotValidate(); } -public class SourceMappingExpression : ISourceMemberConfigurationExpression, ISourceMemberConfiguration +public sealed class SourceMappingExpression : ISourceMemberConfigurationExpression, ISourceMemberConfiguration { private readonly MemberInfo _sourceMember; private readonly List> _sourceMemberActions = new List>(); @@ -37,7 +37,7 @@ public void Configure(TypeMap typeMap) /// /// Contains member configuration relating to source members /// -public class SourceMemberConfig +public sealed class SourceMemberConfig { private bool _ignored; diff --git a/src/AutoMapper/ConstructorMap.cs b/src/AutoMapper/ConstructorMap.cs index c79e445b25..53d5d711ee 100644 --- a/src/AutoMapper/ConstructorMap.cs +++ b/src/AutoMapper/ConstructorMap.cs @@ -1,6 +1,6 @@ namespace AutoMapper; [EditorBrowsable(EditorBrowsableState.Never)] -public class ConstructorMap +public sealed class ConstructorMap { private bool? _canResolve; private readonly List _ctorParams = new(); @@ -17,11 +17,11 @@ public bool CanResolve get => _canResolve ??= ParametersCanResolve(); set => _canResolve = value; } - private bool ParametersCanResolve() + bool ParametersCanResolve() { foreach (var param in _ctorParams) { - if (!param.CanResolveValue) + if (!param.IsMapped) { return false; } @@ -42,35 +42,31 @@ public ConstructorParameterMap this[string name] return null; } } - public void AddParameter(ParameterInfo parameter, IEnumerable sourceMembers, TypeMap typeMap) => - _ctorParams.Add(new(typeMap, parameter, sourceMembers.ToArray())); - public bool ApplyIncludedMember(IncludedMember includedMember) + public void AddParameter(ParameterInfo parameter, IEnumerable sourceMembers, TypeMap typeMap) => _ctorParams.Add(new(typeMap, parameter, sourceMembers.ToArray())); + public bool ApplyMap(TypeMap typeMap, IncludedMember includedMember = null) { - var includedMap = includedMember.TypeMap.ConstructorMap; - if (CanResolve || includedMap?.Ctor != Ctor) + var constructorMap = typeMap.ConstructorMap; + if(constructorMap == null) { return false; } - bool canResolve = false; - var includedParams = includedMap._ctorParams; - for(int index = 0; index < includedParams.Count; index++) + bool applied = false; + foreach(var parameterMap in _ctorParams) { - var includedParam = includedParams[index]; - if (!includedParam.CanResolveValue || _ctorParams[index].CanResolveValue) + var inheritedParameterMap = constructorMap[parameterMap.DestinationName]; + if(inheritedParameterMap is not { IsMapped: true, DestinationType: var type } || type != parameterMap.DestinationType || !parameterMap.ApplyMap(inheritedParameterMap, includedMember)) { continue; } - canResolve = true; + applied = true; _canResolve = null; - _ctorParams[index] = new(includedParam, includedMember); } - return canResolve; + return applied; } } [EditorBrowsable(EditorBrowsableState.Never)] public class ConstructorParameterMap : MemberMap { - private Type _sourceType; public ConstructorParameterMap(TypeMap typeMap, ParameterInfo parameter, MemberInfo[] sourceMembers) : base(typeMap) { Parameter = parameter; @@ -83,17 +79,26 @@ public ConstructorParameterMap(TypeMap typeMap, ParameterInfo parameter, MemberI SourceMembers = Array.Empty(); } } - public ConstructorParameterMap(ConstructorParameterMap parameterMap, IncludedMember includedMember) : - this(includedMember.TypeMap, parameterMap.Parameter, parameterMap.SourceMembers) => - IncludedMember = includedMember.Chain(parameterMap.IncludedMember); public ParameterInfo Parameter { get; } - public override Type SourceType => _sourceType ??= GetSourceType(); public override Type DestinationType => Parameter.ParameterType; - public override IncludedMember IncludedMember { get; } + public override IncludedMember IncludedMember { get; protected set; } public override MemberInfo[] SourceMembers { get; set; } public override string DestinationName => Parameter.Name; public Expression DefaultValue(IGlobalConfiguration configuration) => Parameter.IsOptional ? Parameter.GetDefaultValue(configuration) : configuration.Default(DestinationType); - public override string ToString() => $"{Constructor}, parameter {DestinationName}"; - private MemberInfo Constructor => Parameter.Member; + public override string ToString() => $"{Parameter.Member}, parameter {DestinationName}"; + public bool ApplyMap(ConstructorParameterMap inheritedParameterMap, IncludedMember includedMember) + { + if(includedMember != null && IsMapped) + { + return false; + } + ExplicitExpansion ??= inheritedParameterMap.ExplicitExpansion; + if(ApplyInheritedMap(inheritedParameterMap)) + { + IncludedMember = includedMember?.Chain(inheritedParameterMap.IncludedMember); + return true; + } + return false; + } public override bool? ExplicitExpansion { get; set; } } \ No newline at end of file diff --git a/src/AutoMapper/Execution/ExpressionBuilder.cs b/src/AutoMapper/Execution/ExpressionBuilder.cs index 7ffd081c30..46d55c34a8 100644 --- a/src/AutoMapper/Execution/ExpressionBuilder.cs +++ b/src/AutoMapper/Execution/ExpressionBuilder.cs @@ -68,6 +68,7 @@ public static Expression MapExpression(this IGlobalConfiguration configuration, bool nullCheck; if (typeMap != null) { + typeMap.CheckProjection(); var allowNull = memberMap?.AllowNull; nullCheck = !typeMap.HasTypeConverter && (destination.NodeType != ExpressionType.Default || (allowNull.HasValue && allowNull != profileMap.AllowNullDestinationValues)); @@ -87,14 +88,13 @@ public static Expression MapExpression(this IGlobalConfiguration configuration, { mapExpression = mapper.MapExpression(configuration, profileMap, memberMap, source, destination); nullCheck = mapExpression != source; - mapExpression = ToType(mapExpression, typePair.DestinationType); } else { nullCheck = true; } } - mapExpression ??= ContextMap(typePair, source, destination, memberMap); + mapExpression = mapExpression == null ? ContextMap(typePair, source, destination, memberMap) : ToType(mapExpression, typePair.DestinationType); return nullCheck ? configuration.NullCheckSource(profileMap, source, destination, mapExpression, memberMap) : mapExpression; } public static Expression NullCheckSource(this IGlobalConfiguration configuration, ProfileMap profileMap, Expression source, Expression destination, @@ -362,10 +362,10 @@ private static Expression Replace(this ParameterReplaceVisitor visitor, LambdaEx return newLambda; } public static Expression Replace(this Expression exp, Expression old, Expression replace) => new ReplaceVisitor().Replace(exp, old, replace); - public static Expression NullCheck(this Expression expression, IGlobalConfiguration configuration, MemberMap memberMap = null, Expression defaultValue = null) + public static Expression NullCheck(this Expression expression, IGlobalConfiguration configuration, MemberMap memberMap = null, Expression defaultValue = null, IncludedMember includedMember = null) { var chain = expression.GetChain(); - var min = memberMap?.IncludedMember == null ? 2 : 1; + var min = (includedMember ?? memberMap?.IncludedMember) == null ? 2 : 1; if (chain.Count < min || chain.Peek().Target is not ParameterExpression parameter) { return expression; diff --git a/src/AutoMapper/Execution/ObjectFactory.cs b/src/AutoMapper/Execution/ObjectFactory.cs index 1f8ce014d2..d66f8d450b 100644 --- a/src/AutoMapper/Execution/ObjectFactory.cs +++ b/src/AutoMapper/Execution/ObjectFactory.cs @@ -19,7 +19,7 @@ private static Func GenerateConstructor(Type type) => }; private static Expression CallConstructor(Type type, IGlobalConfiguration configuration) { - var defaultCtor = type.GetConstructor(TypeExtensions.InstanceFlags, null, Type.EmptyTypes, null); + var defaultCtor = type.GetConstructor(Internal.TypeExtensions.InstanceFlags, null, Type.EmptyTypes, null); if (defaultCtor != null) { return New(defaultCtor); diff --git a/src/AutoMapper/Execution/TypeMapPlanBuilder.cs b/src/AutoMapper/Execution/TypeMapPlanBuilder.cs index f3334bc478..6cfc920c9b 100644 --- a/src/AutoMapper/Execution/TypeMapPlanBuilder.cs +++ b/src/AutoMapper/Execution/TypeMapPlanBuilder.cs @@ -109,7 +109,7 @@ private static void CheckForCycles(IGlobalConfiguration configuration, TypeMap t } if (typeMapsPath.Contains(memberTypeMap)) { - if (memberTypeMap.SourceType.IsValueType) + if (memberTypeMap.SourceType.IsValueType || memberTypeMap.DestinationType.IsValueType) { if (memberTypeMap.MaxDepth == 0) { diff --git a/src/AutoMapper/Internal/InternalApi.cs b/src/AutoMapper/Internal/InternalApi.cs index 5be71f4b05..fe0362d948 100644 --- a/src/AutoMapper/Internal/InternalApi.cs +++ b/src/AutoMapper/Internal/InternalApi.cs @@ -32,11 +32,6 @@ public interface IGlobalConfigurationExpression : IMapperConfigurationExpression /// the validation callback void Validator(Validator validator); /// - /// Allow the same map to exist in different profiles. - /// The default is to throw an exception, true means the maps are merged. - /// - bool AllowAdditiveTypeMapCreation { get; set; } - /// /// How many levels deep should AutoMapper try to inline the execution plan for child classes. /// See the docs for details. /// diff --git a/src/AutoMapper/Internal/TypeDetails.cs b/src/AutoMapper/Internal/TypeDetails.cs index b69b2f226c..c3c07bbc90 100644 --- a/src/AutoMapper/Internal/TypeDetails.cs +++ b/src/AutoMapper/Internal/TypeDetails.cs @@ -4,7 +4,7 @@ namespace AutoMapper.Internal; /// [DebuggerDisplay("{Type}")] [EditorBrowsable(EditorBrowsableState.Never)] -public class TypeDetails +public sealed class TypeDetails { private Dictionary _nameToMember; private ConstructorParameters[] _constructors; @@ -88,7 +88,7 @@ where targetType.IsInterface && targetType.ContainsGenericParameters select new GenericMethod(method, genericInterface)); } } - class GenericMethod : MemberInfo + sealed class GenericMethod : MemberInfo { readonly MethodInfo _genericMethod; readonly Type _genericInterface; diff --git a/src/AutoMapper/Mapper.cs b/src/AutoMapper/Mapper.cs index 960f4f78f5..2e3c42f997 100644 --- a/src/AutoMapper/Mapper.cs +++ b/src/AutoMapper/Mapper.cs @@ -15,7 +15,7 @@ public interface IMapperBase /// /// Execute a mapping from the source object to a new destination object. /// - /// Source type to use, regardless of the runtime type + /// Source type to use /// Destination type to create /// Source object to map from /// Mapped destination object @@ -27,7 +27,7 @@ public interface IMapperBase /// Destination type /// Source object to map from /// Destination object to map into - /// The mapped destination object, same instance as the object + /// The mapped destination object TDestination Map(TSource source, TDestination destination); /// /// Execute a mapping from the source object to a new destination object with explicit objects @@ -44,7 +44,7 @@ public interface IMapperBase /// Destination object to map into /// Source type to use /// Destination type to use - /// Mapped destination object, same instance as the object + /// Mapped destination object object Map(object source, object destination, Type sourceType, Type destinationType); } public interface IMapper : IMapperBase @@ -74,7 +74,7 @@ public interface IMapper : IMapperBase /// Source object to map from /// Destination object to map into /// Mapping options - /// The mapped destination object, same instance as the object + /// The mapped destination object TDestination Map(TSource source, TDestination destination, Action> opts); /// /// Execute a mapping from the source object to a new destination object with explicit objects and supplied mapping options. @@ -93,7 +93,7 @@ public interface IMapper : IMapperBase /// Source type to use /// Destination type to use /// Mapping options - /// Mapped destination object, same instance as the object + /// Mapped destination object object Map(object source, object destination, Type sourceType, Type destinationType, Action opts); /// /// Configuration provider for performing maps @@ -137,15 +137,17 @@ internal interface IInternalRuntimeMapper : IRuntimeMapper ResolutionContext DefaultContext { get; } Factory ServiceCtor { get; } } -public class Mapper : IMapper, IInternalRuntimeMapper +public sealed class Mapper : IMapper, IInternalRuntimeMapper { private readonly IGlobalConfiguration _configuration; private readonly Factory _serviceCtor; public Mapper(IConfigurationProvider configuration) : this(configuration, configuration.Internal().ServiceCtor) { } public Mapper(IConfigurationProvider configuration, Factory serviceCtor) { - _configuration = (IGlobalConfiguration)configuration ?? throw new ArgumentNullException(nameof(configuration)); - _serviceCtor = serviceCtor ?? throw new NullReferenceException(nameof(serviceCtor)); + ArgumentNullException.ThrowIfNull(configuration); + ArgumentNullException.ThrowIfNull(serviceCtor); + _configuration = (IGlobalConfiguration)configuration; + _serviceCtor = serviceCtor; DefaultContext = new(this); } internal ResolutionContext DefaultContext { get; } @@ -164,10 +166,23 @@ public TDestination Map(TSource source, TDestination dest public object Map(object source, Type sourceType, Type destinationType) => Map(source, null, sourceType, destinationType); public object Map(object source, Type sourceType, Type destinationType, Action opts) => Map(source, null, sourceType, destinationType, opts); - public object Map(object source, object destination, Type sourceType, Type destinationType) => - MapCore(source, destination, DefaultContext, sourceType, destinationType); - public object Map(object source, object destination, Type sourceType, Type destinationType, Action opts) => - MapWithOptions(source, destination, opts, sourceType, destinationType); + public object Map(object source, object destination, Type sourceType, Type destinationType) + { + CheckDestination(destination, destinationType); + return MapCore(source, destination, DefaultContext, sourceType, destinationType); + } + private static void CheckDestination(object destination, Type destinationType) + { + if (destination == null) + { + ArgumentNullException.ThrowIfNull(destinationType); + } + } + public object Map(object source, object destination, Type sourceType, Type destinationType, Action opts) + { + CheckDestination(destination, destinationType); + return MapWithOptions(source, destination, opts, sourceType, destinationType); + } public IQueryable ProjectTo(IQueryable source, object parameters, params Expression>[] membersToExpand) => source.ProjectTo(ConfigurationProvider, parameters, membersToExpand); public IQueryable ProjectTo(IQueryable source, IDictionary parameters, params string[] membersToExpand) diff --git a/src/AutoMapper/Mappers/CollectionMapper.cs b/src/AutoMapper/Mappers/CollectionMapper.cs index 9122e6d01a..d53a9fb585 100644 --- a/src/AutoMapper/Mappers/CollectionMapper.cs +++ b/src/AutoMapper/Mappers/CollectionMapper.cs @@ -49,11 +49,9 @@ Expression MapCollectionCore(Expression destExpression) var sourceElementType = GetEnumerableElementType(sourceType); if (destinationCollectionType == null || (sourceType == sourceElementType && destinationType == destinationElementType)) { - if (destinationType.IsAssignableFrom(sourceType)) - { - return sourceExpression; - } - throw new NotSupportedException($"Unknown collection. Consider a custom type converter from {sourceType} to {destinationType}."); + return destinationType.IsAssignableFrom(sourceType) ? + sourceExpression : + Throw(Constant(new NotSupportedException($"Unknown collection. Consider a custom type converter from {sourceType} to {destinationType}.")), destinationType); } var itemParam = Parameter(sourceElementType, "item"); var itemExpr = configuration.MapExpression(profileMap, new TypePair(sourceElementType, destinationElementType), itemParam); @@ -109,7 +107,8 @@ void GetDestinationType() return; } destinationElementType = GetEnumerableElementType(destinationType); - destinationCollectionType = typeof(ICollection<>).MakeGenericType(destinationElementType); + destinationCollectionType = destinationType.IsGenericType(typeof(IReadOnlySet<>)) ? typeof(HashSet<>) : typeof(ICollection<>); + destinationCollectionType = destinationCollectionType.MakeGenericType(destinationElementType); destExpression = Convert(mustUseDestination ? destExpression : Null, destinationCollectionType); addMethod = destinationCollectionType.GetMethod("Add"); } diff --git a/src/AutoMapper/MemberMap.cs b/src/AutoMapper/MemberMap.cs index c1a5a3dcf8..2944a415ba 100644 --- a/src/AutoMapper/MemberMap.cs +++ b/src/AutoMapper/MemberMap.cs @@ -5,6 +5,7 @@ namespace AutoMapper; [EditorBrowsable(EditorBrowsableState.Never)] public class MemberMap : IValueResolver { + private protected Type _sourceType; protected MemberMap(TypeMap typeMap = null) => TypeMap = typeMap; internal static readonly MemberMap Instance = new(); public TypeMap TypeMap { get; protected set; } @@ -14,16 +15,17 @@ public class MemberMap : IValueResolver public void SetResolver(IValueResolver resolver) { Resolver = resolver; + _sourceType = resolver.ResolvedType; Ignored = false; } - public virtual Type SourceType => default; + public virtual Type SourceType => _sourceType ??= GetSourceType(); public virtual MemberInfo[] SourceMembers { get => Array.Empty(); set { } } - public virtual IncludedMember IncludedMember => null; + public virtual IncludedMember IncludedMember { get => default; protected set { } } public virtual string DestinationName => default; public virtual Type DestinationType { get => default; protected set { } } public virtual TypePair Types() => new(SourceType, DestinationType); public bool CanResolveValue => !Ignored && Resolver != null; - public bool IsMapped => Ignored || CanResolveValue; + public bool IsMapped => Ignored || Resolver != null; public virtual bool Ignored { get => default; set { } } public virtual bool? ExplicitExpansion { get => default; set { } } public virtual bool Inline { get; set; } = true; @@ -61,6 +63,31 @@ public void MapByConvention(MemberInfo[] sourceMembers) SourceMembers = sourceMembers; Resolver = this; } + protected bool ApplyInheritedMap(MemberMap inheritedMap) + { + if(Ignored || IsResolveConfigured) + { + return false; + } + if(inheritedMap.Ignored) + { + Ignored = true; + return true; + } + if(inheritedMap.IsResolveConfigured) + { + _sourceType = inheritedMap._sourceType; + Resolver = inheritedMap.Resolver.CloseGenerics(TypeMap); + return true; + } + if(Resolver == null) + { + _sourceType = inheritedMap._sourceType; + MapByConvention(inheritedMap.SourceMembers); + return true; + } + return false; + } Expression IValueResolver.GetExpression(IGlobalConfiguration configuration, MemberMap memberMap, Expression source, Expression destination, Expression destinationMember) => ChainSourceMembers(configuration, source, destinationMember); MemberInfo IValueResolver.GetSourceMember(MemberMap memberMap) => SourceMembers[0]; diff --git a/src/AutoMapper/PathMap.cs b/src/AutoMapper/PathMap.cs index 7c9d29ecae..fef27fdd07 100644 --- a/src/AutoMapper/PathMap.cs +++ b/src/AutoMapper/PathMap.cs @@ -1,7 +1,7 @@ namespace AutoMapper; [DebuggerDisplay("{DestinationExpression}")] [EditorBrowsable(EditorBrowsableState.Never)] -public class PathMap : MemberMap +public sealed class PathMap : MemberMap { public PathMap(PathMap pathMap, TypeMap typeMap, IncludedMember includedMember) : this(pathMap.DestinationExpression, pathMap.MemberPath, typeMap) { @@ -22,6 +22,6 @@ public PathMap(LambdaExpression destinationExpression, MemberPath memberPath, Ty public override string DestinationName => MemberPath.ToString(); public override bool CanBeSet => ReflectionHelper.CanBeSet(MemberPath.Last); public override bool Ignored { get; set; } - public override IncludedMember IncludedMember { get; } + public override IncludedMember IncludedMember { get; protected set; } public override LambdaExpression Condition { get; set; } } \ No newline at end of file diff --git a/src/AutoMapper/ProfileMap.cs b/src/AutoMapper/ProfileMap.cs index ddf92c71be..5b0ae92417 100644 --- a/src/AutoMapper/ProfileMap.cs +++ b/src/AutoMapper/ProfileMap.cs @@ -3,7 +3,7 @@ namespace AutoMapper; [DebuggerDisplay("{Name}")] [EditorBrowsable(EditorBrowsableState.Never)] -public class ProfileMap +public sealed class ProfileMap { private static readonly HashSet EmptyHashSet = new(); private TypeMapConfiguration[] _typeMapConfigs; @@ -88,7 +88,7 @@ internal void Clear() public Func ShouldMapProperty { get; } public Func ShouldMapMethod { get; } public Func ShouldUseConstructor { get; } - public IEnumerable> AllPropertyMapActions { get; } + public IEnumerable AllPropertyMapActions { get; } public IEnumerable> AllTypeMapActions { get; } public HashSet GlobalIgnores { get; } public MemberConfiguration MemberConfiguration { get; } @@ -185,16 +185,20 @@ private void Configure(TypeMap typeMap, IGlobalConfiguration configuration) } foreach (var action in AllTypeMapActions) { - var expression = new MappingExpression(typeMap.Types, typeMap.ConfiguredMemberList); + var expression = new MappingExpression(typeMap); action(typeMap, expression); expression.Configure(typeMap, configuration.SourceMembers); } foreach (var action in AllPropertyMapActions) { foreach (var propertyMap in typeMap.PropertyMaps) - { + { + if (!action.Condition(propertyMap)) + { + continue; + } var memberExpression = new MemberConfigurationExpression(propertyMap.DestinationMember, typeMap.SourceType); - action(propertyMap, memberExpression); + action.Action(propertyMap, memberExpression); memberExpression.Configure(typeMap); } } @@ -238,7 +242,7 @@ private void ApplyMemberMaps(TypeMap currentMap, IGlobalConfiguration configurat ApplyMemberMaps(includedMap, configuration); foreach (var inheritedIncludedMember in includedMap.IncludedMembersTypeMaps) { - currentMap.AddMemberMap(includedMember.Chain(inheritedIncludedMember)); + currentMap.AddMemberMap(includedMember.Chain(inheritedIncludedMember, configuration)); } } } @@ -271,13 +275,13 @@ private IncludedMember(TypeMap typeMap, LambdaExpression memberExpression, Param Variable = variable; ProjectToCustomSource = projectToCustomSource; } - public IncludedMember Chain(IncludedMember other) + public IncludedMember Chain(IncludedMember other, IGlobalConfiguration configuration = null) { if (other == null) { return this; } - return new(other.TypeMap, Chain(other.MemberExpression), other.Variable, Chain(MemberExpression, other.MemberExpression)); + return new(other.TypeMap, Chain(other.MemberExpression, other, configuration), other.Variable, Chain(MemberExpression, other.MemberExpression)); } public static LambdaExpression Chain(LambdaExpression customSource, LambdaExpression lambda) => Lambda(lambda.ReplaceParameters(customSource.Body), customSource.Parameters); @@ -285,7 +289,9 @@ public static LambdaExpression Chain(LambdaExpression customSource, LambdaExpres public LambdaExpression MemberExpression { get; } public ParameterExpression Variable { get; } public LambdaExpression ProjectToCustomSource { get; } - public LambdaExpression Chain(LambdaExpression lambda) => Lambda(lambda.ReplaceParameters(Variable), lambda.Parameters); + public LambdaExpression Chain(LambdaExpression lambda) => Chain(lambda, null, null); + public LambdaExpression Chain(LambdaExpression lambda, IncludedMember includedMember, IGlobalConfiguration configuration) => + Lambda(lambda.ReplaceParameters(Variable).NullCheck(configuration, includedMember: includedMember), lambda.Parameters); public bool Equals(IncludedMember other) => TypeMap == other?.TypeMap; public override int GetHashCode() => TypeMap.GetHashCode(); } \ No newline at end of file diff --git a/src/AutoMapper/PropertyMap.cs b/src/AutoMapper/PropertyMap.cs index 41639057ff..6ac0ee9598 100644 --- a/src/AutoMapper/PropertyMap.cs +++ b/src/AutoMapper/PropertyMap.cs @@ -2,10 +2,9 @@ namespace AutoMapper; [DebuggerDisplay("{DestinationMember.Name}")] [EditorBrowsable(EditorBrowsableState.Never)] -public class PropertyMap : MemberMap +public sealed class PropertyMap : MemberMap { private MemberMapDetails _details; - private Type _sourceType; public PropertyMap(MemberInfo destinationMember, Type destinationMemberType, TypeMap typeMap) : base(typeMap) { DestinationMember = destinationMember; @@ -32,36 +31,14 @@ public PropertyMap(PropertyMap includedMemberMap, TypeMap typeMap, IncludedMembe public override string DestinationName => DestinationMember?.Name; public override Type DestinationType { get; protected set; } public override MemberInfo[] SourceMembers { get; set; } = Array.Empty(); - public override bool CanBeSet => ReflectionHelper.CanBeSet(DestinationMember); + public override bool CanBeSet => DestinationMember.CanBeSet(); public override bool Ignored { get; set; } - public override Type SourceType => _sourceType ??= GetSourceType(); - public void ApplyInheritedPropertyMap(PropertyMap inheritedMappedProperty) + public void ApplyInheritedPropertyMap(PropertyMap inheritedMap) { - if (Ignored) + ApplyInheritedMap(inheritedMap); + if (!Ignored && inheritedMap._details != null) { - return; - } - if (!IsResolveConfigured) - { - if (inheritedMappedProperty.Ignored) - { - Ignored = true; - return; - } - if (inheritedMappedProperty.IsResolveConfigured) - { - _sourceType = inheritedMappedProperty._sourceType; - Resolver = inheritedMappedProperty.Resolver.CloseGenerics(TypeMap); - } - else if (Resolver == null) - { - _sourceType = inheritedMappedProperty._sourceType; - MapByConvention(inheritedMappedProperty.SourceMembers); - } - } - if (inheritedMappedProperty._details != null) - { - Details.ApplyInheritedPropertyMap(inheritedMappedProperty._details); + Details.ApplyInheritedPropertyMap(inheritedMap._details); } } public override IncludedMember IncludedMember => _details?.IncludedMember; diff --git a/src/AutoMapper/QueryableExtensions/Extensions.cs b/src/AutoMapper/QueryableExtensions/Extensions.cs index 8b50d06579..6ecb99e1fa 100644 --- a/src/AutoMapper/QueryableExtensions/Extensions.cs +++ b/src/AutoMapper/QueryableExtensions/Extensions.cs @@ -72,7 +72,7 @@ static IQueryable ToCore(this IQueryable source, Type destinationType, IConfigur configuration.Internal().ProjectionBuilder.GetProjection(source.ElementType, destinationType, parameters, memberPathsToExpand.Select(m => new MemberPath(m)).ToArray()) .Chain(source, Select); } -public class MemberVisitor : ExpressionVisitor +public sealed class MemberVisitor : ExpressionVisitor { private readonly List _members = new(); public static MemberInfo[] GetMemberPath(Expression expression) diff --git a/src/AutoMapper/QueryableExtensions/ProjectionBuilder.cs b/src/AutoMapper/QueryableExtensions/ProjectionBuilder.cs index 586f65de10..8ce201ce29 100644 --- a/src/AutoMapper/QueryableExtensions/ProjectionBuilder.cs +++ b/src/AutoMapper/QueryableExtensions/ProjectionBuilder.cs @@ -15,7 +15,7 @@ public interface IProjectionMapper Expression Project(IGlobalConfiguration configuration, in ProjectionRequest request, Expression resolvedSource, LetPropertyMaps letPropertyMaps); } [EditorBrowsable(EditorBrowsableState.Never)] -public class ProjectionBuilder : IProjectionBuilder +public sealed class ProjectionBuilder : IProjectionBuilder { internal static List DefaultProjectionMappers() => new(capacity: 5) @@ -26,9 +26,9 @@ internal static List DefaultProjectionMappers() => new StringProjectionMapper(), new EnumProjectionMapper(), }; - private readonly LockingConcurrentDictionary _projectionCache; - private readonly IGlobalConfiguration _configuration; - private readonly IProjectionMapper[] _projectionMappers; + readonly LockingConcurrentDictionary _projectionCache; + readonly IGlobalConfiguration _configuration; + readonly IProjectionMapper[] _projectionMappers; public ProjectionBuilder(IGlobalConfiguration configuration, IProjectionMapper[] projectionMappers) { _configuration = configuration; @@ -45,165 +45,190 @@ public QueryExpressions GetProjection(Type sourceType, Type destinationType, obj } return cachedExpressions.Prepare(_configuration.EnableNullPropagationForQueryMapping, parameters); } - private QueryExpressions CreateProjection(ProjectionRequest request) => - CreateProjection(request, new FirstPassLetPropertyMaps(_configuration, MemberPath.Empty, new())); - public QueryExpressions CreateProjection(in ProjectionRequest request, LetPropertyMaps letPropertyMaps) + QueryExpressions CreateProjection(ProjectionRequest request) + { + var (typeMap, polymorphicMaps) = PolymorphicMaps(request); + var letPropertyMaps = polymorphicMaps.Length > 0 ? new LetPropertyMaps(_configuration, MemberPath.Empty, new()) : new FirstPassLetPropertyMaps(_configuration, MemberPath.Empty, new()); + return CreateProjection(request, letPropertyMaps, typeMap, polymorphicMaps); + } + (TypeMap, TypeMap[]) PolymorphicMaps(in ProjectionRequest request) { - var instanceParameter = Parameter(request.SourceType, "dto"+ request.SourceType.Name); var typeMap = _configuration.ResolveTypeMap(request.SourceType, request.DestinationType) ?? throw TypeMap.MissingMapException(request.SourceType, request.DestinationType); - var projection = CreateProjectionCore(request, instanceParameter, typeMap, letPropertyMaps); - return letPropertyMaps.Count > 0 ? - letPropertyMaps.GetSubQueryExpression(this, projection, typeMap, request, instanceParameter) : - new(projection, instanceParameter); + return (typeMap, PolymorphicMaps(typeMap)); } - private Expression CreateProjectionCore(ProjectionRequest request, Expression instanceParameter, TypeMap typeMap, LetPropertyMaps letPropertyMaps) + TypeMap[] PolymorphicMaps(TypeMap typeMap) => _configuration.GetIncludedTypeMaps(typeMap.IncludedDerivedTypes + .Where(tp => tp.SourceType != typeMap.SourceType && !tp.DestinationType.IsAbstract).DistinctBy(tp => tp.SourceType).ToArray()); + public QueryExpressions CreateProjection(in ProjectionRequest request, LetPropertyMaps letPropertyMaps) { - var customProjection = typeMap.CustomMapExpression?.ReplaceParameters(instanceParameter); - if (customProjection != null) + var (typeMap, polymorphicMaps) = PolymorphicMaps(request); + return CreateProjection(request, letPropertyMaps, typeMap, polymorphicMaps); + } + QueryExpressions CreateProjection(in ProjectionRequest request, LetPropertyMaps letPropertyMaps, TypeMap typeMap, TypeMap[] polymorphicMaps) + { + var instanceParameter = Parameter(request.SourceType, "dto" + request.SourceType.Name); + var projection = CreateProjection(request, letPropertyMaps, typeMap, polymorphicMaps, instanceParameter); + return letPropertyMaps.Count > 0 ? letPropertyMaps.GetSubQueryExpression(this, projection, typeMap, request, instanceParameter) : new(projection, instanceParameter); + } + Expression CreateProjection(in ProjectionRequest request, LetPropertyMaps letPropertyMaps, TypeMap typeMap, TypeMap[] polymorphicMaps, Expression source) + { + var destinationType = typeMap.DestinationType; + var projection = (polymorphicMaps.Length > 0 && destinationType.IsAbstract) ? Default(destinationType) : CreateProjectionCore(request, letPropertyMaps, typeMap, source); + foreach(var derivedMap in polymorphicMaps) { - return customProjection; + var sourceType = derivedMap.SourceType; + var derivedRequest = request.InnerRequest(sourceType, derivedMap.DestinationType); + var derivedProjection = CreateProjectionCore(derivedRequest, letPropertyMaps, derivedMap, Convert(source, sourceType)); + projection = Condition(TypeIs(source, sourceType), derivedProjection, projection, projection.Type); } - var propertiesProjections = new List(); - int depth; - if (OverMaxDepth()) + return projection; + Expression CreateProjectionCore(ProjectionRequest request, LetPropertyMaps letPropertyMaps, TypeMap typeMap, Expression instanceParameter) { - if (typeMap.Profile.AllowNullDestinationValues) + var customProjection = typeMap.CustomMapExpression?.ReplaceParameters(instanceParameter); + if(customProjection != null) { - return null; + return customProjection; } - } - else - { - ProjectProperties(); - } - var constructorExpression = CreateDestination(); - var expression = MemberInit(constructorExpression, propertiesProjections); - return expression; - bool OverMaxDepth() - { - depth = letPropertyMaps.IncrementDepth(request); - return typeMap.MaxDepth > 0 && depth >= typeMap.MaxDepth; - } - void ProjectProperties() - { - foreach (var propertyMap in typeMap.PropertyMaps.Where(pm => - pm.CanResolveValue && pm.DestinationMember.CanBeSet() && !typeMap.ConstructorParameterMatches(pm.DestinationName)) - .OrderBy(pm => pm.DestinationMember.MetadataToken)) + var propertiesProjections = new List(); + int depth; + if(OverMaxDepth()) { - var propertyProjection = TryProjectMember(propertyMap); - if (propertyProjection != null) + if(typeMap.Profile.AllowNullDestinationValues) { - propertiesProjections.Add(Bind(propertyMap.DestinationMember, propertyProjection)); + return null; } } - } - Expression TryProjectMember(MemberMap memberMap, Expression defaultSource = null) - { - var memberProjection = new MemberProjection(memberMap); - letPropertyMaps.Push(memberProjection); - var memberExpression = ShouldExpand() ? ProjectMemberCore() : null; - letPropertyMaps.Pop(); - return memberExpression; - bool ShouldExpand() => memberMap.ExplicitExpansion != true || request.ShouldExpand(letPropertyMaps.GetCurrentPath()); - Expression ProjectMemberCore() + else { - var memberTypeMap = _configuration.ResolveTypeMap(memberMap.SourceType, memberMap.DestinationType); - var resolvedSource = ResolveSource(); - memberProjection.Expression ??= resolvedSource; - var memberRequest = request.InnerRequest(resolvedSource.Type, memberMap.DestinationType); - if (memberRequest.AlreadyExists && depth >= _configuration.RecursiveQueriesMaxDepth) - { - return null; - } - Expression mappedExpression; - if (memberTypeMap != null) + ProjectProperties(); + } + var constructorExpression = CreateDestination(); + var expression = MemberInit(constructorExpression, propertiesProjections); + return expression; + bool OverMaxDepth() + { + depth = letPropertyMaps.IncrementDepth(request); + return typeMap.MaxDepth > 0 && depth >= typeMap.MaxDepth; + } + void ProjectProperties() + { + foreach(var propertyMap in typeMap.PropertyMaps) { - mappedExpression = CreateProjectionCore(memberRequest, resolvedSource, memberTypeMap, letPropertyMaps); - if (mappedExpression != null && memberTypeMap.CustomMapExpression == null && memberMap.AllowsNullDestinationValues && - resolvedSource is not ParameterExpression && !resolvedSource.Type.IsCollection()) + if(!propertyMap.CanResolveValue || !propertyMap.CanBeSet || typeMap.ConstructorParameterMatches(propertyMap.DestinationName)) { - // Handles null source property so it will not create an object with possible non-nullable properties which would result in an exception. - mappedExpression = resolvedSource.IfNullElse(Constant(null, mappedExpression.Type), mappedExpression); + continue; + } + var propertyProjection = TryProjectMember(propertyMap); + if(propertyProjection != null) + { + propertiesProjections.Add(Bind(propertyMap.DestinationMember, propertyProjection)); } } - else - { - var projectionMapper = GetProjectionMapper(); - mappedExpression = projectionMapper.Project(_configuration, memberRequest, resolvedSource, letPropertyMaps); - } - return mappedExpression == null ? null : memberMap.ApplyTransformers(mappedExpression, _configuration); - Expression ResolveSource() + } + Expression TryProjectMember(MemberMap memberMap, Expression defaultSource = null) + { + var memberProjection = new MemberProjection(memberMap); + letPropertyMaps.Push(memberProjection); + var memberExpression = ShouldExpand() ? ProjectMemberCore() : null; + letPropertyMaps.Pop(); + return memberExpression; + bool ShouldExpand() => memberMap.ExplicitExpansion != true || request.ShouldExpand(letPropertyMaps.GetCurrentPath()); + Expression ProjectMemberCore() { - var customSource = memberMap.IncludedMember?.ProjectToCustomSource; - var resolvedSource = memberMap switch + var memberTypeMap = _configuration.ResolveTypeMap(memberMap.SourceType, memberMap.DestinationType); + var resolvedSource = ResolveSource(); + memberProjection.Expression ??= resolvedSource; + var memberRequest = request.InnerRequest(resolvedSource.Type, memberMap.DestinationType); + if(memberRequest.AlreadyExists && depth >= _configuration.RecursiveQueriesMaxDepth) { - { CustomMapExpression: LambdaExpression mapFrom } => MapFromExpression(mapFrom), - { SourceMembers.Length: > 0 } => memberMap.ChainSourceMembers(CheckCustomSource()), - _ => defaultSource ?? throw CannotMap(memberMap, request.SourceType) - }; - if (NullSubstitute()) + return null; + } + Expression mappedExpression; + if(memberTypeMap != null) { - return memberMap.NullSubstitute(resolvedSource); + mappedExpression = CreateProjection(memberRequest, letPropertyMaps, memberTypeMap, PolymorphicMaps(memberTypeMap), resolvedSource); + if(mappedExpression != null && memberTypeMap.CustomMapExpression == null && memberMap.AllowsNullDestinationValues && + resolvedSource is not ParameterExpression && !resolvedSource.Type.IsCollection()) + { + // Handles null source property so it will not create an object with possible non-nullable properties which would result in an exception. + mappedExpression = resolvedSource.IfNullElse(Default(mappedExpression.Type), mappedExpression); + } } - return resolvedSource; - Expression MapFromExpression(LambdaExpression mapFrom) + else + { + var projectionMapper = GetProjectionMapper(); + mappedExpression = projectionMapper.Project(_configuration, memberRequest, resolvedSource, letPropertyMaps); + } + return mappedExpression == null ? null : memberMap.ApplyTransformers(mappedExpression, _configuration); + Expression ResolveSource() { - if (memberTypeMap == null || mapFrom.IsMemberPath(out _) || mapFrom.Body is ParameterExpression) + var customSource = memberMap.IncludedMember?.ProjectToCustomSource; + var resolvedSource = memberMap switch { - return mapFrom.ReplaceParameters(CheckCustomSource()); + { CustomMapExpression: LambdaExpression mapFrom } => MapFromExpression(mapFrom), + { SourceMembers.Length: > 0 } => memberMap.ChainSourceMembers(CheckCustomSource()), + _ => defaultSource ?? throw CannotMap(memberMap, request.SourceType) + }; + if(NullSubstitute()) + { + return memberMap.NullSubstitute(resolvedSource); } - if (customSource == null) + return resolvedSource; + Expression MapFromExpression(LambdaExpression mapFrom) { - memberProjection.Expression = mapFrom; - return letPropertyMaps.GetSubQueryMarker(mapFrom); + if(memberTypeMap == null || letPropertyMaps.IsDefault || mapFrom.IsMemberPath(out _) || mapFrom.Body is ParameterExpression) + { + return mapFrom.ReplaceParameters(CheckCustomSource()); + } + if(customSource == null) + { + memberProjection.Expression = mapFrom; + return letPropertyMaps.GetSubQueryMarker(mapFrom); + } + var newMapFrom = IncludedMember.Chain(customSource, mapFrom); + memberProjection.Expression = newMapFrom; + return letPropertyMaps.GetSubQueryMarker(newMapFrom); } - var newMapFrom = IncludedMember.Chain(customSource, mapFrom); - memberProjection.Expression = newMapFrom; - return letPropertyMaps.GetSubQueryMarker(newMapFrom); - } - bool NullSubstitute() => memberMap.NullSubstitute != null && resolvedSource is MemberExpression && (resolvedSource.Type.IsNullableType() || resolvedSource.Type == typeof(string)); - Expression CheckCustomSource() - { - if (customSource == null) + bool NullSubstitute() => memberMap.NullSubstitute != null && resolvedSource is MemberExpression && (resolvedSource.Type.IsNullableType() || resolvedSource.Type == typeof(string)); + Expression CheckCustomSource() { - return instanceParameter; + if(customSource == null) + { + return instanceParameter; + } + return customSource.IsMemberPath(out _) || letPropertyMaps.IsDefault ? customSource.ReplaceParameters(instanceParameter) : letPropertyMaps.GetSubQueryMarker(customSource); } - return customSource.IsMemberPath(out _) ? customSource.ReplaceParameters(instanceParameter) : letPropertyMaps.GetSubQueryMarker(customSource); } - } - IProjectionMapper GetProjectionMapper() - { - var context = memberMap.Types(); - foreach (var mapper in _projectionMappers) + IProjectionMapper GetProjectionMapper() { - if (mapper.IsMatch(context)) + var context = memberMap.Types(); + foreach(var mapper in _projectionMappers) { - return mapper; + if(mapper.IsMatch(context)) + { + return mapper; + } } + throw CannotMap(memberMap, resolvedSource.Type); } - throw CannotMap(memberMap, resolvedSource.Type); } } + NewExpression CreateDestination() => typeMap switch + { + { CustomCtorExpression: LambdaExpression ctorExpression } => (NewExpression)ctorExpression.ReplaceParameters(instanceParameter), + { ConstructorMap: { CanResolve: true } constructorMap } => + New(constructorMap.Ctor, constructorMap.CtorParams.Select(map => TryProjectMember(map, map.DefaultValue(null)) ?? Default(map.DestinationType))), + _ => New(typeMap.DestinationType) + }; } - NewExpression CreateDestination() => typeMap switch - { - { CustomCtorExpression: LambdaExpression ctorExpression } => (NewExpression)ctorExpression.ReplaceParameters(instanceParameter), - { ConstructorMap: { CanResolve: true } constructorMap } => - New(constructorMap.Ctor, constructorMap.CtorParams.Select(map => TryProjectMember(map, map.DefaultValue(null)) ?? Default(map.DestinationType))), - _ => New(typeMap.DestinationType) - }; } - private static AutoMapperMappingException CannotMap(MemberMap memberMap, Type sourceType) => new( + static AutoMapperMappingException CannotMap(MemberMap memberMap, Type sourceType) => new( $"Unable to create a map expression from {memberMap.SourceMember?.DeclaringType?.Name}.{memberMap.SourceMember?.Name} ({sourceType}) to {memberMap.DestinationType.Name}.{memberMap.DestinationName} ({memberMap.DestinationType})", null, memberMap); [EditorBrowsable(EditorBrowsableState.Never)] - class FirstPassLetPropertyMaps : LetPropertyMaps + sealed class FirstPassLetPropertyMaps : LetPropertyMaps { - readonly Stack _currentPath = new(); readonly List _savedPaths = new(); - readonly MemberPath _parentPath; - public FirstPassLetPropertyMaps(IGlobalConfiguration configuration, MemberPath parentPath, TypePairCount builtProjections) : base(configuration, builtProjections) - => _parentPath = parentPath; + public FirstPassLetPropertyMaps(IGlobalConfiguration configuration, MemberPath parentPath, TypePairCount builtProjections) : base(configuration, parentPath, builtProjections) { } public override Expression GetSubQueryMarker(LambdaExpression letExpression) { var subQueryPath = new SubQueryPath(_currentPath.Reverse().ToArray(), letExpression); @@ -215,11 +240,8 @@ public override Expression GetSubQueryMarker(LambdaExpression letExpression) _savedPaths.Add(subQueryPath); return subQueryPath.Marker; } - public override void Push(MemberProjection memberProjection) => _currentPath.Push(memberProjection); - public override MemberPath GetCurrentPath() => _parentPath.Concat( - _currentPath.Reverse().Select(p => (p.MemberMap as PropertyMap)?.DestinationMember).Where(p => p != null)); - public override void Pop() => _currentPath.Pop(); public override int Count => _savedPaths.Count; + public override bool IsDefault => false; public override LetPropertyMaps New() => new FirstPassLetPropertyMaps(Configuration, GetCurrentPath(), BuiltProjections); public override QueryExpressions GetSubQueryExpression(ProjectionBuilder builder, Expression projection, TypeMap typeMap, in ProjectionRequest request, ParameterExpression instanceParameter) { @@ -237,7 +259,7 @@ public override QueryExpressions GetSubQueryExpression(ProjectionBuilder builder } var secondParameter = Parameter(letType, "dtoLet"); ReplaceSubQueries(); - var letClause = builder.CreateProjectionCore(request, instanceParameter, letTypeMap, base.New()); + var letClause = builder.CreateProjection(request, base.New(), letTypeMap, Array.Empty(), instanceParameter); return new(Lambda(projection, secondParameter), Lambda(letClause, instanceParameter)); void ReplaceSubQueries() { @@ -260,18 +282,7 @@ public Expression GetSourceExpression(Expression parameter) for (int index = 0; index < Members.Length - 1; index++) { var sourceMember = Members[index].Expression; - if (sourceMember is LambdaExpression lambda) - { - sourceExpression = lambda.ReplaceParameters(sourceExpression); - } - else - { - var chain = sourceMember.GetChain(); - if (chain.TryPeek(out var first)) - { - sourceExpression = sourceMember.Replace(first.Target, sourceExpression); - } - } + sourceExpression = sourceMember is LambdaExpression lambda ? lambda.ReplaceParameters(sourceExpression) : sourceMember; } return sourceExpression; } @@ -279,9 +290,9 @@ public Expression GetSourceExpression(Expression parameter) internal bool IsEquivalentTo(SubQueryPath other) => LetExpression == other.LetExpression && Members.Length == other.Members.Length && Members.Take(Members.Length - 1).Zip(other.Members, (left, right) => left.MemberMap == right.MemberMap).All(item => item); } - class GePropertiesVisitor : ExpressionVisitor + sealed class GePropertiesVisitor : ExpressionVisitor { - private readonly Expression _target; + readonly Expression _target; public List Members { get; } = new(); public GePropertiesVisitor(Expression target) => _target = target; protected override Expression VisitMember(MemberExpression node) @@ -299,9 +310,9 @@ public static IEnumerable Retrieve(Expression expression, E return visitor.Members.Select(member => new PropertyDescription(member.Name, member.GetMemberType())); } } - class ReplaceMemberAccessesVisitor : ExpressionVisitor + sealed class ReplaceMemberAccessesVisitor : ExpressionVisitor { - private readonly Expression _oldObject, _newObject; + readonly Expression _oldObject, _newObject; public ReplaceMemberAccessesVisitor(Expression oldObject, Expression newObject) { _oldObject = oldObject; @@ -321,10 +332,13 @@ protected override Expression VisitMember(MemberExpression node) [EditorBrowsable(EditorBrowsableState.Never)] public class LetPropertyMaps { - protected LetPropertyMaps(IGlobalConfiguration configuration, TypePairCount builtProjections) + protected private readonly Stack _currentPath = new(); + readonly MemberPath _parentPath; + protected internal LetPropertyMaps(IGlobalConfiguration configuration, MemberPath parentPath, TypePairCount builtProjections) { Configuration = configuration; BuiltProjections = builtProjections; + _parentPath = parentPath; } protected TypePairCount BuiltProjections { get; } public int IncrementDepth(in ProjectionRequest request) @@ -337,14 +351,16 @@ public int IncrementDepth(in ProjectionRequest request) return depth; } public virtual Expression GetSubQueryMarker(LambdaExpression letExpression) => letExpression.Body; - public virtual void Push(MemberProjection memberProjection) { } - public virtual MemberPath GetCurrentPath() => MemberPath.Empty; - public virtual void Pop() {} + public void Push(MemberProjection memberProjection) => _currentPath.Push(memberProjection); + public MemberPath GetCurrentPath() => _parentPath.Concat( + _currentPath.Reverse().Select(p => (p.MemberMap as PropertyMap)?.DestinationMember).Where(p => p != null)); + public void Pop() => _currentPath.Pop(); public virtual int Count => 0; public IGlobalConfiguration Configuration { get; } - public virtual LetPropertyMaps New() => new(Configuration, BuiltProjections); + public virtual LetPropertyMaps New() => new(Configuration, GetCurrentPath(), BuiltProjections); public virtual QueryExpressions GetSubQueryExpression(ProjectionBuilder builder, Expression projection, TypeMap typeMap, in ProjectionRequest request, ParameterExpression instanceParameter) => default; + public virtual bool IsDefault => true; } [EditorBrowsable(EditorBrowsableState.Never)] public readonly record struct QueryExpressions(LambdaExpression Projection, LambdaExpression LetClause = null) @@ -362,41 +378,40 @@ LambdaExpression Prepare(Expression cachedExpression) } } } -public class MemberProjection +public sealed record MemberProjection(MemberMap MemberMap) { - public MemberProjection(MemberMap memberMap) => MemberMap = memberMap; public Expression Expression { get; set; } - public MemberMap MemberMap { get; } } abstract class ParameterExpressionVisitor : ExpressionVisitor { public static Expression SetParameters(object parameters, Expression expression) { - var visitor = parameters is ParameterBag dictionary ? (ParameterExpressionVisitor)new ConstantExpressionReplacementVisitor(dictionary) : new ObjectParameterExpressionReplacementVisitor(parameters); + ParameterExpressionVisitor visitor = parameters is ParameterBag dictionary ? new ConstantExpressionReplacementVisitor(dictionary) : new ObjectParameterExpressionReplacementVisitor(parameters); return visitor.Visit(expression); } protected abstract Expression GetValue(string name); protected override Expression VisitMember(MemberExpression node) { - if (!node.Member.DeclaringType.Has()) + var member = node.Member; + if (!member.DeclaringType.Has()) { return base.VisitMember(node); } - var parameterName = node.Member.Name; + var parameterName = member.Name; var parameterValue = GetValue(parameterName); if (parameterValue == null) { const string VbPrefix = "$VB$Local_"; - if (!parameterName.StartsWith(VbPrefix, StringComparison.Ordinal) || (parameterValue = GetValue(parameterName.Substring(VbPrefix.Length))) == null) + if (!parameterName.StartsWith(VbPrefix, StringComparison.Ordinal) || (parameterValue = GetValue(parameterName[VbPrefix.Length..])) == null) { return base.VisitMember(node); } } - return ToType(parameterValue, node.Member.GetMemberType()); + return ToType(parameterValue, member.GetMemberType()); } - class ObjectParameterExpressionReplacementVisitor : ParameterExpressionVisitor + sealed class ObjectParameterExpressionReplacementVisitor : ParameterExpressionVisitor { - private readonly object _parameters; + readonly object _parameters; public ObjectParameterExpressionReplacementVisitor(object parameters) => _parameters = parameters; protected override Expression GetValue(string name) { @@ -404,9 +419,9 @@ protected override Expression GetValue(string name) return matchingMember != null ? Property(Constant(_parameters), matchingMember) : null; } } - class ConstantExpressionReplacementVisitor : ParameterExpressionVisitor + sealed class ConstantExpressionReplacementVisitor : ParameterExpressionVisitor { - private readonly ParameterBag _paramValues; + readonly ParameterBag _paramValues; public ConstantExpressionReplacementVisitor(ParameterBag paramValues) => _paramValues = paramValues; protected override Expression GetValue(string name) => _paramValues.TryGetValue(name, out object parameterValue) ? Constant(parameterValue) : null; } diff --git a/src/AutoMapper/ResolutionContext.cs b/src/AutoMapper/ResolutionContext.cs index 759b12b11e..88465994bd 100644 --- a/src/AutoMapper/ResolutionContext.cs +++ b/src/AutoMapper/ResolutionContext.cs @@ -1,9 +1,9 @@ +using System.Runtime.CompilerServices; namespace AutoMapper; - /// /// Context information regarding resolution of a destination value /// -public class ResolutionContext : IInternalRuntimeMapper +public sealed class ResolutionContext : IInternalRuntimeMapper { private Dictionary _instanceCache; private Dictionary _typeDepth; @@ -15,7 +15,13 @@ internal ResolutionContext(IInternalRuntimeMapper mapper, IMappingOperationOptio _options = options; } /// + /// The state passed in the options of the Map call. + /// Mutually exclusive with per Map call. + /// + public object State => _options?.State; + /// /// The items passed in the options of the Map call. + /// Mutually exclusive with per Map call. /// public Dictionary Items { @@ -103,4 +109,8 @@ private void CheckDefault() } private static void ThrowInvalidMap() => throw new InvalidOperationException("Context.Items are only available when using a Map overload that takes Action! Consider using Context.TryGetItems instead."); } -public readonly record struct ContextCacheKey(object Source, Type DestinationType); \ No newline at end of file +public readonly record struct ContextCacheKey(object Source, Type DestinationType) +{ + public override int GetHashCode() => HashCode.Combine(DestinationType, RuntimeHelpers.GetHashCode(Source)); + public bool Equals(ContextCacheKey other) => DestinationType == other.DestinationType && Source == other.Source; +} \ No newline at end of file diff --git a/src/AutoMapper/ServiceCollectionExtensions.cs b/src/AutoMapper/ServiceCollectionExtensions.cs new file mode 100644 index 0000000000..a2cb48bdc5 --- /dev/null +++ b/src/AutoMapper/ServiceCollectionExtensions.cs @@ -0,0 +1,102 @@ +namespace Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using AutoMapper; +using AutoMapper.Internal; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; + +/// +/// Extensions to scan for AutoMapper classes and register the configuration, mapping, and extensions with the service collection: +/// +/// Finds classes and initializes a new , +/// Scans for , , and implementations and registers them as , +/// Registers as , and +/// Registers as a configurable (default is ) +/// +/// After calling AddAutoMapper you can resolve an instance from a scoped service provider, or as a dependency +/// To use you can resolve the instance directly for from an instance. +/// +public static class ServiceCollectionExtensions +{ + public static IServiceCollection AddAutoMapper(this IServiceCollection services, Action configAction) + => AddAutoMapperClasses(services, (sp, cfg) => configAction?.Invoke(cfg), null); + + public static IServiceCollection AddAutoMapper(this IServiceCollection services, params Assembly[] assemblies) + => AddAutoMapperClasses(services, null, assemblies); + + public static IServiceCollection AddAutoMapper(this IServiceCollection services, Action configAction, params Assembly[] assemblies) + => AddAutoMapperClasses(services, (sp, cfg) => configAction?.Invoke(cfg), assemblies); + + public static IServiceCollection AddAutoMapper(this IServiceCollection services, Action configAction, params Assembly[] assemblies) + => AddAutoMapperClasses(services, configAction, assemblies); + + public static IServiceCollection AddAutoMapper(this IServiceCollection services, Action configAction, IEnumerable assemblies, ServiceLifetime serviceLifetime = ServiceLifetime.Transient) + => AddAutoMapperClasses(services, (sp, cfg) => configAction?.Invoke(cfg), assemblies, serviceLifetime); + + public static IServiceCollection AddAutoMapper(this IServiceCollection services, Action configAction, IEnumerable assemblies, ServiceLifetime serviceLifetime = ServiceLifetime.Transient) + => AddAutoMapperClasses(services, configAction, assemblies, serviceLifetime); + + public static IServiceCollection AddAutoMapper(this IServiceCollection services, IEnumerable assemblies, ServiceLifetime serviceLifetime = ServiceLifetime.Transient) + => AddAutoMapperClasses(services, null, assemblies, serviceLifetime); + + public static IServiceCollection AddAutoMapper(this IServiceCollection services, params Type[] profileAssemblyMarkerTypes) + => AddAutoMapperClasses(services, null, profileAssemblyMarkerTypes.Select(t => t.GetTypeInfo().Assembly)); + + public static IServiceCollection AddAutoMapper(this IServiceCollection services, Action configAction, params Type[] profileAssemblyMarkerTypes) + => AddAutoMapperClasses(services, (sp, cfg) => configAction?.Invoke(cfg), profileAssemblyMarkerTypes.Select(t => t.GetTypeInfo().Assembly)); + + public static IServiceCollection AddAutoMapper(this IServiceCollection services, Action configAction, params Type[] profileAssemblyMarkerTypes) + => AddAutoMapperClasses(services, configAction, profileAssemblyMarkerTypes.Select(t => t.GetTypeInfo().Assembly)); + + public static IServiceCollection AddAutoMapper(this IServiceCollection services, Action configAction, + IEnumerable profileAssemblyMarkerTypes, ServiceLifetime serviceLifetime = ServiceLifetime.Transient) + => AddAutoMapperClasses(services, (sp, cfg) => configAction?.Invoke(cfg), profileAssemblyMarkerTypes.Select(t => t.GetTypeInfo().Assembly), serviceLifetime); + + public static IServiceCollection AddAutoMapper(this IServiceCollection services, Action configAction, + IEnumerable profileAssemblyMarkerTypes, ServiceLifetime serviceLifetime = ServiceLifetime.Transient) + => AddAutoMapperClasses(services, configAction, profileAssemblyMarkerTypes.Select(t => t.GetTypeInfo().Assembly), serviceLifetime); + + private static IServiceCollection AddAutoMapperClasses(IServiceCollection services, Action configAction, + IEnumerable assembliesToScan, ServiceLifetime serviceLifetime = ServiceLifetime.Transient) + { + if (configAction != null) + { + services.AddOptions().Configure((options, sp) => configAction(sp, options)); + } + if (assembliesToScan != null) + { + assembliesToScan = new HashSet(assembliesToScan.Where(a => !a.IsDynamic && a != typeof(Mapper).Assembly)); + services.Configure(options => options.AddMaps(assembliesToScan)); + var openTypes = new[] + { + typeof(IValueResolver<,,>), + typeof(IMemberValueResolver<,,,>), + typeof(ITypeConverter<,>), + typeof(IValueConverter<,>), + typeof(IMappingAction<,>) + }; + foreach (var type in assembliesToScan.SelectMany(a => a.GetTypes().Where(type => type.IsClass && !type.IsAbstract && Array.Exists(openTypes, + openType => type.GetGenericInterface(openType) != null)))) + { + // use try add to avoid double-registration + services.TryAddTransient(type); + } + } + // Just return if we've already added AutoMapper to avoid double-registration + if (services.Any(sd => sd.ServiceType == typeof(IMapper))) + { + return services; + } + services.AddSingleton(sp => + { + // A mapper configuration is required + var options = sp.GetRequiredService>(); + return new MapperConfiguration(options.Value); + }); + services.Add(new ServiceDescriptor(typeof(IMapper), sp => new Mapper(sp.GetRequiredService(), sp.GetService), serviceLifetime)); + return services; + } +} \ No newline at end of file diff --git a/src/AutoMapper/TypeMap.cs b/src/AutoMapper/TypeMap.cs index 08f981772d..e2059385ca 100644 --- a/src/AutoMapper/TypeMap.cs +++ b/src/AutoMapper/TypeMap.cs @@ -7,7 +7,7 @@ namespace AutoMapper; /// [DebuggerDisplay("{SourceType.Name} -> {DestinationType.Name}")] [EditorBrowsable(EditorBrowsableState.Never)] -public class TypeMap +public sealed class TypeMap { static readonly LambdaExpression EmptyLambda = Lambda(ExpressionBuilder.Empty); static readonly MethodInfo CreateProxyMethod = typeof(ObjectFactory).GetStaticMethod(nameof(ObjectFactory.CreateInterfaceProxy)); @@ -46,6 +46,7 @@ public string CheckRecord() => ConstructorMap?.Ctor is ConstructorInfo ctor && c " When mapping to records, consider using only public constructors. See https://docs.automapper.org/en/latest/Construction.html." : null; public Features Features => Details.Features; private TypeMapDetails Details => _details ??= new(); + public bool HasDetails => _details != null; public void CheckProjection() { if (Projection) @@ -76,9 +77,9 @@ internal bool CanConstructorMap() => Profile.ConstructorMappingEnabled && !Desti public MemberList ConfiguredMemberList { get => (_details?.ConfiguredMemberList).GetValueOrDefault(); - set + set { - if (value == default) + if (_details == null && value == default) { return; } @@ -305,7 +306,8 @@ public void Seal(IGlobalConfiguration configuration, TypeMap thisMap) { foreach (var inheritedTypeMap in InheritedTypeMaps) { - var includedMaps = inheritedTypeMap?._details?.IncludedMembersTypeMaps; + inheritedTypeMap.Seal(configuration); + var includedMaps = inheritedTypeMap._details?.IncludedMembersTypeMaps; if (includedMaps != null) { IncludedMembersTypeMaps ??= new(); @@ -427,7 +429,7 @@ private void ApplyIncludedMemberTypeMap(IncludedMember includedMember, TypeMap t .Select(p => new PropertyMap(p, thisMap, includedMember)) .ToArray(); var notOverridenPathMaps = NotOverridenPathMaps(typeMap); - var appliedConstructorMap = thisMap.ConstructorMap?.ApplyIncludedMember(includedMember); + var appliedConstructorMap = thisMap.ConstructorMap?.ApplyMap(typeMap, includedMember); if (includedMemberMaps.Length == 0 && notOverridenPathMaps.Length == 0 && appliedConstructorMap is not true) { return; @@ -456,6 +458,7 @@ private void ApplyInheritedTypeMap(TypeMap inheritedTypeMap, TypeMap thisMap) { ApplyInheritedPropertyMaps(inheritedTypeMap, thisMap); } + thisMap.ConstructorMap?.ApplyMap(inheritedTypeMap); var inheritedDetails = inheritedTypeMap._details; if (inheritedDetails == null) { diff --git a/src/Benchmark/BenchEngine.cs b/src/Benchmark/BenchEngine.cs index 7cc527f21f..bd3d074050 100644 --- a/src/Benchmark/BenchEngine.cs +++ b/src/Benchmark/BenchEngine.cs @@ -14,17 +14,17 @@ public BenchEngine(IObjectToObjectMapper mapper, string mode) public void Start() { _mapper.Initialize(); - //_mapper.Map(); + _mapper.Map(); - //var timer = Stopwatch.StartNew(); + var timer = Stopwatch.StartNew(); - //for (int i = 0; i < 1_000_000; i++) - //{ - // _mapper.Map(); - //} + for(int i = 0; i < 1_000_000; i++) + { + _mapper.Map(); + } - //timer.Stop(); + timer.Stop(); - //Console.WriteLine("{2:D3} ms {0}: - {1}", _mapper.Name, _mode, (int)timer.Elapsed.TotalMilliseconds); + Console.WriteLine("{2:D3} ms {0}: - {1}", _mapper.Name, _mode, (int)timer.Elapsed.TotalMilliseconds); } } \ No newline at end of file diff --git a/src/Benchmark/Benchmark.csproj b/src/Benchmark/Benchmark.csproj index eccda679ed..15056f7dcd 100644 --- a/src/Benchmark/Benchmark.csproj +++ b/src/Benchmark/Benchmark.csproj @@ -1,7 +1,7 @@  - net7.0 + net8.0 Exe diff --git a/src/Benchmark/Program.cs b/src/Benchmark/Program.cs index fd1d9f7037..ff5418f1c2 100644 --- a/src/Benchmark/Program.cs +++ b/src/Benchmark/Program.cs @@ -13,7 +13,7 @@ public static void Main(string[] args) { "Complex", new IObjectToObjectMapper[] { new ComplexTypeMapper(), new ManualComplexTypeMapper() } }, { "Deep", new IObjectToObjectMapper[] { new DeepTypeMapper(), new ManualDeepTypeMapper() } } }; - //while (true) + while (true) { foreach (var pair in mappers) { @@ -22,7 +22,7 @@ public static void Main(string[] args) new BenchEngine(mapper, pair.Key).Start(); } } - //Console.ReadLine(); + Console.ReadLine(); } } } diff --git a/src/IntegrationTests/AutoMapper.IntegrationTests.csproj b/src/IntegrationTests/AutoMapper.IntegrationTests.csproj index d6422a78b7..5c170ccb7f 100644 --- a/src/IntegrationTests/AutoMapper.IntegrationTests.csproj +++ b/src/IntegrationTests/AutoMapper.IntegrationTests.csproj @@ -1,18 +1,18 @@  - net7.0 + net8.0 $(NoWarn);618 - - - - - + + + + + diff --git a/src/IntegrationTests/ConstructorDefaultValue.cs b/src/IntegrationTests/ConstructorDefaultValue.cs index c580d8346a..d99f749d64 100644 --- a/src/IntegrationTests/ConstructorDefaultValue.cs +++ b/src/IntegrationTests/ConstructorDefaultValue.cs @@ -29,4 +29,39 @@ public void Can_map_with_projection() using var context = new Context(); ProjectTo(context.Customers).Single().Value.ShouldBe(5); } +} +public class StructConstructorMapping : IntegrationTest +{ + public class Customer + { + public int Id { get; set; } + public DateTime Date { get; set; } + } + public class CustomerViewModel + { + public DateOnly Date { get; set; } + } + public class Context : LocalDbContext + { + public DbSet Customers { get; set; } + } + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(Context context) + { + context.Customers.Add(new Customer { Date = new(1984, 5, 23) }); + base.Seed(context); + } + } + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateProjection(); + cfg.CreateProjection(); + }); + [Fact] + public void Can_map_with_projection() + { + using var context = new Context(); + ProjectTo(context.Customers).Single().Date.ShouldBe(new(1984, 5, 23)); + } } \ No newline at end of file diff --git a/src/IntegrationTests/CustomMapFrom/MapObjectPropertyFromSubQuery.cs b/src/IntegrationTests/CustomMapFrom/MapObjectPropertyFromSubQuery.cs index 0e2d5d7280..b810e952cb 100644 --- a/src/IntegrationTests/CustomMapFrom/MapObjectPropertyFromSubQuery.cs +++ b/src/IntegrationTests/CustomMapFrom/MapObjectPropertyFromSubQuery.cs @@ -1,5 +1,92 @@ namespace AutoMapper.IntegrationTests.CustomMapFrom; - +public class MultipleLevelsSubquery : IntegrationTest +{ + [Fact] + public void Should_work() + { + using var context = new Context(); + var resultQuery = ProjectTo(context.Foos); + resultQuery.Single().MyBar.MyBaz.FirstWidget.Id.ShouldBe(1); + } + protected override MapperConfiguration CreateConfiguration() => new(c => + { + c.CreateMap().ForMember(f => f.MyBar, opts => opts.MapFrom(src => src.Bar)); + c.CreateMap().ForMember(f => f.MyBaz, opts => opts.MapFrom(src => src.Baz)); + c.CreateMap().ForMember(f => f.FirstWidget, opts => opts.MapFrom(src => src.Widgets.FirstOrDefault())); + c.CreateMap(); + }); + public class Context : LocalDbContext + { + public virtual DbSet Foos { get; set; } + public virtual DbSet Bazs { get; set; } + } + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(Context context) + { + var testBaz = new Baz(); + testBaz.Widgets.Add(new Widget()); + testBaz.Widgets.Add(new Widget()); + var testBar = new Bar(); + testBar.Foos.Add(new Foo()); + testBaz.Bars.Add(testBar); + context.Bazs.Add(testBaz); + } + } + public class Foo + { + public int Id { get; set; } + public int BarId { get; set; } + public virtual Bar Bar { get; set; } + } + public class Bar + { + public Bar() => Foos = new HashSet(); + public int Id { get; set; } + public int BazId { get; set; } + public virtual Baz Baz { get; set; } + public virtual ICollection Foos { get; set; } + } + public class Baz + { + public Baz() + { + Bars = new HashSet(); + Widgets = new HashSet(); + } + public int Id { get; set; } + public virtual ICollection Bars { get; set; } + public virtual ICollection Widgets { get; set; } + } + public partial class Widget + { + public int Id { get; set; } + public int BazId { get; set; } + public virtual Baz Baz { get; set; } + } + public class FooModel + { + public int Id { get; set; } + public int BarId { get; set; } + public BarModel MyBar { get; set; } + } + public class BarModel + { + public int Id { get; set; } + public int BazId { get; set; } + public BazModel MyBaz { get; set; } + } + public class BazModel + { + public int Id { get; set; } + public WidgetModel FirstWidget { get; set; } + } + public class WidgetModel + { + public int Id { get; set; } + public int BazId { get; set; } + } +} public class MemberWithSubQueryProjections : IntegrationTest { public class Customer @@ -1016,4 +1103,1484 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) } public DbSet AEntities { get; set; } } +} + +public class MultipleLevelsSubqueryWithInheritance : IntegrationTest +{ + [Fact] + public void Should_work() + { + using var context = new Context(); + var resultQuery = ProjectTo(context.Foos); + resultQuery.Single().MyBar.MyBaz.FirstWidget.Id.ShouldBe(1); + } + protected override MapperConfiguration CreateConfiguration() => new(c => + { + c.CreateMap().ForMember(f => f.MyBar, opts => opts.MapFrom(src => src.Bar)); + c.CreateMap().ForMember(f => f.MyBaz, opts => opts.MapFrom(src => src.Baz)); + c.CreateMap().ForMember(f => f.FirstWidget, opts => opts.MapFrom(src => src.Widgets.FirstOrDefault())); + c.CreateMap(); + }); + public class Context : LocalDbContext + { + public virtual DbSet Foos { get; set; } + public virtual DbSet Bazs { get; set; } + } + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(Context context) + { + var testBaz = new Baz(); + testBaz.Widgets.Add(new Widget()); + testBaz.Widgets.Add(new Widget()); + var testBar = new Bar(); + testBar.Foos.Add(new Foo()); + testBaz.Bars.Add(testBar); + context.Bazs.Add(testBaz); + } + } + public class Foo + { + public int Id { get; set; } + public int BarId { get; set; } + public virtual Bar Bar { get; set; } + } + public class Bar + { + public Bar() => Foos = new HashSet(); + public int Id { get; set; } + public int BazId { get; set; } + public virtual Baz Baz { get; set; } + public virtual ICollection Foos { get; set; } + } + public class Baz + { + public Baz() + { + Bars = new HashSet(); + Widgets = new HashSet(); + } + public int Id { get; set; } + public virtual ICollection Bars { get; set; } + public virtual ICollection Widgets { get; set; } + } + public partial class Widget + { + public int Id { get; set; } + public int BazId { get; set; } + public virtual Baz Baz { get; set; } + } + public class FooModel + { + public int Id { get; set; } + public int BarId { get; set; } + public BarModel MyBar { get; set; } + } + public class BarModel + { + public int Id { get; set; } + public int BazId { get; set; } + public BazModel MyBaz { get; set; } + } + public class BazModel + { + public int Id { get; set; } + public WidgetModel FirstWidget { get; set; } + } + public class WidgetModel + { + public int Id { get; set; } + public int BazId { get; set; } + } +} +public class MemberWithSubQueryProjectionsWithInheritance : IntegrationTest +{ + public class Customer + { + [Key] + public int Id { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public ICollection Items { get; set; } + } + public class CustomerA : Customer + { + } + public class CustomerB : Customer + { + public string B { get; set; } + } + public class Item + { + public int Id { get; set; } + public int Code { get; set; } + } + public class ItemA : Item + { + public string A { get; set; } + } + public class ItemModel + { + public int Id { get; set; } + public int Code { get; set; } + } + public class ItemModelA : ItemModel + { + public string A { get; set; } + } + public class CustomerViewModel + { + public CustomerNameModel Name { get; set; } + public ItemModel FirstItem { get; set; } + } + public class CustomerAViewModel : CustomerViewModel + { + public ItemModelA FirstItemA { get; set; } + } + public class CustomerBViewModel : CustomerViewModel + { + public string B { get; set; } + } + public class CustomerNameModel + { + public string FirstName { get; set; } + public string LastName { get; set; } + } + public class Context : LocalDbContext + { + public DbSet Customers { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity(); + modelBuilder.Entity(); + modelBuilder.Entity(); + } + } + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(Context context) + { + context.Customers.Add(new CustomerA + { + FirstName = "Alice", + LastName = "Smith", + Items = new[] { new Item { Code = 1 }, new ItemA { Code = 3, A = "a", }, new Item { Code = 5 } } + }); + context.Customers.Add(new CustomerB + { + FirstName = "Bob", + LastName = "Smith", + B = "b", + Items = new[] { new Item { Code = 1 }, new Item { Code = 3 }, new Item { Code = 5 } } + }); context.Customers.Add(new Customer + { + FirstName = "Jim", + LastName = "Smith", + Items = new[] { new Item { Code = 1 }, new Item { Code = 3 }, new Item { Code = 5 } } + }); + base.Seed(context); + } + } + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap() + .ForMember(dst => dst.Name, opt => opt.MapFrom(src => src.LastName != null ? src : null)) + .ForMember(dst => dst.FirstItem, opt => opt.MapFrom(src => src.Items.FirstOrDefault())) + .Include() + .Include(); + + + cfg.CreateMap() + .ForMember(dst => dst.FirstItemA, opt => opt.MapFrom(src => src.Items.OfType().FirstOrDefault())); + cfg.CreateMap(); + cfg.CreateMap(); + cfg.CreateMap() + .Include(); + cfg.CreateMap(); + }); + [Fact] + public void Should_work() + { + using (var context = new Context()) + { + var resultQuery = ProjectTo(context.Customers.OrderBy(p => p.FirstName)); + var list = resultQuery.ToList(); + + var resultA = list[0].ShouldBeOfType(); + resultA.Name.FirstName.ShouldBe("Alice"); + resultA.Name.LastName.ShouldBe("Smith"); + resultA.FirstItem.Code.ShouldBe(1); + resultA.FirstItemA.A.ShouldBe("a"); + + var resultB = list[1].ShouldBeOfType(); + resultB.Name.FirstName.ShouldBe("Bob"); + resultB.Name.LastName.ShouldBe("Smith"); + resultB.FirstItem.Code.ShouldBe(1); + resultB.B.ShouldBe("b"); + + var result = list[2].ShouldBeOfType(); + result.Name.FirstName.ShouldBe("Jim"); + result.Name.LastName.ShouldBe("Smith"); + result.FirstItem.Code.ShouldBe(1); + } + } +} +public class MemberWithSubQueryProjectionsNoMapWithInheritance : IntegrationTest +{ + public class Customer + { + [Key] + public int Id { get; set; } + public string FirstName { get; set; } + public string LastName { get; set; } + public ICollection Items { get; set; } + } + public class CustomerA : Customer + { + public string A { get; set; } + } + public class CustomerB : Customer + { + public string B { get; set; } + } + public class Item + { + public int Id { get; set; } + public int Code { get; set; } + } + public class ItemModel + { + public int Id { get; set; } + public int Code { get; set; } + } + public class CustomerViewModel + { + public string Name { get; set; } + public ItemModel FirstItem { get; set; } + } + public class CustomerAViewModel : CustomerViewModel + { + public string A { get; set; } + } + public class CustomerBViewModel : CustomerViewModel + { + public string B { get; set; } + } + public class Context : LocalDbContext + { + public DbSet Customers { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity(); + modelBuilder.Entity(); + } + } + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(Context context) + { + context.Customers.Add(new CustomerA + { + FirstName = "Alice", + LastName = "Smith", + A = "a", + Items = new[] { new Item { Code = 1 }, new Item { Code = 3 }, new Item { Code = 5 } } + }); + context.Customers.Add(new CustomerB + { + FirstName = "Bob", + LastName = "Smith", + B = "b", + Items = new[] { new Item { Code = 1 }, new Item { Code = 3 }, new Item { Code = 5 } } + }); context.Customers.Add(new Customer + { + FirstName = "Jim", + LastName = "Smith", + Items = new[] { new Item { Code = 1 }, new Item { Code = 3 }, new Item { Code = 5 } } + }); + base.Seed(context); + } + } + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap() + .ForMember(dst => dst.Name, opt => opt.MapFrom(src => src.LastName != null ? src.LastName : null)) + .ForMember(dst => dst.FirstItem, opt => opt.MapFrom(src => src.Items.FirstOrDefault())) + .Include() + .Include(); + + cfg.CreateMap(); + cfg.CreateMap(); + cfg.CreateMap(); + }); + [Fact] + public void Should_work() + { + using (var context = new Context()) + { + var resultQuery = ProjectTo(context.Customers.OrderBy(p => p.FirstName)); + var list = resultQuery.ToList(); + + var resultA = list[0].ShouldBeOfType(); + resultA.Name.ShouldBe("Smith"); + resultA.FirstItem.Code.ShouldBe(1); + resultA.A.ShouldBe("a"); + + var resultB = list[1].ShouldBeOfType(); + resultB.Name.ShouldBe("Smith"); + resultB.FirstItem.Code.ShouldBe(1); + resultB.B.ShouldBe("b"); + + var result = list[2].ShouldBeOfType(); + result.Name.ShouldBe("Smith"); + result.FirstItem.Code.ShouldBe(1); + } + } +} +public class MapObjectPropertyFromSubQueryTypeNameMaxWithInheritance : IntegrationTest +{ + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap() + .ForMember(d => d.Price, o => o.MapFrom(source => source.Articles.Where(x => x.IsDefault && x.NationId == 1 && source.ECommercePublished).FirstOrDefault())) + .Include() + .Include(); + + cfg.CreateMap(); + cfg.CreateMap(); + cfg.CreateMap() + .ForMember(d => d.RegionId, o => o.MapFrom(s => s.NationId)); + }); + + [Fact] + public void Should_cache_the_subquery() + { + using (var context = new ClientContext()) + { + var projection = ProjectTo(context.Products.OrderBy(p => p.Name)); + var counter = new FirstOrDefaultCounter(); + counter.Visit(projection.Expression); + counter.Count.ShouldBe(12); + var list = projection.ToList(); + + var productAModel = list[0].ShouldBeOfType(); + productAModel.Price.RegionId.ShouldBe((short)1); + productAModel.Price.IsDefault.ShouldBeTrue(); + productAModel.A.ShouldBe("a"); + + var productBModel = list[1].ShouldBeOfType(); + productBModel.Price.RegionId.ShouldBe((short)1); + productBModel.Price.IsDefault.ShouldBeTrue(); + productBModel.B.ShouldBe("b"); + + var productModel = list[2].ShouldBeOfType(); + productModel.Price.RegionId.ShouldBe((short)1); + productModel.Price.IsDefault.ShouldBeTrue(); + } + } + + class FirstOrDefaultCounter : ExpressionVisitor + { + public int Count; + + protected override Expression VisitMethodCall(MethodCallExpression node) + { + if (node.Method.Name == "FirstOrDefault") + { + Count++; + } + return base.VisitMethodCall(node); + } + } + + public partial class Article + { + public int Id { get; set; } + public int ProductId { get; set; } + public bool IsDefault { get; set; } + public short NationId { get; set; } + public virtual Product Product { get; set; } + } + + public partial class Product + { + public int Id { get; set; } + public string Name { get; set; } + public bool ECommercePublished { get; set; } + public virtual ICollection
Articles { get; set; } + public int Value { get; } + [NotMapped] + public int NotMappedValue { get; set; } + public int VeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnName1 { get; set; } + public int VeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnName2 { get; set; } + public int VeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnName3 { get; set; } + public int VeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnName4 { get; set; } + public int VeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnName5 { get; set; } + public int VeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnName6 { get; set; } + public int VeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnName7 { get; set; } + public int VeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnName8 { get; set; } + public int VeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnName9 { get; set; } + public int VeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnName10 { get; set; } + public int VeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnName11 { get; set; } + } + + public class ProductA : Product + { + public string A { get; set; } + } + public class ProductB : Product + { + public string B { get; set; } + } + + public class PriceModel + { + public int Id { get; set; } + public short RegionId { get; set; } + public bool IsDefault { get; set; } + } + + public class ProductModel + { + public int Id { get; set; } + public PriceModel Price { get; set; } + public int VeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnName1 { get; set; } + public int VeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnName2 { get; set; } + public int VeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnName3 { get; set; } + public int VeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnName4 { get; set; } + public int VeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnName5 { get; set; } + public int VeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnName6 { get; set; } + public int VeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnName7 { get; set; } + public int VeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnName8 { get; set; } + public int VeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnName9 { get; set; } + public int VeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnName10 { get; set; } + public int VeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnNameVeryLongColumnName11 { get; set; } + } + + public class ProductAModel : ProductModel + { + public string A { get; set; } + } + public class ProductBModel : ProductModel + { + public string B { get; set; } + } + + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(ClientContext context) + { + context.Products.Add(new ProductA { Name = "P1", A = "a", ECommercePublished = true, Articles = new[] { new Article { IsDefault = true, NationId = 1, ProductId = 1 } } }); + context.Products.Add(new ProductB { Name = "P2", B = "b", ECommercePublished = true, Articles = new[] { new Article { IsDefault = true, NationId = 1, ProductId = 1 } } }); + context.Products.Add(new Product { Name = "P3", ECommercePublished = true, Articles = new[] { new Article { IsDefault = true, NationId = 1, ProductId = 1 } } }); + } + } + + public class ClientContext : LocalDbContext + { + public DbSet Products { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity(); + modelBuilder.Entity(); + } + } +} +public class MapObjectPropertyFromSubQueryExplicitExpansionWithInheritance : IntegrationTest +{ + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap() + .ForMember(d => d.Price, o => + { + o.MapFrom(source => source.Articles.Where(x => x.IsDefault && x.NationId == 1 && source.ECommercePublished).FirstOrDefault()); + o.ExplicitExpansion(); + }) + .Include() + .Include(); + + cfg.CreateMap(); + cfg.CreateMap(); + + cfg.CreateMap() + .ForMember(d => d.RegionId, o => o.MapFrom(s => s.NationId)); + }); + + [Fact] + public void Should_map_ok() + { + using (var context = new ClientContext()) + { + var projection = ProjectTo(context.Products.OrderBy(p => p.Name)); + var counter = new FirstOrDefaultCounter(); + counter.Visit(projection.Expression); + counter.Count.ShouldBe(0); + var list = projection.ToList(); + + var productAModel = list[0].ShouldBeOfType(); + productAModel.Price.ShouldBeNull(); + productAModel.Name.ShouldBe("P1"); + productAModel.A.ShouldBe("a"); + + var productBModel = list[1].ShouldBeOfType(); + productBModel.Price.ShouldBeNull(); + productBModel.Name.ShouldBe("P2"); + productBModel.B.ShouldBe("b"); + + var productModel = list[2].ShouldBeOfType(); + productModel.Price.ShouldBeNull(); + productModel.Name.ShouldBe("P3"); + } + } + + class FirstOrDefaultCounter : ExpressionVisitor + { + public int Count; + + protected override Expression VisitMethodCall(MethodCallExpression node) + { + if (node.Method.Name == "FirstOrDefault") + { + Count++; + } + return base.VisitMethodCall(node); + } + } + + public partial class Article + { + public int Id { get; set; } + public int ProductId { get; set; } + public bool IsDefault { get; set; } + public short NationId { get; set; } + public virtual Product Product { get; set; } + } + + public partial class Product + { + public int Id { get; set; } + public string Name { get; set; } + public bool ECommercePublished { get; set; } + public virtual ICollection
Articles { get; set; } + public int Value { get; } + } + + public class ProductA : Product + { + public string A { get; set; } + } + public class ProductB : Product + { + public string B { get; set; } + } + + public class PriceModel + { + public int Id { get; set; } + public short RegionId { get; set; } + public bool IsDefault { get; set; } + } + + public class ProductModel + { + public string Name { get; set; } + public PriceModel Price { get; set; } + } + + public class ProductAModel : ProductModel + { + public string A { get; set; } + } + public class ProductBModel : ProductModel + { + public string B { get; set; } + } + + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(ClientContext context) + { + context.Products.Add(new ProductA { ECommercePublished = true, Name = "P1", A = "a", Articles = new[] { new Article { IsDefault = true, NationId = 1, ProductId = 1 } } }); + context.Products.Add(new ProductB { ECommercePublished = true, Name = "P2", B = "b", Articles = new[] { new Article { IsDefault = true, NationId = 1, ProductId = 1 } } }); + context.Products.Add(new Product { ECommercePublished = true, Name = "P3", Articles = new[] { new Article { IsDefault = true, NationId = 1, ProductId = 1 } } }); + } + } + + public class ClientContext : LocalDbContext + { + public DbSet Products { get; set; } + public DbSet
Articles { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity(); + modelBuilder.Entity(); + } + } +} +public class MapObjectPropertyFromSubQueryWithInheritance : IntegrationTest +{ + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap() + .ForMember(d => d.Price, o => o.MapFrom(source => source.Articles.Where(x => x.IsDefault && x.NationId == 1 && source.ECommercePublished).FirstOrDefault())) + .Include() + .Include(); + cfg.CreateMap(); + cfg.CreateMap(); + cfg.CreateMap() + .ForMember(d => d.RegionId, o => o.MapFrom(s => s.NationId)); + }); + + [Fact] + public void Should_cache_the_subquery() + { + using (var context = new ClientContext()) + { + var projection = ProjectTo(context.Products.OrderBy(p => p.Name)); + var counter = new FirstOrDefaultCounter(); + counter.Visit(projection.Expression); + counter.Count.ShouldBe(12); + var list = projection.ToList(); + + var productAModel = list[0].ShouldBeOfType(); + productAModel.Price.RegionId.ShouldBe((short)1); + productAModel.Price.IsDefault.ShouldBeTrue(); + productAModel.Name.ShouldBe("P1"); + productAModel.A.ShouldBe("a"); + + var productBModel = list[1].ShouldBeOfType(); + productBModel.Price.RegionId.ShouldBe((short)1); + productBModel.Price.IsDefault.ShouldBeTrue(); + productBModel.Name.ShouldBe("P2"); + productBModel.B.ShouldBe("b"); + + var productModel = list[2].ShouldBeOfType(); + productModel.Price.RegionId.ShouldBe((short)1); + productModel.Price.IsDefault.ShouldBeTrue(); + productModel.Name.ShouldBe("P3"); + } + } + + class FirstOrDefaultCounter : ExpressionVisitor + { + public int Count; + + protected override Expression VisitMethodCall(MethodCallExpression node) + { + if (node.Method.Name == "FirstOrDefault") + { + Count++; + } + return base.VisitMethodCall(node); + } + } + + public partial class Article + { + public int Id { get; set; } + public int ProductId { get; set; } + public bool IsDefault { get; set; } + public short NationId { get; set; } + public virtual Product Product { get; set; } + } + + public partial class Product + { + public int Id { get; set; } + public string Name { get; set; } + public bool ECommercePublished { get; set; } + public virtual ICollection
Articles { get; set; } + public int Value { get; } + [NotMapped] + public int NotMappedValue { get; set; } + } + public partial class ProductA : Product + { + public string A { get; set; } + } + public partial class ProductB : Product + { + public string B { get; set; } + } + public class PriceModel + { + public int Id { get; set; } + public short RegionId { get; set; } + public bool IsDefault { get; set; } + } + + public class ProductModel + { + public int Id { get; set; } + public string Name { get; set; } + public PriceModel Price { get; set; } + } + public class ProductAModel : ProductModel + { + public string A { get; set; } + } + public class ProductBModel : ProductModel + { + public string B { get; set; } + } + + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(ClientContext context) + { + context.Products.Add(new ProductA { Name = "P1", A = "a", ECommercePublished = true, Articles = new[] { new Article { IsDefault = true, NationId = 1, ProductId = 1 } } }); + context.Products.Add(new ProductB { Name = "P2", B = "b", ECommercePublished = true, Articles = new[] { new Article { IsDefault = true, NationId = 1, ProductId = 1 } } }); + context.Products.Add(new Product { Name = "P3", ECommercePublished = true, Articles = new[] { new Article { IsDefault = true, NationId = 1, ProductId = 1 } } }); + } + } + + public class ClientContext : LocalDbContext + { + public DbSet Products { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity(); + modelBuilder.Entity(); + } + } +} +public class MapObjectPropertyFromSubQueryWithInnerObjectWithInheritance : IntegrationTest +{ + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap() + .Include() + .Include(); + + cfg.CreateMap(); + cfg.CreateMap(); + cfg.CreateMap() + .ForMember(d => d.Price, o => o.MapFrom(source => source.Articles.Where(x => x.IsDefault && source.ECommercePublished).FirstOrDefault())); + cfg.CreateMap() + .ForMember(d => d.RegionId, o => o.MapFrom(s => s.NationId)); + }); + + [Fact] + public void Should_cache_the_subquery() + { + using (var context = new ClientContext()) + { + var projection = ProjectTo(context.ProductArticles.OrderBy(p => p.Name)); + var counter = new FirstOrDefaultCounter(); + counter.Visit(projection.Expression); + counter.Count.ShouldBe(24); + var list = projection.ToList(); + + var productArticleAModel = list[0].ShouldBeOfType(); + productArticleAModel.Name.ShouldBe("P1"); + productArticleAModel.A.ShouldBe("a"); + var productModel = productArticleAModel.Product; + productModel.Price.RegionId.ShouldBe((short)1); + productModel.Price.IsDefault.ShouldBeTrue(); + var otherProductModel = productArticleAModel.OtherProduct; + otherProductModel.Price.RegionId.ShouldBe((short)2); + otherProductModel.Price.IsDefault.ShouldBeTrue(); + + var productArticleBModel = list[1].ShouldBeOfType(); + productArticleBModel.Name.ShouldBe("P2"); + productArticleBModel.B.ShouldBe("b"); + productModel = productArticleBModel.Product; + productModel.Price.RegionId.ShouldBe((short)3); + productModel.Price.IsDefault.ShouldBeTrue(); + otherProductModel = productArticleBModel.OtherProduct; + otherProductModel.Price.RegionId.ShouldBe((short)4); + otherProductModel.Price.IsDefault.ShouldBeTrue(); + + var productArticleModel = list[2].ShouldBeOfType(); + productArticleModel.Name.ShouldBe("P3"); + productModel = productArticleModel.Product; + productModel.Price.RegionId.ShouldBe((short)5); + productModel.Price.IsDefault.ShouldBeTrue(); + otherProductModel = productArticleModel.OtherProduct; + otherProductModel.Price.RegionId.ShouldBe((short)6); + otherProductModel.Price.IsDefault.ShouldBeTrue(); + } + } + + public class ProductArticle + { + public int Id { get; set; } + + public string Name { get; set; } + public Product Product { get; set; } + public Product OtherProduct { get; set; } + } + public class ProductArticleA : ProductArticle + { + public string A { get; set; } + } + public class ProductArticleB : ProductArticle + { + public string B { get; set; } + } + + public class ProductArticleModel + { + public int Id { get; set; } + public string Name { get; set; } + public ProductModel Product { get; set; } + public ProductModel OtherProduct { get; set; } + } + public class ProductArticleAModel : ProductArticleModel + { + public string A { get; set; } + } + public class ProductArticleBModel : ProductArticleModel + { + public string B { get; set; } + } + + public partial class Article + { + public int Id { get; set; } + public int ProductId { get; set; } + public bool IsDefault { get; set; } + public short NationId { get; set; } + public virtual Product Product { get; set; } + } + + public partial class Product + { + public int Id { get; set; } + public string Name { get; set; } + public bool ECommercePublished { get; set; } + public virtual ICollection
Articles { get; set; } + } + + public class PriceModel + { + public int Id { get; set; } + public short RegionId { get; set; } + public bool IsDefault { get; set; } + } + + public class ProductModel + { + public int Id { get; set; } + public PriceModel Price { get; set; } + } + + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(ClientContext context) + { + var product1 = context.Products.Add(new Product { ECommercePublished = true, Articles = new[] { new Article { IsDefault = true, NationId = 1, ProductId = 1 } } }); + var product2 = context.Products.Add(new Product { ECommercePublished = true, Articles = new[] { new Article { IsDefault = true, NationId = 2, ProductId = 2 } } }); + context.ProductArticles.Add(new ProductArticleA { A = "a", Name = "P1", Product = product1.Entity, OtherProduct = product2.Entity }); + + var product3 = context.Products.Add(new Product { ECommercePublished = true, Articles = new[] { new Article { IsDefault = true, NationId = 3, ProductId = 1 } } }); + var product4 = context.Products.Add(new Product { ECommercePublished = true, Articles = new[] { new Article { IsDefault = true, NationId = 4, ProductId = 2 } } }); + context.ProductArticles.Add(new ProductArticleB { B = "b", Name = "P2", Product = product3.Entity, OtherProduct = product4.Entity }); + + var product5 = context.Products.Add(new Product { ECommercePublished = true, Articles = new[] { new Article { IsDefault = true, NationId = 5, ProductId = 1 } } }); + var product6 = context.Products.Add(new Product { ECommercePublished = true, Articles = new[] { new Article { IsDefault = true, NationId = 6, ProductId = 2 } } }); + context.ProductArticles.Add(new ProductArticle { Name = "P3", Product = product5.Entity, OtherProduct = product6.Entity }); + } + } + + public class ClientContext : LocalDbContext + { + public DbSet Products { get; set; } + public DbSet ProductArticles { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity(); + modelBuilder.Entity(); + } + } +} +public class MapObjectPropertyFromSubQueryWithCollectionWithInheritance : IntegrationTest +{ + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap() + .Include() + .Include(); + cfg.CreateMap(); + cfg.CreateMap(); + cfg.CreateMap() + .ForMember(d => d.Price, o => o.MapFrom(source => source.Articles.Where(x => x.IsDefault && x.NationId == 1 && source.ECommercePublished).FirstOrDefault())); + cfg.CreateMap() + .ForMember(d => d.RegionId, o => o.MapFrom(s => s.NationId)); + }); + + [Fact] + public void Should_cache_the_subquery() + { + using (var context = new ClientContext()) + { + var projection = ProjectTo(context.ProductArticles.OrderBy(p => p.Name)); + var counter = new FirstOrDefaultCounter(); + counter.Visit(projection.Expression); + counter.Count.ShouldBe(12); + var productModel = projection.First().Products.First(); + productModel.Price.RegionId.ShouldBe((short)1); + productModel.Price.IsDefault.ShouldBeTrue(); + productModel.Price.Id.ShouldBe(1); + productModel.Id.ShouldBe(1); + } + } + + class FirstOrDefaultCounter : ExpressionVisitor + { + public int Count; + + protected override Expression VisitMethodCall(MethodCallExpression node) + { + if (node.Method.Name == "FirstOrDefault") + { + Count++; + } + return base.VisitMethodCall(node); + } + } + + public class ProductArticle + { + public int Id { get; set; } + public string Name { get; set; } + public ICollection Products { get; set; } + } + + public class ProductArticleA : ProductArticle + { + public string A { get; set; } + } + + public class ProductArticleB : ProductArticle + { + public string B { get; set; } + } + + public class ProductArticleModel + { + public int Id { get; set; } + public string Name { get; set; } + public ICollection Products { get; set; } + } + + public class ProductArticleAModel : ProductArticleModel + { + public string A { get; set; } + } + public class ProductArticleBModel : ProductArticleModel + { + public string B { get; set; } + } + + public partial class Article + { + public int Id { get; set; } + public int ProductId { get; set; } + public bool IsDefault { get; set; } + public short NationId { get; set; } + public virtual Product Product { get; set; } + } + + public partial class Product + { + public int Id { get; set; } + public string Name { get; set; } + public bool ECommercePublished { get; set; } + public virtual ICollection
Articles { get; set; } + } + + public class PriceModel + { + public int Id { get; set; } + public short RegionId { get; set; } + public bool IsDefault { get; set; } + } + + public class ProductModel + { + public int Id { get; set; } + public PriceModel Price { get; set; } + } + + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(ClientContext context) + { + var product = context.Products.Add(new Product { ECommercePublished = true, Articles = new[] { new Article { IsDefault = true, NationId = 1, ProductId = 1 } } }); + context.ProductArticles.Add(new ProductArticleA { Name = "P1", A = "a", Products = new[] { product.Entity } }); + + product = context.Products.Add(new Product { ECommercePublished = true, Articles = new[] { new Article { IsDefault = true, NationId = 1, ProductId = 1 } } }); + context.ProductArticles.Add(new ProductArticleB { Name = "P2", B = "b", Products = new[] { product.Entity } }); + + product = context.Products.Add(new Product { ECommercePublished = true, Articles = new[] { new Article { IsDefault = true, NationId = 1, ProductId = 1 } } }); + context.ProductArticles.Add(new ProductArticle { Name = "P3", Products = new[] { product.Entity } }); + } + } + + public class ClientContext : LocalDbContext + { + public DbSet Products { get; set; } + public DbSet ProductArticles { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity(); + modelBuilder.Entity(); + } + } +} +public class MapObjectPropertyFromSubQueryWithCollectionSameNameWithInheritance : NonValidatingSpecBase, IAsyncLifetime +{ + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap() + .Include() + .Include(); + cfg.CreateMap() + .ForMember(d => d.ECommerceProducts, o => o.MapFrom(source => source.Products.Where(p => p.ECommercePublished))); + cfg.CreateMap(); + cfg.CreateMap() + .ForMember(d => d.ArticlesModel, o => o.MapFrom(s => s)) + .ForMember(d => d.Articles, o => o.MapFrom(source => source.Articles.Where(x => x.IsDefault && x.NationId == 1 && source.ECommercePublished).FirstOrDefault())) + .Include(); + cfg.CreateMap(); + cfg.CreateMap() + .ForMember(d => d.RegionId, o => o.MapFrom(s => s.NationId)); + + cfg.CreateMap(); + }); + + [Fact] + public void Should_cache_the_subquery() + { + using (var context = new ClientContext()) + { + var projection = ProjectTo(context.ProductArticles.OrderBy(p => p.Name)); + var counter = new FirstOrDefaultCounter(); + counter.Visit(projection.Expression); + counter.Count.ShouldBe(16); + var ecommerce = projection.ToList().OfType().First(); + ecommerce.ECommerceProducts.Count.ShouldBe(1); + ecommerce.Products.Count.ShouldBe(2); + + var productModel = projection.First().Products.First(); + Check(productModel.Articles); + productModel.Id.ShouldBe(1); + productModel.ArticlesCount.ShouldBe(1); + productModel.ArticlesModel.Articles.Count.ShouldBe(1); + Check(productModel.ArticlesModel.Articles.Single()); + } + } + + private static void Check(PriceModel priceModel) + { + priceModel.RegionId.ShouldBe((short)1); + priceModel.IsDefault.ShouldBeTrue(); + priceModel.Id.ShouldBe(1); + } + + class FirstOrDefaultCounter : ExpressionVisitor + { + public int Count; + + protected override Expression VisitMethodCall(MethodCallExpression node) + { + if (node.Method.Name == "FirstOrDefault") + { + Count++; + } + return base.VisitMethodCall(node); + } + } + + public class ProductArticle + { + public int Id { get; set; } + public string Name { get; set; } + public ICollection Products { get; set; } + } + + public class ProductArticleA : ProductArticle + { + public string A { get; set; } + } + + public class ProductArticleB : ProductArticle + { + public string B { get; set; } + } + + public class ProductArticleModel + { + public int Id { get; set; } + public string Name { get; set; } + public ICollection Products { get; set; } + } + + public class ECommerceProductArticleModel : ProductArticleModel + { + public ICollection ECommerceProducts { get; set; } + } + + public class ProductArticleBModel : ProductArticleModel + { + public string B { get; set; } + } + + public partial class Article + { + public int Id { get; set; } + public int ProductId { get; set; } + public bool IsDefault { get; set; } + public short NationId { get; set; } + public virtual Product Product { get; set; } + } + + public partial class Product + { + public int Id { get; set; } + public string Name { get; set; } + public bool ECommercePublished { get; set; } + public virtual ICollection
Articles { get; set; } + } + + public class PriceModel + { + public int Id { get; set; } + public short RegionId { get; set; } + public bool IsDefault { get; set; } + } + + public class ProductModel + { + public int Id { get; set; } + public PriceModel Articles { get; set; } + public int ArticlesCount { get; set; } + public ArticlesModel ArticlesModel { get; set; } + } + + public class ECommerceProductModel : ProductModel + { + } + + public class ArticlesModel + { + public ICollection Articles { get; set; } + } + + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(ClientContext context) + { + var product = context.Products.Add(new Product { ECommercePublished = true, Articles = new[] { new Article { IsDefault = true, NationId = 1, ProductId = 1 } } }); + var product2 = context.Products.Add(new Product { ECommercePublished = false, Articles = new[] { new Article { IsDefault = true, NationId = 1, ProductId = 1 } } }); + context.ProductArticles.Add(new ProductArticleA { Name = "P1", A = "a", Products = new[] { product.Entity, product2.Entity } }); + product = context.Products.Add(new Product { ECommercePublished = true, Articles = new[] { new Article { IsDefault = true, NationId = 1, ProductId = 1 } } }); + context.ProductArticles.Add(new ProductArticleB { Name = "P2", B = "b", Products = new[] { product.Entity } }); + product = context.Products.Add(new Product { ECommercePublished = true, Articles = new[] { new Article { IsDefault = true, NationId = 1, ProductId = 1 } } }); + context.ProductArticles.Add(new ProductArticle { Name = "P3", Products = new[] { product.Entity } }); + } + } + + public class ClientContext : LocalDbContext + { + public DbSet Products { get; set; } + public DbSet ProductArticles { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity(); + modelBuilder.Entity(); + } + } + public async Task InitializeAsync() + { + var initializer = new DatabaseInitializer(); + + await initializer.Migrate(); + } + + public Task DisposeAsync() => Task.CompletedTask; +} +public class SubQueryWithMapFromNullableWithInheritance : IntegrationTest +{ + // Source Types + public class Cable + { + public int CableId { get; set; } + public string Name { get; set; } + public ICollection Ends { get; set; } = new List(); + } + public class CableA : Cable + { + public string A { get; set; } + } + public class CableB : Cable + { + public string B { get; set; } + } + + public class CableEnd + { + [ForeignKey(nameof(CrossConnectId))] + public virtual Cable CrossConnect { get; set; } + [Column(Order = 0), Key] + public int CrossConnectId { get; set; } + [Column(Order = 1), Key] + public string Name { get; set; } + [ForeignKey(nameof(RackId))] + public virtual Rack Rack { get; set; } + public int? RackId { get; set; } + } + + public class DataHall + { + public int DataHallId { get; set; } + public int DataCentreId { get; set; } + public ICollection Racks { get; set; } = new List(); + } + + public class Rack + { + public int RackId { get; set; } + [ForeignKey(nameof(DataHallId))] + public virtual DataHall DataHall { get; set; } + public int DataHallId { get; set; } + } + + // Dest Types + public class CableListModel + { + public int CableId { get; set; } + public CableEndModel AEnd { get; set; } + public CableEndModel AnotherEnd { get; set; } + } + public class CableListModelA : CableListModel + { + public string A { get; set; } + } + public class CableListModelB : CableListModel + { + public string B { get; set; } + } + + public class CableEndModel + { + public string Name { get; set; } + public int? DataHallId { get; set; } + } + + public class ClientContext : LocalDbContext + { + public DbSet Cables { get; set; } + public DbSet CableEnds { get; set; } + public DbSet DataHalls { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().HasKey(c => new { c.CrossConnectId, c.Name }); + modelBuilder.Entity(); + modelBuilder.Entity(); + } + } + + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(ClientContext context) + { + var rack = new Rack(); + var dh = new DataHall { DataCentreId = 10, Racks = { rack } }; + context.DataHalls.Add(dh); + + context.Cables.Add(new CableA + { + Name = "C1", + A = "a", + Ends = new List() + { + new CableEnd { Name = "A", Rack = rack}, + new CableEnd { Name = "B" }, + } + }); + context.Cables.Add(new CableB + { + Name = "C2", + B = "b", + Ends = new List() + { + new CableEnd { Name = "A", Rack = rack}, + new CableEnd { Name = "B" }, + } + }); + context.Cables.Add(new Cable + { + Name = "C3", + Ends = new List() + { + new CableEnd { Name = "A", Rack = rack}, + new CableEnd { Name = "B" }, + } + }); + } + } + + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap().ForMember(dest => dest.DataHallId, opt => opt.MapFrom(src => src.Rack.DataHall.DataCentreId)); + cfg.CreateMap() + .ForMember(dest => dest.AEnd, opt => opt.MapFrom(src => src.Ends.FirstOrDefault(x => x.Name == "A"))) + .ForMember(dest => dest.AnotherEnd, opt => opt.MapFrom(src => src.Ends.FirstOrDefault(x => x.Name == "B"))) + .Include() + .Include(); + cfg.CreateMap(); + cfg.CreateMap(); + }); + + [Fact] + public void Should_project_ok() + { + using (var context = new ClientContext()) + { + var projection = ProjectTo(context.Cables.OrderBy(c => c.Name)); + var list = projection.ToList(); + + var resultA = list[0].ShouldBeOfType(); + resultA.AEnd.DataHallId.ShouldBe(10); + resultA.AnotherEnd.DataHallId.ShouldBeNull(); + resultA.A.ShouldBe("a"); + + var resultB = list[1].ShouldBeOfType(); + resultB.AEnd.DataHallId.ShouldBe(10); + resultB.AnotherEnd.DataHallId.ShouldBeNull(); + resultB.B.ShouldBe("b"); + + var result = list[2].ShouldBeOfType(); + result.AEnd.DataHallId.ShouldBe(10); + result.AnotherEnd.DataHallId.ShouldBeNull(); + } + } +} +public class MapObjectPropertyFromSubQueryCustomSourceWithInheritance : IntegrationTest +{ + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap(); + cfg.CreateMap() + .ForMember(dest => dest.Owner, opt => opt.MapFrom(src => src.Owners.FirstOrDefault())); + cfg.CreateMap() + .ForMember(dest => dest.Brand, opt => opt.MapFrom(src => src.Product.Brand)) + .Include() + .Include(); + cfg.CreateMap(); + cfg.CreateMap(); + }); + + public class Owner + { + public int Id { get; set; } + public string Name { get; set; } + } + public class Brand + { + public int Id { get; set; } + public List Owners { get; set; } = new List(); + } + public class Product + { + public int Id { get; set; } + public Brand Brand { get; set; } + } + public class ProductReview + { + public int Id { get; set; } + public Product Product { get; set; } + } + public class ProductReviewA : ProductReview + { + public string A { get; set; } + } + public class ProductReviewB : ProductReview + { + public string B { get; set; } + } + /* Destination types */ + public class ProductReviewDto + { + public int Id { get; set; } + public BrandDto Brand { get; set; } + } + public class ProductReviewADto : ProductReviewDto + { + public string A { get; set; } + } + public class ProductReviewBDto : ProductReviewDto + { + public string B { get; set; } + } + public class BrandDto + { + public int Id { get; set; } + public OwnerDto Owner { get; set; } + } + public class OwnerDto + { + public int Id { get; set; } + public string Name { get; set; } + } + + public class ClientContext : LocalDbContext + { + public DbSet Owners { get; set; } + public DbSet Products { get; set; } + public DbSet Brands { get; set; } + public DbSet ProductReviews { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(); + modelBuilder.Entity(); + } + } + + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(ClientContext context) + { + context.ProductReviews.Add(new ProductReviewA + { A = "a", Product = new Product { Brand = new Brand { Owners = { new Owner { Name = "Owner" } } } } }); + context.ProductReviews.Add(new ProductReviewB + { B = "b", Product = new Product { Brand = new Brand { Owners = { new Owner() } } } }); + context.ProductReviews.Add(new ProductReview { Product = new Product() }); + } + } + + [Fact] + public void Should_project_ok() + { + using (var context = new ClientContext()) + { + var projection = ProjectTo(context.ProductReviews); + var results = projection.ToArray(); + results.Any(result => result?.Brand?.Owner?.Name == "Owner").ShouldBeTrue(); + results.Any(result => result?.Brand?.Owner == null).ShouldBeTrue(); + results.Any(result => result?.Brand == null).ShouldBeTrue(); + results.OfType().Any(result => result.A == "a").ShouldBeTrue(); + results.OfType().Any(result => result.B == "b").ShouldBeTrue(); + } + } } \ No newline at end of file diff --git a/src/IntegrationTests/ExplicitExpansion/ConstructorExplicitExpansionOverride.cs b/src/IntegrationTests/ExplicitExpansion/ConstructorExplicitExpansionOverride.cs new file mode 100644 index 0000000000..355a78d92c --- /dev/null +++ b/src/IntegrationTests/ExplicitExpansion/ConstructorExplicitExpansionOverride.cs @@ -0,0 +1,38 @@ +namespace AutoMapper.IntegrationTests.ExplicitExpansion; + +public class ConstructorExplicitExpansionOverride : IntegrationTest { + public class Entity { + public int Id { get; set; } + public string Name { get; set; } + } + + public class SubEntity : Entity { + } + + record Dto(string Name); + record SubDto(string Name) : Dto(Name) { } + + public class Context : LocalDbContext { + public DbSet Entities { get; set; } + public DbSet SubEntities { get; set; } + } + public class DatabaseInitializer : DropCreateDatabaseAlways { + protected override void Seed(Context context) { + context.Entities.Add(new() { Name = "base" }); + context.SubEntities.Add(new() { Name = "derived" }); + base.Seed(context); + } + } + protected override MapperConfiguration CreateConfiguration() => new(c => { + c.CreateMap().ForCtorParam("Name", o => o.ExplicitExpansion()); + c.CreateMap().IncludeBase().ForCtorParam("Name", o => o.ExplicitExpansion(false)); + }); + [Fact] + public void Should_work() { + using var context = new Context(); + var dtos = ProjectTo(context.Entities).ToList(); + dtos.Count.ShouldBe(2); + dtos[0].ShouldBeOfType().Name.ShouldBeNull(); + dtos[1].ShouldBeOfType().Name.ShouldBe("derived"); + } +} \ No newline at end of file diff --git a/src/IntegrationTests/ExplicitExpansion/ExpandCollectionsOverride.cs b/src/IntegrationTests/ExplicitExpansion/ExpandCollectionsOverride.cs new file mode 100644 index 0000000000..eefbca2db3 --- /dev/null +++ b/src/IntegrationTests/ExplicitExpansion/ExpandCollectionsOverride.cs @@ -0,0 +1,110 @@ +namespace AutoMapper.IntegrationTests.ExplicitExpansion; + +public class ExpandCollectionsOverride : IntegrationTest +{ + TrainingCourseDto _course; + + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap(); + cfg.CreateMap() + .ForMember(p => p.CourseName, opt => opt.ExplicitExpansion()); + cfg.CreateMap().ForMember(c => c.Category, o => o.ExplicitExpansion()); + cfg.CreateMap() + .IncludeBase() + .ForMember(c => c.CourseName, opt => opt.ExplicitExpansion(false)); + }); + + [Fact] + public void Should_notexpand_courseName() { + using (var context = new ClientContext()) { + _course = ProjectTo(context.TrainingCourses).FirstOrDefault(); + } + _course.CourseName.ShouldBeNull(); + } + + [Fact] + public void Should_expand_courseName() { + using (var context = new ClientContext()) { + _course = ProjectTo(context.TrainingCourses).FirstOrDefault(); + } + _course.CourseName.ShouldBe("Course 1"); + } + + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(ClientContext context) + { + var category = new Category { CategoryName = "Category 1" }; + var course = new TrainingCourse { CourseName = "Course 1" }; + context.TrainingCourses.Add(course); + var content = new TrainingContent { ContentName = "Content 1", Category = category }; + context.TrainingContents.Add(content); + course.Content.Add(content); + } + } + + public class ClientContext : LocalDbContext + { + public DbSet Categories { get; set; } + public DbSet TrainingCourses { get; set; } + public DbSet TrainingContents { get; set; } + } + + public class TrainingCourse + { + [Key] + public int CourseId { get; set; } + + public string CourseName { get; set; } + + public virtual IList Content { get; set; } = new List(); + } + + public class TrainingContent + { + [Key] + public int ContentId { get; set; } + + public string ContentName { get; set; } + public string CaptionName { get; set; } + + public Category Category { get; set; } + } + + public class Category + { + public int CategoryId { get; set; } + public string CategoryName { get; set; } + } + + + public class TrainingCourseDto + { + public int CourseId { get; set; } + + public string CourseName { get; set; } + + public virtual IList Content { get; set; } + } + + public class TrainingCourseDetailDto : TrainingCourseDto + { + } + + public class CategoryDto + { + public int CategoryId { get; set; } + public string CategoryName { get; set; } + } + + public class TrainingContentDto + { + public int ContentId { get; set; } + + public string ContentName { get; set; } + + public CategoryDto Category { get; set; } + } + +} \ No newline at end of file diff --git a/src/IntegrationTests/IncludeMembers.cs b/src/IntegrationTests/IncludeMembers.cs index a1474dc9f5..3f92395e57 100644 --- a/src/IntegrationTests/IncludeMembers.cs +++ b/src/IntegrationTests/IncludeMembers.cs @@ -1005,4 +1005,2383 @@ public void Should_flatten() result.TheField.ShouldBe(2); } } +} +public class IncludeMembersWithIheritance : IntegrationTest +{ + public class Source + { + public int Id { get; set; } + public string Name { get; set; } + public InnerSource InnerSource { get; set; } + public OtherInnerSource OtherInnerSource { get; set; } + } + public class SourceA : Source + { + public InnerSourceA InnerSourceA { get; set; } + } + public class SourceB : Source + { + public string B { get; set; } + } + public class InnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + } + public class InnerSourceA + { + public int Id { get; set; } + public string A { get; set; } + } + public class OtherInnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public string Title { get; set; } + } + public class Destination + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public string Title { get; set; } + } + public class DestinationA : Destination + { + public string A { get; set; } + } + public class DestinationB : Destination + { + public string B { get; set; } + } + public class Context : LocalDbContext + { + public DbSet Sources { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity(); + modelBuilder.Entity(); + } + } + + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(Context context) + { + var sourceA = new SourceA { Name = "name1", InnerSourceA = new InnerSourceA() { A = "a" }, InnerSource = new InnerSource { Description = "description" }, OtherInnerSource = new OtherInnerSource { Title = "title" } }; + context.Sources.Add(sourceA); + var sourceB = new SourceB { Name = "name2", B = "b", InnerSource = new InnerSource { Description = "description" }, OtherInnerSource = new OtherInnerSource { Title = "title" } }; + context.Sources.Add(sourceB); + var sourceC = new Source { Name = "name3", InnerSource = new InnerSource { Description = "description" }, OtherInnerSource = new OtherInnerSource { Title = "title" } }; + context.Sources.Add(sourceC); + base.Seed(context); + } + } + + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap().IncludeMembers(s => s.InnerSource, s => s.OtherInnerSource) + .Include() + .Include(); + cfg.CreateMap().IncludeMembers(s => s.InnerSourceA); + cfg.CreateMap(); + cfg.CreateMap(MemberList.None); + cfg.CreateMap(MemberList.None); + cfg.CreateMap(MemberList.None); + }); + [Fact] + public void Should_flatten() + { + using (var context = new Context()) + { + var projectTo = ProjectTo(context.Sources.OrderBy(p => p.Name)); + var list = projectTo.ToList(); + + var resultA = list[0].ShouldBeOfType(); + resultA.Name.ShouldBe("name1"); + resultA.Description.ShouldBe("description"); + resultA.Title.ShouldBe("title"); + resultA.A.ShouldBe("a"); + + var resultB = list[1].ShouldBeOfType(); + resultB.Name.ShouldBe("name2"); + resultB.Description.ShouldBe("description"); + resultB.Title.ShouldBe("title"); + resultB.B.ShouldBe("b"); + + var resultC = list[2].ShouldBeOfType(); + resultC.Name.ShouldBe("name3"); + resultC.Description.ShouldBe("description"); + resultC.Title.ShouldBe("title"); + } + } +} +public class IncludeMembersExplicitExpansionWithIheritance : IntegrationTest +{ + public class Source + { + public int Id { get; set; } + public string Name { get; set; } + public InnerSource InnerSource { get; set; } + public OtherInnerSource OtherInnerSource { get; set; } + } + public class SourceA : Source + { + public InnerSourceA InnerSourceA { get; set; } + } + public class SourceB : Source + { + public string B { get; set; } + } + public class InnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + } + public class InnerSourceA + { + public int Id { get; set; } + public string A { get; set; } + } + public class OtherInnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public string Title { get; set; } + } + public class Destination + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public string Title { get; set; } + } + public class DestinationA : Destination + { + public string A { get; set; } + } + public class DestinationB : Destination + { + public string B { get; set; } + } + public class Context : LocalDbContext + { + public DbSet Sources { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity(); + modelBuilder.Entity(); + } + } + + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(Context context) + { + var sourceA = new SourceA { Name = "name1", InnerSourceA = new InnerSourceA { A = "a" }, InnerSource = new InnerSource { Description = "description" }, OtherInnerSource = new OtherInnerSource { Title = "title" } }; + context.Sources.Add(sourceA); + var sourceB = new SourceB { Name = "name2", B = "b", InnerSource = new InnerSource { Description = "description" }, OtherInnerSource = new OtherInnerSource { Title = "title" } }; + context.Sources.Add(sourceB); + var source = new Source { Name = "name3", InnerSource = new InnerSource { Description = "description" }, OtherInnerSource = new OtherInnerSource { Title = "title" } }; + context.Sources.Add(source); + base.Seed(context); + } + } + + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap().IncludeMembers(s => s.InnerSource, s => s.OtherInnerSource) + .Include() + .Include(); + cfg.CreateMap().IncludeMembers(s => s.InnerSourceA); + cfg.CreateMap(); + cfg.CreateProjection(MemberList.None).ForMember(d => d.Description, o => o.ExplicitExpansion()); + cfg.CreateProjection(MemberList.None).ForMember(d => d.Title, o => o.ExplicitExpansion()); + cfg.CreateProjection(MemberList.None).ForMember(d => d.A, o => o.ExplicitExpansion()); + }); + [Fact] + public void Should_flatten() + { + using (var context = new Context()) + { + var projectTo = ProjectTo(context.Sources.OrderBy(p => p.Name), null, d => d.Title, d => d.GetType() == typeof(DestinationA) ? ((DestinationA)d).A : null); + var list = projectTo.ToList(); + + var resultA = list[0].ShouldBeOfType(); + resultA.Name.ShouldBe("name1"); + resultA.Description.ShouldBeNull(); + resultA.Title.ShouldBe("title"); + resultA.A.ShouldBe("a"); + + var resultB = list[1].ShouldBeOfType(); + resultB.Name.ShouldBe("name2"); + resultB.Description.ShouldBeNull(); + resultB.Title.ShouldBe("title"); + resultB.B.ShouldBe("b"); + + var result = list[2].ShouldBeOfType(); + result.Name.ShouldBe("name3"); + result.Description.ShouldBeNull(); + result.Title.ShouldBe("title"); + } + } +} +public class IncludeMembersFirstOrDefaultWithIheritance : IntegrationTest +{ + public class Source + { + public int Id { get; set; } + public string Name { get; set; } + public List InnerSources { get; set; } = new List(); + public List OtherInnerSources { get; set; } = new List(); + } + public class SourceA : Source + { + public List InnerSourcesA { get; set; } = new List(); + } + public class SourceB : Source + { + public string B { get; set; } + } + public class InnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public string Publisher { get; set; } + } + public class InnerSourceA + { + public int Id { get; set; } + public string A { get; set; } + } + public class OtherInnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public string Title { get; set; } + public string Author { get; set; } + } + public class Destination + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public string Title { get; set; } + public string Author { get; set; } + public string Publisher { get; set; } + } + public class DestinationA : Destination + { + public string A { get; set; } + } + public class DestinationB : Destination + { + public string B { get; set; } + } + public class Context : LocalDbContext + { + public DbSet Sources { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity(); + modelBuilder.Entity(); + } + } + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(Context context) + { + var sourceA = new SourceA + { + Name = "name1", + InnerSourcesA = { new InnerSourceA { A = "a" } }, + InnerSources = { new InnerSource { Description = "description", Publisher = "publisher" } }, + OtherInnerSources = { new OtherInnerSource { Title = "title", Author = "author" } } + }; + context.Sources.Add(sourceA); + var sourceB = new SourceB + { + Name = "name2", + B = "b", + InnerSources = { new InnerSource { Description = "description", Publisher = "publisher" } }, + OtherInnerSources = { new OtherInnerSource { Title = "title", Author = "author" } } + }; + context.Sources.Add(sourceB); + var source = new Source + { + Name = "name3", + InnerSources = { new InnerSource { Description = "description", Publisher = "publisher" } }, + OtherInnerSources = { new OtherInnerSource { Title = "title", Author = "author" } } + }; + context.Sources.Add(source); + base.Seed(context); + } + } + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap().IncludeMembers(s => s.InnerSources.FirstOrDefault(), s => s.OtherInnerSources.FirstOrDefault()) + .Include() + .Include(); + cfg.CreateMap().IncludeMembers(s => s.InnerSourcesA.FirstOrDefault()); + cfg.CreateMap(); + cfg.CreateMap(MemberList.None); + cfg.CreateMap(MemberList.None); + cfg.CreateMap(MemberList.None); + }); + [Fact] + public void Should_flatten() + { + using (var context = new Context()) + { + var projectTo = ProjectTo(context.Sources.OrderBy(p => p.Name)); + FirstOrDefaultCounter.Assert(projectTo, 13); + var list = projectTo.ToList(); + + var resultA = list[0].ShouldBeOfType(); + resultA.Name.ShouldBe("name1"); + resultA.Description.ShouldBe("description"); + resultA.Title.ShouldBe("title"); + resultA.Author.ShouldBe("author"); + resultA.Publisher.ShouldBe("publisher"); + resultA.A.ShouldBe("a"); + + var resultB = list[1].ShouldBeOfType(); + resultB.Name.ShouldBe("name2"); + resultB.Description.ShouldBe("description"); + resultB.Title.ShouldBe("title"); + resultB.Author.ShouldBe("author"); + resultB.Publisher.ShouldBe("publisher"); + resultB.B.ShouldBe("b"); + + var result = list[2].ShouldBeOfType(); + result.Name.ShouldBe("name3"); + result.Description.ShouldBe("description"); + result.Title.ShouldBe("title"); + result.Author.ShouldBe("author"); + result.Publisher.ShouldBe("publisher"); + } + } +} +public class IncludeMembersFirstOrDefaultMixedPolymorhism : IntegrationTest +{ + public class Source + { + public int Id { get; set; } + public string Name { get; set; } + public List InnerSources { get; set; } = new List(); + public List OtherInnerSources { get; set; } = new List(); + } + public class SourceA : Source + { + public List InnerSourcesA { get; set; } = new List(); + } + public class SourceB : Source + { + public string B { get; set; } + } + public class InnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public string Publisher { get; set; } + } + public class InnerSourceA + { + public int Id { get; set; } + public string A { get; set; } + } + public class OtherInnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public string Title { get; set; } + public string Author { get; set; } + } + public class Destination + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public string Title { get; set; } + public string Author { get; set; } + public string Publisher { get; set; } + } + public class DestinationA : Destination + { + public string A { get; set; } + } + public class DestinationB : Destination + { + public string B { get; set; } + } + public class Context : LocalDbContext + { + public DbSet Sources { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity(); + modelBuilder.Entity(); + } + } + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(Context context) + { + var sourceA = new SourceA + { + Name = "name1", + InnerSourcesA = { new InnerSourceA { A = "a" } }, + InnerSources = { new InnerSource { Description = "description", Publisher = "publisher" } }, + OtherInnerSources = { new OtherInnerSource { Title = "title", Author = "author" } } + }; + context.Sources.Add(sourceA); + var sourceB = new SourceB + { + Name = "name2", + B = "b", + InnerSources = { new InnerSource { Description = "description", Publisher = "publisher" } }, + OtherInnerSources = { new OtherInnerSource { Title = "title", Author = "author" } } + }; + context.Sources.Add(sourceB); + var source = new Source + { + Name = "name3", + InnerSources = { new InnerSource { Description = "description", Publisher = "publisher" } }, + OtherInnerSources = { new OtherInnerSource { Title = "title", Author = "author" } } + }; + context.Sources.Add(source); + base.Seed(context); + } + } + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap().IncludeMembers(s => s.InnerSources.FirstOrDefault(), s => s.OtherInnerSources.FirstOrDefault()) + .Include() + .Include() + .Include() + .Include(); + cfg.CreateMap().IncludeMembers(s => s.InnerSourcesA.FirstOrDefault()); + cfg.CreateMap(); + cfg.CreateMap(); + cfg.CreateMap().ForMember(s => s.B, o => o.MapFrom(s => "b")); + cfg.CreateMap(MemberList.None); + cfg.CreateMap(MemberList.None); + cfg.CreateMap(MemberList.None); + cfg.CreateMap(MemberList.None); + }); + [Fact] + public void Should_flatten() + { + using (var context = new Context()) + { + var projectTo = ProjectTo(context.Sources.OrderBy(p => p.Name)); + var list = projectTo.ToList(); + + var resultA = list[0].ShouldBeOfType(); + resultA.Name.ShouldBe("name1"); + resultA.Description.ShouldBe("description"); + resultA.Title.ShouldBe("title"); + resultA.Author.ShouldBe("author"); + resultA.Publisher.ShouldBe("publisher"); + resultA.A.ShouldBe("a"); + + var resultB = list[1].ShouldBeOfType(); + resultB.Name.ShouldBe("name2"); + resultB.Description.ShouldBe("description"); + resultB.Title.ShouldBe("title"); + resultB.Author.ShouldBe("author"); + resultB.Publisher.ShouldBe("publisher"); + resultB.B.ShouldBe("b"); + + var result = list[2].ShouldBeOfType(); + result.Name.ShouldBe("name3"); + result.Description.ShouldBe("description"); + result.Title.ShouldBe("title"); + result.Author.ShouldBe("author"); + result.Publisher.ShouldBe("publisher"); + } + } +} +public class IncludeMembersFirstOrDefaultNoPolymorhism : IntegrationTest +{ + public class Source + { + public int Id { get; set; } + public string Name { get; set; } + public List InnerSources { get; set; } = new List(); + public List OtherInnerSources { get; set; } = new List(); + } + public class SourceA : Source + { + public List InnerSourcesA { get; set; } = new List(); + } + public class SourceB : Source + { + public string B { get; set; } + } + public class InnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public string Publisher { get; set; } + } + public class InnerSourceA + { + public int Id { get; set; } + public string A { get; set; } + } + public class OtherInnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public string Title { get; set; } + public string Author { get; set; } + } + public class Destination + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public string Title { get; set; } + public string Author { get; set; } + public string Publisher { get; set; } + } + public class DestinationA : Destination + { + public string A { get; set; } + } + public class DestinationB : Destination + { + public string B { get; set; } + } + public class Context : LocalDbContext + { + public DbSet Sources { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity(); + modelBuilder.Entity(); + } + } + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(Context context) + { + var sourceA = new SourceA + { + Name = "name1", + InnerSourcesA = { new InnerSourceA { A = "a" } }, + InnerSources = { new InnerSource { Description = "description", Publisher = "publisher" } }, + OtherInnerSources = { new OtherInnerSource { Title = "title", Author = "author" } } + }; + context.Sources.Add(sourceA); + var sourceB = new SourceB + { + Name = "name2", + B = "b", + InnerSources = { new InnerSource { Description = "description", Publisher = "publisher" } }, + OtherInnerSources = { new OtherInnerSource { Title = "title", Author = "author" } } + }; + context.Sources.Add(sourceB); + var source = new Source + { + Name = "name3", + InnerSources = { new InnerSource { Description = "description", Publisher = "publisher" } }, + OtherInnerSources = { new OtherInnerSource { Title = "title", Author = "author" } } + }; + context.Sources.Add(source); + base.Seed(context); + } + } + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap().IncludeMembers(s => s.InnerSources.FirstOrDefault(), s => s.OtherInnerSources.FirstOrDefault()) + .Include() + .Include(); + cfg.CreateMap().ForMember(d=>d.Description, o=>o.MapFrom(s=>"descriptionA")); + cfg.CreateMap().ForMember(s => s.B, o => o.MapFrom(s => "b")); + cfg.CreateMap(MemberList.None); + cfg.CreateMap(MemberList.None); + cfg.CreateMap(MemberList.None); + cfg.CreateMap(MemberList.None); + }); + [Fact] + public void Should_flatten() + { + using (var context = new Context()) + { + var projectTo = ProjectTo(context.Sources.OrderBy(p => p.Name)); + var list = projectTo.ToList(); + + var resultA = list[0].ShouldBeOfType(); + resultA.Name.ShouldBe("name1"); + resultA.Description.ShouldBe("descriptionA"); + resultA.Title.ShouldBe("title"); + resultA.Author.ShouldBe("author"); + resultA.Publisher.ShouldBe("publisher"); + + var resultB = list[1].ShouldBeOfType(); + resultB.Name.ShouldBe("name2"); + resultB.Description.ShouldBe("description"); + resultB.Title.ShouldBe("title"); + resultB.Author.ShouldBe("author"); + resultB.Publisher.ShouldBe("publisher"); + + var result = list[2].ShouldBeOfType(); + result.Name.ShouldBe("name3"); + result.Description.ShouldBe("description"); + result.Title.ShouldBe("title"); + result.Author.ShouldBe("author"); + result.Publisher.ShouldBe("publisher"); + } + } +} +public class IncludeMembersFirstOrDefaultWithMapFromExpressionWithIheritance : IntegrationTest +{ + public class Source + { + public int Id { get; set; } + public string Name { get; set; } + public List InnerSources { get; set; } = new List(); + public List OtherInnerSources { get; set; } = new List(); + } + public class SourceA : Source + { + public List InnerSourcesA { get; set; } = new List(); + } + public class SourceB : Source + { + public string B { get; set; } + } + public class InnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public string Description1 { get; set; } + public string Publisher { get; set; } + } + + public class InnerSourceA + { + public int Id { get; set; } + public string A { get; set; } + } + public class OtherInnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public string Title1 { get; set; } + public string Author { get; set; } + } + public class Destination + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public string Title { get; set; } + public string Author { get; set; } + public string Publisher { get; set; } + } + public class DestinationA : Destination + { + public string A { get; set; } + } + public class DestinationB : Destination + { + public string B { get; set; } + } + public class Context : LocalDbContext + { + public DbSet Sources { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity(); + modelBuilder.Entity(); + } + } + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(Context context) + { + var sourceA = new SourceA + { + Name = "name1", + InnerSourcesA = { new InnerSourceA { A = "a" } }, + InnerSources = { new InnerSource { Description1 = "description", Publisher = "publisher" } }, + OtherInnerSources = { new OtherInnerSource { Title1 = "title", Author = "author" } } + }; + context.Sources.Add(sourceA); + var sourceB = new SourceB + { + Name = "name2", + B = "b", + InnerSources = { new InnerSource { Description1 = "description", Publisher = "publisher" } }, + OtherInnerSources = { new OtherInnerSource { Title1 = "title", Author = "author" } } + }; + context.Sources.Add(sourceB); + var source = new Source + { + Name = "name3", + InnerSources = { new InnerSource { Description1 = "description", Publisher = "publisher" } }, + OtherInnerSources = { new OtherInnerSource { Title1 = "title", Author = "author" } } + }; + context.Sources.Add(source); + base.Seed(context); + } + } + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap().IncludeMembers(s => s.InnerSources.FirstOrDefault(), s => s.OtherInnerSources.FirstOrDefault()) + .Include() + .Include(); + cfg.CreateMap().IncludeMembers(s => s.InnerSourcesA.FirstOrDefault()); + cfg.CreateMap(); + cfg.CreateMap(MemberList.None).ForMember(d => d.Description, o => o.MapFrom(s => s.Description1)); + cfg.CreateMap(MemberList.None).ForMember(d => d.Title, o => o.MapFrom(s => s.Title1)); + cfg.CreateMap(MemberList.None).ForMember(d => d.A, o => o.MapFrom(s => s.A)); + }); + [Fact] + public void Should_flatten() + { + using (var context = new Context()) + { + var projectTo = ProjectTo(context.Sources.OrderBy(p => p.Name)); + FirstOrDefaultCounter.Assert(projectTo, 13); + var list = projectTo.ToList(); + + var resultA = list[0].ShouldBeOfType(); + resultA.Name.ShouldBe("name1"); + resultA.Description.ShouldBe("description"); + resultA.Title.ShouldBe("title"); + resultA.Author.ShouldBe("author"); + resultA.Publisher.ShouldBe("publisher"); + resultA.A.ShouldBe("a"); + + var resultB = list[1].ShouldBeOfType(); + resultB.Name.ShouldBe("name2"); + resultB.Description.ShouldBe("description"); + resultB.Title.ShouldBe("title"); + resultB.Author.ShouldBe("author"); + resultB.Publisher.ShouldBe("publisher"); + resultB.B.ShouldBe("b"); + + var result = list[2].ShouldBeOfType(); + result.Name.ShouldBe("name3"); + result.Description.ShouldBe("description"); + result.Title.ShouldBe("title"); + result.Author.ShouldBe("author"); + result.Publisher.ShouldBe("publisher"); + } + } +} +public class IncludeMembersFirstOrDefaultWithSubqueryMapFromWithIheritance : IntegrationTest +{ + public class Source + { + public int Id { get; set; } + public string Name { get; set; } + public List InnerSources { get; set; } = new List(); + public List OtherInnerSources { get; set; } = new List(); + } + public class SourceA : Source + { + public List InnerSourcesA { get; set; } = new List(); + } + public class SourceB : Source + { + public string B { get; set; } + } + public class InnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public List InnerSourceDetails { get; } = new List(); + } + public class InnerSourceA + { + public int Id { get; set; } + public List InnerSourceDetails { get; } = new List(); + } + + public class InnerSourceDetails + { + public int Id { get; set; } + public string Description { get; set; } + public string Publisher { get; set; } + } + public class InnerSourceDetailsA + { + public int Id { get; set; } + public string A { get; set; } + } + public class OtherInnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public List OtherInnerSourceDetails { get; } = new List(); + } + public class OtherInnerSourceDetails + { + public int Id { get; set; } + public string Title { get; set; } + public string Author { get; set; } + } + public class Destination + { + public int Id { get; set; } + public string Name { get; set; } + public DestinationDetails Details { get; set; } + public OtherDestinationDetails OtherDetails { get; set; } + } + public class DestinationA : Destination + { + public DestinationADetails DetailsA { get; set; } + } + public class DestinationB : Destination + { + public string B { get; set; } + } + public class DestinationDetails + { + public string Description { get; set; } + public string Publisher { get; set; } + } + public class DestinationADetails + { + public string A { get; set; } + } + public class OtherDestinationDetails + { + public string Title { get; set; } + public string Author { get; set; } + } + public class Context : LocalDbContext + { + public DbSet Sources { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity(); + modelBuilder.Entity(); + } + } + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(Context context) + { + var sourceA = new SourceA + { + Name = "name1", + InnerSourcesA = { new InnerSourceA { InnerSourceDetails = { new InnerSourceDetailsA { A = "a" } } } }, + InnerSources = { new InnerSource { InnerSourceDetails = { new InnerSourceDetails { Description = "description", Publisher = "publisher" } } } }, + OtherInnerSources = { new OtherInnerSource { OtherInnerSourceDetails = { new OtherInnerSourceDetails { Title = "title", Author = "author" } } } } + }; + context.Sources.Add(sourceA); + var sourceB = new SourceB + { + Name = "name2", + B = "b", + InnerSources = { new InnerSource { InnerSourceDetails = { new InnerSourceDetails { Description = "description", Publisher = "publisher" } } } }, + OtherInnerSources = { new OtherInnerSource { OtherInnerSourceDetails = { new OtherInnerSourceDetails { Title = "title", Author = "author" } } } } + }; + context.Sources.Add(sourceB); + var source = new Source + { + Name = "name3", + InnerSources = { new InnerSource { InnerSourceDetails = { new InnerSourceDetails { Description = "description", Publisher = "publisher" } } } }, + OtherInnerSources = { new OtherInnerSource { OtherInnerSourceDetails = { new OtherInnerSourceDetails { Title = "title", Author = "author" } } } } + }; + context.Sources.Add(source); + base.Seed(context); + } + } + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap().IncludeMembers(s => s.InnerSources.FirstOrDefault(), s => s.OtherInnerSources.FirstOrDefault()) + .Include() + .Include(); + cfg.CreateMap().IncludeMembers(s => s.InnerSourcesA.FirstOrDefault()); + cfg.CreateMap(); + cfg.CreateMap(MemberList.None).ForMember(d => d.Details, o => o.MapFrom(s => s.InnerSourceDetails.FirstOrDefault())); + cfg.CreateMap(MemberList.None).ForMember(d => d.OtherDetails, o => o.MapFrom(s => s.OtherInnerSourceDetails.FirstOrDefault())); + cfg.CreateMap(MemberList.None).ForMember(d => d.DetailsA, o => o.MapFrom(s => s.InnerSourceDetails.FirstOrDefault())); + cfg.CreateMap(); + cfg.CreateMap(); + cfg.CreateMap(); + }); + [Fact] + public void Should_flatten() + { + using (var context = new Context()) + { + var projectTo = ProjectTo(context.Sources.OrderBy(p => p.Name)); + FirstOrDefaultCounter.Assert(projectTo, 40); + var list = projectTo.ToList(); + + var resultA = list[0].ShouldBeOfType(); + resultA.Name.ShouldBe("name1"); + resultA.Details.Description.ShouldBe("description"); + resultA.Details.Publisher.ShouldBe("publisher"); + resultA.OtherDetails.Title.ShouldBe("title"); + resultA.OtherDetails.Author.ShouldBe("author"); + resultA.DetailsA.A.ShouldBe("a"); + + var resultB = list[1].ShouldBeOfType(); + resultB.Name.ShouldBe("name2"); + resultB.Details.Description.ShouldBe("description"); + resultB.Details.Publisher.ShouldBe("publisher"); + resultB.OtherDetails.Title.ShouldBe("title"); + resultB.OtherDetails.Author.ShouldBe("author"); + resultB.B.ShouldBe("b"); + + var result = list[2].ShouldBeOfType(); + result.Name.ShouldBe("name3"); + result.Details.Description.ShouldBe("description"); + result.Details.Publisher.ShouldBe("publisher"); + result.OtherDetails.Title.ShouldBe("title"); + result.OtherDetails.Author.ShouldBe("author"); + } + } +} +public class IncludeMembersSelectFirstOrDefaultWithSubqueryMapFromWithIheritance : IntegrationTest +{ + public class Source + { + public int Id { get; set; } + public string Name { get; set; } + public List InnerSourceWrappers { get; set; } = new List(); + public List OtherInnerSources { get; set; } = new List(); + } + public class SourceA : Source + { + public List InnerSourceWrappersA { get; set; } = new List(); + } + public class SourceB : Source + { + public string B { get; set; } + } + public class InnerSourceWrapper + { + public int Id { get; set; } + public InnerSource InnerSource { get; set; } + } + public class InnerSourceWrapperA + { + public int Id { get; set; } + public InnerSourceA InnerSource { get; set; } + } + public class InnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public List InnerSourceDetailsWrapper { get; } = new List(); + } + public class InnerSourceA + { + public int Id { get; set; } + public string Name { get; set; } + public List InnerSourceDetailsWrapperA { get; } = new List(); + } + public class InnerSourceDetailsWrapper + { + public int Id { get; set; } + public InnerSourceDetails InnerSourceDetails { get; set; } + } + public class InnerSourceDetailsWrapperA + { + public int Id { get; set; } + public InnerSourceDetailsA InnerSourceDetailsA { get; set; } + } + public class InnerSourceDetails + { + public int Id { get; set; } + public string Description { get; set; } + public string Publisher { get; set; } + } + public class InnerSourceDetailsA + { + public int Id { get; set; } + public string A { get; set; } + } + public class OtherInnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public List OtherInnerSourceDetails { get; } = new List(); + } + public class OtherInnerSourceDetails + { + public int Id { get; set; } + public string Title { get; set; } + public string Author { get; set; } + } + public class Destination + { + public int Id { get; set; } + public string Name { get; set; } + public DestinationDetails Details { get; set; } + public OtherDestinationDetails OtherDetails { get; set; } + } + public class DestinationA : Destination + { + public DestinationDetailsA DetailsA { get; set; } + } + public class DestinationB : Destination + { + public string B { get; set; } + } + public class DestinationDetails + { + public string Description { get; set; } + public string Publisher { get; set; } + } + public class DestinationDetailsA + { + public string A { get; set; } + } + public class OtherDestinationDetails + { + public string Title { get; set; } + public string Author { get; set; } + } + public class Context : LocalDbContext + { + public DbSet Sources { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity(); + modelBuilder.Entity(); + } + } + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(Context context) + { + var sourceA = new SourceA + { + Name = "name1", + InnerSourceWrappersA = { new InnerSourceWrapperA { InnerSource = new InnerSourceA { InnerSourceDetailsWrapperA = { new InnerSourceDetailsWrapperA { InnerSourceDetailsA = new InnerSourceDetailsA { A = "a" } } } } } }, + InnerSourceWrappers = { new InnerSourceWrapper { InnerSource = new InnerSource { InnerSourceDetailsWrapper = { new InnerSourceDetailsWrapper { InnerSourceDetails = new InnerSourceDetails { Description = "description", Publisher = "publisher" } } } } } }, + OtherInnerSources = { new OtherInnerSource { OtherInnerSourceDetails = { new OtherInnerSourceDetails { Title = "title", Author = "author" } } } } + }; + context.Sources.Add(sourceA); + var sourceB = new SourceB + { + Name = "name2", + B = "b", + InnerSourceWrappers = { new InnerSourceWrapper { InnerSource = new InnerSource { InnerSourceDetailsWrapper = { new InnerSourceDetailsWrapper { InnerSourceDetails = new InnerSourceDetails { Description = "description", Publisher = "publisher" } } } } } }, + OtherInnerSources = { new OtherInnerSource { OtherInnerSourceDetails = { new OtherInnerSourceDetails { Title = "title", Author = "author" } } } } + }; + context.Sources.Add(sourceB); + var source = new Source + { + Name = "name3", + InnerSourceWrappers = { new InnerSourceWrapper { InnerSource = new InnerSource { InnerSourceDetailsWrapper = { new InnerSourceDetailsWrapper { InnerSourceDetails = new InnerSourceDetails { Description = "description", Publisher = "publisher" } } } } } }, + OtherInnerSources = { new OtherInnerSource { OtherInnerSourceDetails = { new OtherInnerSourceDetails { Title = "title", Author = "author" } } } } + }; + context.Sources.Add(source); + base.Seed(context); + } + } + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap().IncludeMembers(s => s.InnerSourceWrappers.Select(s => s.InnerSource).FirstOrDefault(), s => s.OtherInnerSources.Select(s => s).FirstOrDefault()) + .Include() + .Include(); + cfg.CreateMap().IncludeMembers(s => s.InnerSourceWrappersA.Select(s => s.InnerSource).FirstOrDefault()); + cfg.CreateMap(); + cfg.CreateMap(MemberList.None).ForMember(d => d.Details, o => o.MapFrom(s => s.InnerSourceDetailsWrapper.Select(s => s.InnerSourceDetails).FirstOrDefault())); + cfg.CreateMap(MemberList.None).ForMember(d => d.OtherDetails, o => o.MapFrom(s => s.OtherInnerSourceDetails.Select(s => s).FirstOrDefault())); + cfg.CreateMap(MemberList.None).ForMember(d => d.DetailsA, o => o.MapFrom(s => s.InnerSourceDetailsWrapperA.Select(s => s.InnerSourceDetailsA).FirstOrDefault())); + cfg.CreateMap(); + cfg.CreateMap(); + cfg.CreateMap(); + }); + [Fact] + public void Should_flatten() + { + using (var context = new Context()) + { + var projectTo = ProjectTo(context.Sources.OrderBy(p => p.Name)); + FirstOrDefaultCounter.Assert(projectTo, 40); + var list = projectTo.ToList(); + + var resultA = list[0].ShouldBeOfType(); + resultA.Name.ShouldBe("name1"); + resultA.Details.Description.ShouldBe("description"); + resultA.Details.Publisher.ShouldBe("publisher"); + resultA.OtherDetails.Title.ShouldBe("title"); + resultA.OtherDetails.Author.ShouldBe("author"); + resultA.DetailsA.A.ShouldBe("a"); + + var resultB = list[1].ShouldBeOfType(); + resultB.Name.ShouldBe("name2"); + resultB.Details.Description.ShouldBe("description"); + resultB.Details.Publisher.ShouldBe("publisher"); + resultB.OtherDetails.Title.ShouldBe("title"); + resultB.OtherDetails.Author.ShouldBe("author"); + resultB.B.ShouldBe("b"); + + var result = list[2].ShouldBeOfType(); + result.Name.ShouldBe("name3"); + result.Details.Description.ShouldBe("description"); + result.Details.Publisher.ShouldBe("publisher"); + result.OtherDetails.Title.ShouldBe("title"); + result.OtherDetails.Author.ShouldBe("author"); + } + } +} +public class SubqueryMapFromWithIncludeMembersFirstOrDefaultWithIheritance : IntegrationTest +{ + public class Source + { + public int Id { get; set; } + public string Name { get; set; } + public List InnerSources { get; set; } = new List(); + public List OtherInnerSources { get; set; } = new List(); + } + public class SourceA : Source + { + public List InnerSourcesA { get; set; } = new List(); + } + public class SourceB : Source + { + public string B { get; set; } + } + public class InnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public List InnerSourceDetails { get; } = new List(); + } + public class InnerSourceA + { + public int Id { get; set; } + public string Name { get; set; } + public List InnerSourceDetailsA { get; } = new List(); + } + public class InnerSourceDetails + { + public int Id { get; set; } + public string Description { get; set; } + public string Publisher { get; set; } + } + public class InnerSourceDetailsA + { + public int Id { get; set; } + public string A { get; set; } + } + public class OtherInnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public List OtherInnerSourceDetails { get; } = new List(); + } + public class OtherInnerSourceDetails + { + public int Id { get; set; } + public string Title { get; set; } + public string Author { get; set; } + } + public class Destination + { + public int Id { get; set; } + public string Name { get; set; } + public DestinationDetails Details { get; set; } + public OtherDestinationDetails OtherDetails { get; set; } + } + public class DestinationA : Destination + { + public DestinationDetailsA DetailsA { get; set; } + } + public class DestinationB : Destination + { + public string B { get; set; } + } + public class DestinationDetails + { + public string Description { get; set; } + public string Publisher { get; set; } + } + public class DestinationDetailsA + { + public string A { get; set; } + } + public class OtherDestinationDetails + { + public string Title { get; set; } + public string Author { get; set; } + } + public class Context : LocalDbContext + { + public DbSet Sources { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity(); + modelBuilder.Entity(); + } + } + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(Context context) + { + var sourceA = new SourceA + { + Name = "name1", + InnerSourcesA = { new InnerSourceA { InnerSourceDetailsA = { new InnerSourceDetailsA { A = "a" } } } }, + InnerSources = { new InnerSource { InnerSourceDetails = { new InnerSourceDetails { Description = "description", Publisher = "publisher" } } } }, + OtherInnerSources = { new OtherInnerSource { OtherInnerSourceDetails = { new OtherInnerSourceDetails { Title = "title", Author = "author" } } } } + }; + context.Sources.Add(sourceA); + var sourceB = new SourceB + { + Name = "name2", + B = "b", + InnerSources = { new InnerSource { InnerSourceDetails = { new InnerSourceDetails { Description = "description", Publisher = "publisher" } } } }, + OtherInnerSources = { new OtherInnerSource { OtherInnerSourceDetails = { new OtherInnerSourceDetails { Title = "title", Author = "author" } } } } + }; + context.Sources.Add(sourceB); + var source = new Source + { + Name = "name3", + InnerSources = { new InnerSource { InnerSourceDetails = { new InnerSourceDetails { Description = "description", Publisher = "publisher" } } } }, + OtherInnerSources = { new OtherInnerSource { OtherInnerSourceDetails = { new OtherInnerSourceDetails { Title = "title", Author = "author" } } } } + }; + context.Sources.Add(source); + base.Seed(context); + } + } + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap() + .ForMember(d => d.Details, o => o.MapFrom(s => s.InnerSources.FirstOrDefault())) + .ForMember(d => d.OtherDetails, o => o.MapFrom(s => s.OtherInnerSources.FirstOrDefault())) + .Include() + .Include(); + cfg.CreateMap() + .ForMember(d => d.DetailsA, o => o.MapFrom(s => s.InnerSourcesA.FirstOrDefault())); + cfg.CreateMap(); + cfg.CreateMap().IncludeMembers(s => s.InnerSourceDetails.FirstOrDefault()); + cfg.CreateMap().IncludeMembers(s => s.OtherInnerSourceDetails.FirstOrDefault()); + cfg.CreateMap().IncludeMembers(s => s.InnerSourceDetailsA.FirstOrDefault()); + cfg.CreateMap(); + cfg.CreateMap(); + cfg.CreateMap(); + }); + [Fact] + public void Should_flatten() + { + using (var context = new Context()) + { + var projectTo = ProjectTo(context.Sources.OrderBy(p => p.Name)); + FirstOrDefaultCounter.Assert(projectTo, 33); + var list = projectTo.ToList(); + + var resultA = list[0].ShouldBeOfType(); + resultA.Name.ShouldBe("name1"); + resultA.Details.Description.ShouldBe("description"); + resultA.Details.Publisher.ShouldBe("publisher"); + resultA.OtherDetails.Title.ShouldBe("title"); + resultA.OtherDetails.Author.ShouldBe("author"); + resultA.DetailsA.A.ShouldBe("a"); + + var resultB = list[1].ShouldBeOfType(); + resultB.Name.ShouldBe("name2"); + resultB.Details.Description.ShouldBe("description"); + resultB.Details.Publisher.ShouldBe("publisher"); + resultB.OtherDetails.Title.ShouldBe("title"); + resultB.OtherDetails.Author.ShouldBe("author"); + resultB.B.ShouldBe("b"); + + var result = list[2].ShouldBeOfType(); + result.Name.ShouldBe("name3"); + result.Details.Description.ShouldBe("description"); + result.Details.Publisher.ShouldBe("publisher"); + result.OtherDetails.Title.ShouldBe("title"); + result.OtherDetails.Author.ShouldBe("author"); + } + } +} +public class SubqueryMapFromWithIncludeMembersSelectFirstOrDefaultWithIheritance : IntegrationTest +{ + public class Source + { + public int Id { get; set; } + public string Name { get; set; } + public List InnerSources { get; set; } = new List(); + public List OtherInnerSources { get; set; } = new List(); + } + public class SourceA : Source + { + public List InnerSourcesA { get; set; } = new List(); + } + public class SourceB : Source + { + public string B { get; set; } + } + public class InnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public List InnerSourceDetails { get; } = new List(); + } + public class InnerSourceA + { + public int Id { get; set; } + public string Name { get; set; } + public List InnerSourceDetailsA { get; } = new List(); + } + public class InnerSourceDetails + { + public int Id { get; set; } + public string Description { get; set; } + public string Publisher { get; set; } + } + public class InnerSourceDetailsA + { + public int Id { get; set; } + public string A { get; set; } + } + public class OtherInnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public List OtherInnerSourceDetails { get; } = new List(); + } + public class OtherInnerSourceDetails + { + public int Id { get; set; } + public string Title { get; set; } + public string Author { get; set; } + } + public class Destination + { + public int Id { get; set; } + public string Name { get; set; } + public DestinationDetails Details { get; set; } + public OtherDestinationDetails OtherDetails { get; set; } + } + public class DestinationA : Destination + { + public DestinationDetailsA DetailsA { get; set; } + } + public class DestinationB : Destination + { + public string B { get; set; } + } + public class DestinationDetails + { + public string Description { get; set; } + public string Publisher { get; set; } + } + public class DestinationDetailsA + { + public string A { get; set; } + } + public class OtherDestinationDetails + { + public string Title { get; set; } + public string Author { get; set; } + } + public class Context : LocalDbContext + { + public DbSet Sources { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity(); + modelBuilder.Entity(); + } + } + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(Context context) + { + var sourceA = new SourceA + { + Name = "name1", + InnerSourcesA = { new InnerSourceA { InnerSourceDetailsA = { new InnerSourceDetailsA { A = "a" } } } }, + InnerSources = { new InnerSource { InnerSourceDetails = { new InnerSourceDetails { Description = "description", Publisher = "publisher" } } } }, + OtherInnerSources = { new OtherInnerSource { OtherInnerSourceDetails = { new OtherInnerSourceDetails { Title = "title", Author = "author" } } } } + }; + context.Sources.Add(sourceA); + var sourceB = new SourceB + { + Name = "name2", + B = "b", + InnerSources = { new InnerSource { InnerSourceDetails = { new InnerSourceDetails { Description = "description", Publisher = "publisher" } } } }, + OtherInnerSources = { new OtherInnerSource { OtherInnerSourceDetails = { new OtherInnerSourceDetails { Title = "title", Author = "author" } } } } + }; + context.Sources.Add(sourceB); + var source = new Source + { + Name = "name3", + InnerSources = { new InnerSource { InnerSourceDetails = { new InnerSourceDetails { Description = "description", Publisher = "publisher" } } } }, + OtherInnerSources = { new OtherInnerSource { OtherInnerSourceDetails = { new OtherInnerSourceDetails { Title = "title", Author = "author" } } } } + }; + context.Sources.Add(source); + base.Seed(context); + } + } + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap() + .ForMember(d => d.Details, o => o.MapFrom(s => s.InnerSources.Select(s => s).FirstOrDefault())) + .ForMember(d => d.OtherDetails, o => o.MapFrom(s => s.OtherInnerSources.Select(s => s).FirstOrDefault())) + .Include() + .Include(); + cfg.CreateMap() + .ForMember(d => d.DetailsA, o => o.MapFrom(s => s.InnerSourcesA.Select(s => s).FirstOrDefault())); + cfg.CreateMap(); + cfg.CreateMap().IncludeMembers(s => s.InnerSourceDetails.Select(s => s).FirstOrDefault()); + cfg.CreateMap().IncludeMembers(s => s.OtherInnerSourceDetails.Select(s => s).FirstOrDefault()); + cfg.CreateMap().IncludeMembers(s => s.InnerSourceDetailsA.Select(s => s).FirstOrDefault()); + cfg.CreateMap(); + cfg.CreateMap(); + cfg.CreateMap(); + }); + [Fact] + public void Should_flatten() + { + using (var context = new Context()) + { + var projectTo = ProjectTo(context.Sources.OrderBy(p => p.Name)); + FirstOrDefaultCounter.Assert(projectTo, 33); + var list = projectTo.ToList(); + + var resultA = list[0].ShouldBeOfType(); + resultA.Name.ShouldBe("name1"); + resultA.Details.Description.ShouldBe("description"); + resultA.Details.Publisher.ShouldBe("publisher"); + resultA.OtherDetails.Title.ShouldBe("title"); + resultA.OtherDetails.Author.ShouldBe("author"); + resultA.DetailsA.A.ShouldBe("a"); + + var resultB = list[1].ShouldBeOfType(); + resultB.Name.ShouldBe("name2"); + resultB.Details.Description.ShouldBe("description"); + resultB.Details.Publisher.ShouldBe("publisher"); + resultB.OtherDetails.Title.ShouldBe("title"); + resultB.OtherDetails.Author.ShouldBe("author"); + resultB.B.ShouldBe("b"); + + var result = list[2].ShouldBeOfType(); + result.Name.ShouldBe("name3"); + result.Details.Description.ShouldBe("description"); + result.Details.Publisher.ShouldBe("publisher"); + result.OtherDetails.Title.ShouldBe("title"); + result.OtherDetails.Author.ShouldBe("author"); + } + } +} +public class SubqueryMapFromWithIncludeMembersSelectMemberFirstOrDefaultWithIheritance : IntegrationTest +{ + public class Source + { + public int Id { get; set; } + public string Name { get; set; } + public List InnerSourceWrappers { get; set; } = new List(); + public List OtherInnerSources { get; set; } = new List(); + } + public class SourceA : Source + { + public List InnerSourceWrappersA { get; set; } = new List(); + } + public class SourceB : Source + { + public string B { get; set; } + } + public class InnerSourceWrapper + { + public int Id { get; set; } + public InnerSource InnerSource { get; set; } + } + public class InnerSourceWrapperA + { + public int Id { get; set; } + public InnerSourceA InnerSourceA { get; set; } + } + public class InnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public List InnerSourceDetailsWrapper { get; } = new List(); + } + public class InnerSourceA + { + public int Id { get; set; } + public string Name { get; set; } + public List InnerSourceDetailsWrapperA { get; } = new List(); + } + public class InnerSourceDetailsWrapper + { + public int Id { get; set; } + public InnerSourceDetails InnerSourceDetails { get; set; } + } + public class InnerSourceDetailsWrapperA + { + public int Id { get; set; } + public InnerSourceDetailsA InnerSourceDetailsA { get; set; } + } + public class InnerSourceDetails + { + public int Id { get; set; } + public string Description { get; set; } + public string Publisher { get; set; } + } + public class InnerSourceDetailsA + { + public int Id { get; set; } + public string A { get; set; } + } + public class OtherInnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public List OtherInnerSourceDetails { get; } = new List(); + } + public class OtherInnerSourceDetails + { + public int Id { get; set; } + public string Title { get; set; } + public string Author { get; set; } + } + public class Destination + { + public int Id { get; set; } + public string Name { get; set; } + public DestinationDetails Details { get; set; } + public OtherDestinationDetails OtherDetails { get; set; } + } + public class DestinationA : Destination + { + public DestinationDetailsA DetailsA { get; set; } + } + public class DestinationB : Destination + { + public string B { get; set; } + } + public class DestinationDetails + { + public string Description { get; set; } + public string Publisher { get; set; } + } + public class DestinationDetailsA + { + public string A { get; set; } + } + public class OtherDestinationDetails + { + public string Title { get; set; } + public string Author { get; set; } + } + public class Context : LocalDbContext + { + public DbSet Sources { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity(); + modelBuilder.Entity(); + } + } + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(Context context) + { + var sourceA = new SourceA + { + Name = "name1", + InnerSourceWrappersA = { new InnerSourceWrapperA { InnerSourceA = new InnerSourceA { InnerSourceDetailsWrapperA = { new InnerSourceDetailsWrapperA { InnerSourceDetailsA = new InnerSourceDetailsA { A = "a" } } } } } }, + InnerSourceWrappers = { new InnerSourceWrapper { InnerSource = new InnerSource { InnerSourceDetailsWrapper = { new InnerSourceDetailsWrapper { InnerSourceDetails = new InnerSourceDetails { Description = "description", Publisher = "publisher" } } } } } }, + OtherInnerSources = { new OtherInnerSource { OtherInnerSourceDetails = { new OtherInnerSourceDetails { Title = "title", Author = "author" } } } } + }; + context.Sources.Add(sourceA); + var sourceB = new SourceB + { + Name = "name2", + B = "b", + InnerSourceWrappers = { new InnerSourceWrapper { InnerSource = new InnerSource { InnerSourceDetailsWrapper = { new InnerSourceDetailsWrapper { InnerSourceDetails = new InnerSourceDetails { Description = "description", Publisher = "publisher" } } } } } }, + OtherInnerSources = { new OtherInnerSource { OtherInnerSourceDetails = { new OtherInnerSourceDetails { Title = "title", Author = "author" } } } } + }; + context.Sources.Add(sourceB); + var source = new Source + { + Name = "name3", + InnerSourceWrappers = { new InnerSourceWrapper { InnerSource = new InnerSource { InnerSourceDetailsWrapper = { new InnerSourceDetailsWrapper { InnerSourceDetails = new InnerSourceDetails { Description = "description", Publisher = "publisher" } } } } } }, + OtherInnerSources = { new OtherInnerSource { OtherInnerSourceDetails = { new OtherInnerSourceDetails { Title = "title", Author = "author" } } } } + }; + context.Sources.Add(source); + base.Seed(context); + } + } + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap() + .ForMember(d => d.Details, o => o.MapFrom(s => s.InnerSourceWrappers.Select(s => s.InnerSource).FirstOrDefault())) + .ForMember(d => d.OtherDetails, o => o.MapFrom(s => s.OtherInnerSources.Select(s => s).FirstOrDefault())) + .Include() + .Include(); + cfg.CreateMap() + .ForMember(d => d.DetailsA, o => o.MapFrom(s => s.InnerSourceWrappersA.Select(s => s.InnerSourceA).FirstOrDefault())); + cfg.CreateMap(); + cfg.CreateMap().IncludeMembers(s => s.InnerSourceDetailsWrapper.Select(s => s.InnerSourceDetails).FirstOrDefault()); + cfg.CreateMap().IncludeMembers(s => s.OtherInnerSourceDetails.Select(s => s).FirstOrDefault()); + cfg.CreateMap().IncludeMembers(s => s.InnerSourceDetailsWrapperA.Select(s => s.InnerSourceDetailsA).FirstOrDefault()); + cfg.CreateMap(); + cfg.CreateMap(); + cfg.CreateMap(); + }); + [Fact] + public void Should_flatten() + { + using (var context = new Context()) + { + var projectTo = ProjectTo(context.Sources.OrderBy(p => p.Name)); + FirstOrDefaultCounter.Assert(projectTo, 33); + var list = projectTo.ToList(); + + var resultA = list[0].ShouldBeOfType(); + resultA.Name.ShouldBe("name1"); + resultA.Details.Description.ShouldBe("description"); + resultA.Details.Publisher.ShouldBe("publisher"); + resultA.OtherDetails.Title.ShouldBe("title"); + resultA.OtherDetails.Author.ShouldBe("author"); + resultA.DetailsA.A.ShouldBe("a"); + + var resultB = list[1].ShouldBeOfType(); + resultB.Name.ShouldBe("name2"); + resultB.Details.Description.ShouldBe("description"); + resultB.Details.Publisher.ShouldBe("publisher"); + resultB.OtherDetails.Title.ShouldBe("title"); + resultB.OtherDetails.Author.ShouldBe("author"); + resultB.B.ShouldBe("b"); + + var result = list[2].ShouldBeOfType(); + result.Name.ShouldBe("name3"); + result.Details.Description.ShouldBe("description"); + result.Details.Publisher.ShouldBe("publisher"); + result.OtherDetails.Title.ShouldBe("title"); + result.OtherDetails.Author.ShouldBe("author"); + } + } +} +public class IncludeMembersWithMapFromExpressionWithIheritance : IntegrationTest +{ + public class Source + { + public int Id { get; set; } + public string Name { get; set; } + public InnerSource InnerSource { get; set; } + public OtherInnerSource OtherInnerSource { get; set; } + } + public class SourceA : Source + { + public InnerSourceA InnerSourceA { get; set; } + } + public class SourceB : Source + { + public string B { get; set; } + } + public class InnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public string Description1 { get; set; } + } + public class InnerSourceA + { + public int Id { get; set; } + public string A { get; set; } + } + public class OtherInnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public string Title1 { get; set; } + } + public class Destination + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public string Title { get; set; } + } + public class DestinationA : Destination + { + public string A { get; set; } + } + public class DestinationB : Destination + { + public string B { get; set; } + } + public class Context : LocalDbContext + { + public DbSet Sources { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity(); + modelBuilder.Entity(); + } + } + + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(Context context) + { + var sourceA = new SourceA { Name = "name1", InnerSourceA = new InnerSourceA { A = "a" }, InnerSource = new InnerSource { Description1 = "description" }, OtherInnerSource = new OtherInnerSource { Title1 = "title" } }; + context.Sources.Add(sourceA); + var sourceB = new SourceB { Name = "name2", B = "b", InnerSource = new InnerSource { Description1 = "description" }, OtherInnerSource = new OtherInnerSource { Title1 = "title" } }; + context.Sources.Add(sourceB); + var source = new Source { Name = "name3", InnerSource = new InnerSource { Description1 = "description" }, OtherInnerSource = new OtherInnerSource { Title1 = "title" } }; + context.Sources.Add(source); + base.Seed(context); + } + } + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap().IncludeMembers(s => s.InnerSource, s => s.OtherInnerSource) + .Include() + .Include(); + cfg.CreateMap().IncludeMembers(s => s.InnerSourceA); + cfg.CreateMap(); + cfg.CreateMap(MemberList.None).ForMember(d => d.Description, o => o.MapFrom(s => s.Description1)); + cfg.CreateMap(MemberList.None).ForMember(d => d.Title, o => o.MapFrom(s => s.Title1)); + cfg.CreateMap(MemberList.None).ForMember(d => d.A, o => o.MapFrom(s => s.A)); + }); + [Fact] + public void Should_flatten_with_MapFrom() + { + using (var context = new Context()) + { + var list = ProjectTo(context.Sources.OrderBy(p => p.Name)).ToList(); + + var resultA = list[0].ShouldBeOfType(); + resultA.Name.ShouldBe("name1"); + resultA.Description.ShouldBe("description"); + resultA.Title.ShouldBe("title"); + resultA.A.ShouldBe("a"); + + var resultB = list[1].ShouldBeOfType(); + resultB.Name.ShouldBe("name2"); + resultB.Description.ShouldBe("description"); + resultB.Title.ShouldBe("title"); + resultB.B.ShouldBe("b"); + + var result = list[2].ShouldBeOfType(); + result.Name.ShouldBe("name3"); + result.Description.ShouldBe("description"); + result.Title.ShouldBe("title"); + } + } +} +public class IncludeMembersWithNullSubstituteWithIheritance : IntegrationTest +{ + public class Source + { + public int Id { get; set; } + public string Name { get; set; } + public InnerSource InnerSource { get; set; } + public OtherInnerSource OtherInnerSource { get; set; } + } + public class SourceA : Source + { + public InnerSourceA InnerSourceA { get; set; } + } + public class SourceB : Source + { + public string B { get; set; } + } + public class InnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public int? Code { get; set; } + } + public class InnerSourceA + { + public int Id { get; set; } + public string A { get; set; } + } + public class OtherInnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public int? Code { get; set; } + public int? OtherCode { get; set; } + } + public class Destination + { + public int Id { get; set; } + public string Name { get; set; } + public int Code { get; set; } + public int OtherCode { get; set; } + } + public class DestinationA : Destination + { + public string A { get; set; } + } + public class DestinationB : Destination + { + public string B { get; set; } + } + public class Context : LocalDbContext + { + public DbSet Sources { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity(); + modelBuilder.Entity(); + } + } + + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(Context context) + { + var sourceA = new SourceA { Name = "name1" }; + context.Sources.Add(sourceA); + var sourceB = new SourceB { Name = "name2", B = "b", }; + context.Sources.Add(sourceB); + var source = new Source { Name = "name3" }; + context.Sources.Add(source); + base.Seed(context); + } + } + + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap().IncludeMembers(s => s.InnerSource, s => s.OtherInnerSource) + .Include() + .Include(); + cfg.CreateMap().IncludeMembers(s => s.InnerSourceA); + cfg.CreateMap(); + cfg.CreateProjection(MemberList.None).ForMember(d => d.Code, o => o.NullSubstitute(5)); + cfg.CreateProjection(MemberList.None).ForMember(d => d.OtherCode, o => o.NullSubstitute(7)); + cfg.CreateProjection(MemberList.None).ForMember(d => d.A, o => o.NullSubstitute("a")); + }); + [Fact] + public void Should_flatten() + { + using (var context = new Context()) + { + var list = ProjectTo(context.Sources.OrderBy(p => p.Name)).ToList(); + + var resultA = list[0].ShouldBeOfType(); + resultA.Name.ShouldBe("name1"); + resultA.Code.ShouldBe(5); + resultA.OtherCode.ShouldBe(7); + resultA.A.ShouldBe("a"); + + var resultB = list[1].ShouldBeOfType(); + resultB.Name.ShouldBe("name2"); + resultB.Code.ShouldBe(5); + resultB.OtherCode.ShouldBe(7); + resultB.B.ShouldBe("b"); + + var result = list[2].ShouldBeOfType(); + result.Name.ShouldBe("name3"); + result.Code.ShouldBe(5); + result.OtherCode.ShouldBe(7); + } + } +} +public class IncludeMembersMembersFirstOrDefaultWithNullSubstituteWithIheritance : IntegrationTest +{ + public class Source + { + public int Id { get; set; } + public string Name { get; set; } + public List InnerSources { get; set; } = new List(); + public List OtherInnerSources { get; set; } = new List(); + } + public class SourceA : Source + { + public List InnerSourcesA { get; set; } = new List(); + } + public class SourceB : Source + { + public string B { get; set; } + } + public class InnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public int? Code { get; set; } + } + public class InnerSourceA + { + public int Id { get; set; } + public string A { get; set; } + } + public class OtherInnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public int? Code { get; set; } + public int? OtherCode { get; set; } + } + public class Destination + { + public int Id { get; set; } + public string Name { get; set; } + public int Code { get; set; } + public int OtherCode { get; set; } + } + public class DestinationA : Destination + { + public string A { get; set; } + } + public class DestinationB : Destination + { + public string B { get; set; } + } + public class Context : LocalDbContext + { + public DbSet Sources { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity(); + modelBuilder.Entity(); + } + } + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(Context context) + { + var sourceA = new SourceA { Name = "name1" }; + context.Sources.Add(sourceA); + var sourceB = new SourceB { Name = "name2", B = "b", }; + context.Sources.Add(sourceB); + var source = new Source { Name = "name3" }; + context.Sources.Add(source); + base.Seed(context); + } + } + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap().IncludeMembers(s => s.InnerSources.FirstOrDefault(), s => s.OtherInnerSources.FirstOrDefault()) + .Include() + .Include(); + cfg.CreateMap().IncludeMembers(s => s.InnerSourcesA.FirstOrDefault()); + cfg.CreateMap(); + cfg.CreateProjection(MemberList.None).ForMember(d => d.Code, o => o.NullSubstitute(5)); + cfg.CreateProjection(MemberList.None).ForMember(d => d.OtherCode, o => o.NullSubstitute(7)); + cfg.CreateProjection(MemberList.None).ForMember(d => d.A, o => o.NullSubstitute("a")); + }); + [Fact] + public void Should_flatten() + { + using (var context = new Context()) + { + var projectTo = ProjectTo(context.Sources.OrderBy(p => p.Name)); + FirstOrDefaultCounter.Assert(projectTo, 7); + var list = projectTo.ToList(); + + var resultA = list[0].ShouldBeOfType(); + resultA.Name.ShouldBe("name1"); + resultA.Code.ShouldBe(5); + resultA.OtherCode.ShouldBe(7); + resultA.A.ShouldBe("a"); + + var resultB = list[1].ShouldBeOfType(); + resultB.Name.ShouldBe("name2"); + resultB.Code.ShouldBe(5); + resultB.OtherCode.ShouldBe(7); + resultB.B.ShouldBe("b"); + + var result = list[2].ShouldBeOfType(); + result.Name.ShouldBe("name3"); + result.Code.ShouldBe(5); + result.OtherCode.ShouldBe(7); + } + } +} +public class CascadedIncludeMembersWithIheritance : IntegrationTest +{ + public class Source + { + public int Id { get; set; } + public string Name { get; set; } + public Level1 FieldLevel1 { get; set; } + } + public class SourceA : Source + { + public Level1A FieldLevel1A { get; set; } + } + public class SourceB : Source + { + public string B { get; set; } + } + public class Level1 + { + public int Id { get; set; } + public Level2 FieldLevel2 { get; set; } + } + public class Level2 + { + public int Id { get; set; } + public long TheField { get; set; } + } + + public class Level1A + { + public int Id { get; set; } + public Level2A FieldLevel2A { get; set; } + } + public class Level2A + { + public int Id { get; set; } + public string A { get; set; } + } + public class Destination + { + public int Id { get; set; } + public string Name { get; set; } + public long TheField { get; set; } + } + public class DestinationA : Destination + { + public string A { get; set; } + } + public class DestinationB : Destination + { + public string B { get; set; } + } + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap().IncludeMembers(s => s.FieldLevel1) + .Include() + .Include(); + cfg.CreateMap().IncludeMembers(s => s.FieldLevel1A); + cfg.CreateMap(); + cfg.CreateMap(MemberList.None).IncludeMembers(s => s.FieldLevel2); + cfg.CreateMap(MemberList.None); + cfg.CreateMap(MemberList.None).IncludeMembers(s => s.FieldLevel2A); + cfg.CreateMap(MemberList.None); + }); + public class Context : LocalDbContext + { + public DbSet Sources { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity(); + modelBuilder.Entity(); + } + } + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(Context context) + { + var sourceA = new SourceA { Name = "name1", FieldLevel1A = new Level1A { FieldLevel2A = new Level2A { A = "a" } }, FieldLevel1 = new Level1 { FieldLevel2 = new Level2 { TheField = 2 } } }; + context.Sources.Add(sourceA); + var sourceB = new SourceB { Name = "name2", B = "b", FieldLevel1 = new Level1 { FieldLevel2 = new Level2 { TheField = 2 } } }; + context.Sources.Add(sourceB); + var source = new Source { Name = "name3", FieldLevel1 = new Level1 { FieldLevel2 = new Level2 { TheField = 2 } } }; + context.Sources.Add(source); + base.Seed(context); + } + } + [Fact] + public void Should_flatten() + { + using (var context = new Context()) + { + var projectTo = ProjectTo(context.Sources.OrderBy(p => p.Name)); + var list = projectTo.ToList(); + + var resultA = list[0].ShouldBeOfType(); + resultA.Name.ShouldBe("name1"); + resultA.TheField.ShouldBe(2); + resultA.A.ShouldBe("a"); + + var resultB = list[1].ShouldBeOfType(); + resultB.Name.ShouldBe("name2"); + resultB.TheField.ShouldBe(2); + resultB.B.ShouldBe("b"); + + var result = list[2].ShouldBeOfType(); + result.Name.ShouldBe("name3"); + result.TheField.ShouldBe(2); + } + } +} +public class IncludeOnlySelectedMembersWithIheritance : IntegrationTest +{ + public class Source + { + public int Id { get; set; } + public string Name { get; set; } + public InnerSource InnerSource { get; set; } + public OtherInnerSource OtherInnerSource { get; set; } + public int ImNotIncluded { get; set; } + } + public class SourceA : Source + { + public string A { get; set; } + public int ImNotIncludedA { get; set; } + public List InnerSourcesA { get; set; } = new List(); + } + + public class InnerSourceA + { + public int Id { get; set; } + public int ImNotIncludedA { get; set; } + public string IAmIncluded { get; set; } + } + public class SourceB : Source + { + public string B { get; set; } + public int ImNotIncludedB { get; set; } + } + public class InnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + } + public class OtherInnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public string Title { get; set; } + } + public class Destination + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + } + public class DestinationA : Destination + { + public string A { get; set; } + public string IAmIncluded { get; set; } + } + public class DestinationB : Destination + { + public string B { get; set; } + } + public class Context : LocalDbContext + { + public DbSet Sources { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity(); + modelBuilder.Entity(); + } + } + + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(Context context) + { + var sourceA = new SourceA { Name = "name1", A = "a", InnerSource = new InnerSource { Description = "description" }, OtherInnerSource = new OtherInnerSource { Title = "title" }, InnerSourcesA = { new InnerSourceA { IAmIncluded = "a" } } }; + context.Sources.Add(sourceA); + var sourceB = new SourceB { Name = "name2", B = "b", InnerSource = new InnerSource { Description = "description" }, OtherInnerSource = new OtherInnerSource { Title = "title" } }; + context.Sources.Add(sourceB); + var sourceC = new Source { Name = "name3", InnerSource = new InnerSource { Description = "description" }, OtherInnerSource = new OtherInnerSource { Title = "title" } }; + context.Sources.Add(sourceC); + base.Seed(context); + } + } + + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap().IncludeMembers(s => s.InnerSource, s => s.OtherInnerSource) + .Include() + .Include(); + cfg.CreateMap().IncludeMembers(s => s.InnerSourcesA.FirstOrDefault()); + cfg.CreateMap(MemberList.None); + cfg.CreateMap(); + cfg.CreateProjection(MemberList.None); + cfg.CreateProjection(MemberList.None); + }); + [Fact] + public void Should_flatten() + { + using (var context = new Context()) + { + var projectTo = ProjectTo(context.Sources.OrderBy(p => p.Name)); + var query = projectTo.ToQueryString(); + query.ShouldNotContain(nameof(Source.ImNotIncluded)); + + var list = projectTo.ToList(); + var resultA = list[0].ShouldBeOfType(); + resultA.Name.ShouldBe("name1"); + resultA.Description.ShouldBe("description"); + resultA.A.ShouldBe("a"); + resultA.IAmIncluded.ShouldBe("a"); + + var resultB = list[1].ShouldBeOfType(); + resultB.Name.ShouldBe("name2"); + resultB.Description.ShouldBe("description"); + resultB.B.ShouldBe("b"); + + var resultC = list[2].ShouldBeOfType(); + resultC.Name.ShouldBe("name3"); + resultC.Description.ShouldBe("description"); + } + } +} + +public class IncludeMultipleExpressionsWithIheritance : IntegrationTest +{ + public class Source + { + public int Id { get; set; } + public string Name { get; set; } + public InnerSource InnerSource { get; set; } + public OtherInnerSource OtherInnerSource { get; set; } + public int ImNotIncluded { get; set; } + public List InnerSourcesA { get; set; } = new List(); + } + public class SourceA : Source + { + public List InnerSourcesAFallback { get; set; } = new List(); + } + + public class InnerSourceA + { + public int Id { get; set; } + public int ImNotIncludedA { get; set; } + public string IAmIncluded { get; set; } + } + public class SourceB : Source + { + public string B { get; set; } + public int ImNotIncludedB { get; set; } + } + public class InnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + } + public class OtherInnerSource + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public string Title { get; set; } + } + public class Destination + { + public int Id { get; set; } + public string Name { get; set; } + public string Description { get; set; } + } + public class DestinationA : Destination + { + public string IAmIncluded { get; set; } + } + public class DestinationB : Destination + { + public string B { get; set; } + } + public class Context : LocalDbContext + { + public DbSet Sources { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity(); + modelBuilder.Entity(); + } + } + + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(Context context) + { + var sourceA = new SourceA { Name = "name1", InnerSource = new InnerSource { Description = "description" }, OtherInnerSource = new OtherInnerSource { Title = "title" }, InnerSourcesAFallback = { new InnerSourceA { IAmIncluded = "fallback" } } }; + context.Sources.Add(sourceA); + var sourceB = new SourceB { Name = "name2", B = "b", InnerSource = new InnerSource { Description = "description" }, OtherInnerSource = new OtherInnerSource { Title = "title" } }; + context.Sources.Add(sourceB); + var sourceC = new Source { Name = "name3", InnerSource = new InnerSource { Description = "description" }, OtherInnerSource = new OtherInnerSource { Title = "title" } }; + context.Sources.Add(sourceC); + base.Seed(context); + } + } + + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap().IncludeMembers(s => s.InnerSource, s => s.OtherInnerSource) + .Include() + .Include(); + cfg.CreateMap().IncludeMembers(s => s.InnerSourcesA.FirstOrDefault() ?? s.InnerSourcesAFallback.FirstOrDefault()); + cfg.CreateMap(MemberList.None); + cfg.CreateMap(); + cfg.CreateProjection(MemberList.None); + cfg.CreateProjection(MemberList.None); + }); + [Fact] + public void Should_flatten() + { + using (var context = new Context()) + { + var projectTo = ProjectTo(context.Sources.OrderBy(p => p.Name)); + var list = projectTo.ToList(); + var resultA = list[0].ShouldBeOfType(); + resultA.Name.ShouldBe("name1"); + resultA.Description.ShouldBe("description"); + resultA.IAmIncluded.ShouldBe("fallback"); + + var resultB = list[1].ShouldBeOfType(); + resultB.Name.ShouldBe("name2"); + resultB.Description.ShouldBe("description"); + resultB.B.ShouldBe("b"); + + var resultC = list[2].ShouldBeOfType(); + resultC.Name.ShouldBe("name3"); + resultC.Description.ShouldBe("description"); + } + } } \ No newline at end of file diff --git a/src/IntegrationTests/Inheritance/ProjectToAbstractTypeWithInheritance.cs b/src/IntegrationTests/Inheritance/ProjectToAbstractTypeWithInheritance.cs new file mode 100644 index 0000000000..ef80014b7e --- /dev/null +++ b/src/IntegrationTests/Inheritance/ProjectToAbstractTypeWithInheritance.cs @@ -0,0 +1,143 @@ +namespace AutoMapper.IntegrationTests.Inheritance; + +public class ProjectToAbstractTypeWithInheritance : IntegrationTest +{ + public class StepGroup + { + public int Id { get; set; } + public string Name { get; set; } + public virtual List Steps { get; set; } = new(); + } + public abstract class Step + { + public int Id { get; set; } + public string Name { get; set; } + public int StepGroupId { get; set; } + public virtual StepGroup StepGroup { get; set; } + public virtual ICollection StepInputs { get; set; } = new HashSet(); + } + public class CheckingStep : Step { } + public class InstructionStep : Step { } + public abstract class AbstractStep : Step { } + public class StepInput + { + public int Id { get; set; } + public int StepId { get; set; } + public string Input { get; set; } + public virtual Step Step { get; set; } + } + public class StepGroupModel + { + public int Id { get; set; } + public string Name { get; set; } + public List Steps { get; set; } = new(); + } + public abstract class StepModel + { + public int Id { get; set; } + public string Name { get; set; } + public ICollection StepInputs { get; set; } = new HashSet(); + } + public class CheckingStepModel : StepModel { } + public class InstructionStepModel : StepModel { } + public abstract class AbstractStepModel : StepModel { } + public class StepInputModel + { + public int Id { get; set; } + public int StepId { get; set; } + public string Input { get; set; } + public StepModel Step { get; set; } + } + + public class Context : LocalDbContext + { + public DbSet StepGroups { get; set; } + + public DbSet Steps { get; set; } + + public DbSet StepInputs { get; set; } + + public DbSet CheckingSteps { get; set; } + + public DbSet InstructionSteps { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(entity => + { + entity.HasOne(d => d.StepGroup).WithMany(p => p.Steps) + .HasForeignKey(d => d.StepGroupId); + }); + + modelBuilder.Entity(entity => + { + entity.HasOne(d => d.Step).WithMany(p => p.StepInputs) + .HasForeignKey(d => d.StepId); + }); + } + } + + protected override MapperConfiguration CreateConfiguration() + { + return new MapperConfiguration(cfg => + { + cfg.CreateMap(); + cfg.CreateMap(); + cfg.CreateMap() + .IncludeBase(); + cfg.CreateMap() + .IncludeBase(); + cfg.CreateMap() + .IncludeBase(); + cfg.CreateMap(); + }); + } + + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(Context context) + { + context.StepGroups.Add(new StepGroup + { + Name = "StepGroup", + Steps = new List + { + new InstructionStep + { + Name = "InstructionStep", + StepInputs = new List + { + new StepInput + { + Input = "Input" + } + } + }, + new CheckingStep + { + Name = "CheckingStep" + } + } + }); + + base.Seed(context); + } + } + + [Fact] + public void ProjectCollectionWithElementInheritingAbstractClass() + { + using var context = new Context(); + var steps = ProjectTo(context.StepGroups).Single().Steps; + steps[0].ShouldBeOfType().Name.ShouldBe("CheckingStep"); + steps[1].ShouldBeOfType().Name.ShouldBe("InstructionStep"); + } + + [Fact] + public void ProjectIncludingPolymorphicElement() + { + using var context = new Context(); + var stepInput = ProjectTo(context.StepInputs).Single(); + stepInput.Step.ShouldBeOfType().Name.ShouldBe("InstructionStep"); + } +} \ No newline at end of file diff --git a/src/IntegrationTests/IntegrationTest.cs b/src/IntegrationTests/IntegrationTest.cs index 2739c3c515..47f412748f 100644 --- a/src/IntegrationTests/IntegrationTest.cs +++ b/src/IntegrationTests/IntegrationTest.cs @@ -15,12 +15,19 @@ protected virtual void Seed(TContext context){} public async Task Migrate() { await using var context = new TContext(); - - await context.Database.EnsureDeletedAsync(); - await context.Database.EnsureCreatedAsync(); + var database = context.Database; + await database.EnsureDeletedAsync(); + var strategy = database.CreateExecutionStrategy(); + await strategy.ExecuteAsync(async () => await database.EnsureCreatedAsync()); Seed(context); await context.SaveChangesAsync(); } +} +public abstract class LocalDbContext : DbContext +{ + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder.UseSqlServer( + @$"Data Source=(localdb)\mssqllocaldb;Integrated Security=True;MultipleActiveResultSets=True;Database={GetType()};Connection Timeout=300", + o => o.EnableRetryOnFailure(maxRetryCount: 10).CommandTimeout(120)); } \ No newline at end of file diff --git a/src/IntegrationTests/LocalDbContext.cs b/src/IntegrationTests/LocalDbContext.cs deleted file mode 100644 index 66fb48d50b..0000000000 --- a/src/IntegrationTests/LocalDbContext.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace AutoMapper.IntegrationTests; - -public abstract class LocalDbContext : DbContext -{ - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder.UseSqlServer( - @$"Data Source=(localdb)\mssqllocaldb;Integrated Security=True;MultipleActiveResultSets=True;Database={GetType()};Connection Timeout=300", - o=>o.EnableRetryOnFailure()); -} \ No newline at end of file diff --git a/src/TestApp/Program.cs b/src/TestApp/Program.cs new file mode 100644 index 0000000000..3d20096573 --- /dev/null +++ b/src/TestApp/Program.cs @@ -0,0 +1,93 @@ +using AutoMapper; +using Microsoft.Extensions.DependencyInjection; + +IServiceCollection services = new ServiceCollection(); +services.AddTransient(sp => new FooService(5)); +services.AddAutoMapper(typeof(Source)); +var provider = services.BuildServiceProvider(); +using (var scope = provider.CreateScope()) +{ + var mapper = scope.ServiceProvider.GetRequiredService(); + + foreach (var typeMap in mapper.ConfigurationProvider.Internal().GetAllTypeMaps()) + { + Console.WriteLine($"{typeMap.SourceType.Name} -> {typeMap.DestinationType.Name}"); + } + + foreach (var service in services) + { + Console.WriteLine(service.ServiceType + " - " + service.ImplementationType); + } + + var dest = mapper.Map(new Source2()); + Console.WriteLine(dest!.ResolvedValue); +} + +Console.ReadKey(); + +public class Source +{ +} + +public class Dest +{ +} + +public class Source2 +{ +} + +public class Dest2 +{ + public int ResolvedValue { get; set; } +} + +public class Profile1 : Profile +{ + public Profile1() + { + CreateMap(); + } +} + +public class Profile2 : Profile +{ + public Profile2() + { + CreateMap() + .ForMember(d => d.ResolvedValue, opt => opt.MapFrom()); + } +} + +public class DependencyResolver : IValueResolver +{ + private readonly ISomeService _service; + + public DependencyResolver(ISomeService service) + { + _service = service; + } + + public int Resolve(object source, object destination, int destMember, ResolutionContext context) + { + return _service.Modify(destMember); + } +} + +public interface ISomeService +{ + int Modify(int value); +} + +public class FooService : ISomeService +{ + private readonly int _value; + + public FooService(int value) + { + _value = value; + } + + public int Modify(int value) => value + _value; +} + diff --git a/src/TestApp/Properties/AssemblyInfo.cs b/src/TestApp/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..fdffe52e63 --- /dev/null +++ b/src/TestApp/Properties/AssemblyInfo.cs @@ -0,0 +1,19 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("TestApp")] +[assembly: AssemblyTrademark("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("de95f633-80b5-4248-a594-7fb357c8dac9")] diff --git a/src/TestApp/TestApp.csproj b/src/TestApp/TestApp.csproj new file mode 100644 index 0000000000..1aa0ba2168 --- /dev/null +++ b/src/TestApp/TestApp.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + TestApp + Exe + enable + enable + TestApp + false + false + false + + + + + + + + \ No newline at end of file diff --git a/src/UnitTests/AutoMapper.UnitTests.csproj b/src/UnitTests/AutoMapper.UnitTests.csproj index b7b9514f1e..c0a536086c 100644 --- a/src/UnitTests/AutoMapper.UnitTests.csproj +++ b/src/UnitTests/AutoMapper.UnitTests.csproj @@ -1,16 +1,16 @@  - net7.0 + net8.0 $(NoWarn);649;618 - - - - + + + + diff --git a/src/UnitTests/BidirectionalRelationships.cs b/src/UnitTests/BidirectionalRelationships.cs index 90bc34ad22..72a16bbd7e 100644 --- a/src/UnitTests/BidirectionalRelationships.cs +++ b/src/UnitTests/BidirectionalRelationships.cs @@ -432,7 +432,7 @@ public void Should_map_successfully() object.ReferenceEquals(_dtoParent.Children[0].Parents[0], _dtoParent).ShouldBeTrue(); } - public class Parent + public class Parent : IEquatable { public Guid Id { get; private set; } @@ -445,26 +445,9 @@ public Parent() Id = Guid.NewGuid(); Children = new List(); } - - public bool Equals(Parent other) - { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; - return other.Id.Equals(Id); - } - - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != typeof (Parent)) return false; - return Equals((Parent) obj); - } - - public override int GetHashCode() - { - return Id.GetHashCode(); - } + public bool Equals(Parent other) => throw new NotImplementedException(); + public override bool Equals(object obj) => throw new NotImplementedException(); + public override int GetHashCode() => throw new NotImplementedException(); } public class Child diff --git a/src/UnitTests/Bug/ConvertMapperThreading.cs b/src/UnitTests/Bug/ConvertMapperThreading.cs index 4eb344703e..2bf7033a35 100644 --- a/src/UnitTests/Bug/ConvertMapperThreading.cs +++ b/src/UnitTests/Bug/ConvertMapperThreading.cs @@ -13,7 +13,7 @@ class Destination } [Fact] - public void Should_work() + public async Task Should_work() { var tasks = Enumerable.Range(0, 5).Select(i => Task.Factory.StartNew(() => { @@ -21,7 +21,7 @@ public void Should_work() })).ToArray(); try { - Task.WaitAll(tasks); + await Task.WhenAll(tasks); } catch(AggregateException ex) { diff --git a/src/UnitTests/Bug/MultiThreadingIssues.cs b/src/UnitTests/Bug/MultiThreadingIssues.cs index 98dd98bae0..8bbf0e3145 100644 --- a/src/UnitTests/Bug/MultiThreadingIssues.cs +++ b/src/UnitTests/Bug/MultiThreadingIssues.cs @@ -613,7 +613,7 @@ public class Dto } [Fact] - public void Should_work() + public async Task Should_work() { var sourceType = typeof(Entity<>); var destinationType = typeof(Dto<>); @@ -646,7 +646,7 @@ public void Should_work() .ToArray() .Select(s => Task.Factory.StartNew(() => c.ResolveTypeMap(s.SourceType, s.DestinationType))) .ToArray(); - Task.WaitAll(tasks); + await Task.WhenAll(tasks); } } @@ -1144,7 +1144,7 @@ public class Dto } [Fact] - public void Should_work() + public async Task Should_work() { var sourceType = typeof(Entity<>); var destinationType = typeof(Dto<>); @@ -1178,6 +1178,6 @@ public void Should_work() .ToArray() .Select(s => Task.Factory.StartNew(() => mapper.Map(null, s.SourceType, s.DestinationType))) .ToArray(); - Task.WaitAll(tasks); + await Task.WhenAll(tasks); } } \ No newline at end of file diff --git a/src/UnitTests/CollectionMapping.cs b/src/UnitTests/CollectionMapping.cs index 9f2e906957..387fb57a38 100644 --- a/src/UnitTests/CollectionMapping.cs +++ b/src/UnitTests/CollectionMapping.cs @@ -1,8 +1,61 @@ using System.Collections.Specialized; using System.Collections.Immutable; - namespace AutoMapper.UnitTests; - +public class UnsupportedCollection : AutoMapperSpecBase +{ + class Source + { + public MyList List { get; set; } = new(); + } + class Destination + { + public MyList List { get; set; } + } + class MyList : IEnumerable + { + public IEnumerator GetEnumerator() => new List.Enumerator(); + } + protected override MapperConfiguration CreateConfiguration() => new(c => c.CreateMap()); + [Fact] + public void ThrowsAtMapTime() => new Action(()=>Map(new Source())).ShouldThrow() + .InnerException.ShouldBeOfType().Message.ShouldBe($"Unknown collection. Consider a custom type converter from {typeof(MyList)} to {typeof(MyList)}."); +} +public class When_mapping_interface_to_interface_readonly_set : AutoMapperSpecBase +{ + public class Source + { + public IReadOnlySet Values { get; set; } + } + public class Destination + { + public IReadOnlySet Values { get; set; } + } + protected override MapperConfiguration CreateConfiguration() => new(config => config.CreateMap()); + [Fact] + public void Should_map_readonly_values() + { + HashSet values = [1, 2, 3, 4]; + Map(new Source { Values = values }).Values.ShouldBe(values); + } +} +public class When_mapping_hashset_to_interface_readonly_set : AutoMapperSpecBase +{ + public class Source + { + public HashSet Values { get; set; } + } + public class Destination + { + public IReadOnlySet Values { get; set; } + } + protected override MapperConfiguration CreateConfiguration() => new(config => config.CreateMap()); + [Fact] + public void Should_map_readonly_values() + { + HashSet values = [1, 2, 3, 4]; + Map(new Source { Values = values }).Values.ShouldBe(values); + } +} public class NonPublicEnumeratorCurrent : AutoMapperSpecBase { class Source diff --git a/src/UnitTests/ConfigurationRules.cs b/src/UnitTests/ConfigurationRules.cs index b7f390d725..e297f6d2e2 100644 --- a/src/UnitTests/ConfigurationRules.cs +++ b/src/UnitTests/ConfigurationRules.cs @@ -33,19 +33,6 @@ public void Should_throw_for_multiple_create_map_calls() typeof(DuplicateTypeMapConfigurationException).ShouldBeThrownBy(() => config.AssertConfigurationIsValid()); } - [Fact] - public void Should_not_throw_when_allowing_multiple_create_map_calls() - { - var config = new MapperConfiguration(cfg => - { - cfg.CreateMap(); - cfg.CreateMap(); - cfg.Internal().AllowAdditiveTypeMapCreation = true; - }); - - typeof(DuplicateTypeMapConfigurationException).ShouldNotBeThrownBy(() => config.AssertConfigurationIsValid()); - } - [Fact] public void Should_throw_for_multiple_create_map_calls_in_different_profiles() { @@ -58,19 +45,6 @@ public void Should_throw_for_multiple_create_map_calls_in_different_profiles() typeof(DuplicateTypeMapConfigurationException).ShouldBeThrownBy(() => config.AssertConfigurationIsValid()); } - [Fact] - public void Should_not_throw_when_allowing_multiple_create_map_calls_in_different_profiles() - { - var config = new MapperConfiguration(cfg => - { - cfg.AddProfile(); - cfg.AddProfile(); - cfg.Internal().AllowAdditiveTypeMapCreation = true; - }); - - typeof(DuplicateTypeMapConfigurationException).ShouldNotBeThrownBy(() => config.AssertConfigurationIsValid()); - } - [Fact] public void Should_throw_for_multiple_create_map_calls_in_configuration_expression_and_profile() { diff --git a/src/UnitTests/ConfigurationValidation.cs b/src/UnitTests/ConfigurationValidation.cs index babd8e4a93..5327c9b5b5 100644 --- a/src/UnitTests/ConfigurationValidation.cs +++ b/src/UnitTests/ConfigurationValidation.cs @@ -795,4 +795,26 @@ public class Command } [Fact] public void Validate() => AssertConfigurationIsValid(); +} +public class ObjectPropertyAndNestedTypes : AutoMapperSpecBase +{ + protected override MapperConfiguration CreateConfiguration() => new(cfg => cfg.CreateMap()); + public class RootLevel + { + public object ObjectProperty { get; set; } + public SecondLevel SecondLevel { get; set; } + } + public class RootLevelDto + { + public object ObjectProperty { get; set; } + public SecondLevelDto SecondLevel { get; set; } + } + public class SecondLevel + { + } + public class SecondLevelDto + { + } + [Fact] + public void Should_fail_validation() => new Action(AssertConfigurationIsValid).ShouldThrow().MemberMap.DestinationName.ShouldBe(nameof(RootLevelDto.SecondLevel)); } \ No newline at end of file diff --git a/src/UnitTests/ContextItems.cs b/src/UnitTests/ContextItems.cs index eddba1ed8e..ad488bee3d 100644 --- a/src/UnitTests/ContextItems.cs +++ b/src/UnitTests/ContextItems.cs @@ -1,4 +1,30 @@ namespace AutoMapper.UnitTests; +public class When_mapping_with_context_state +{ + public class Source + { + public int Value { get; set; } + } + public class Dest + { + public int Value { get; set; } + } + public class ContextResolver : IMemberValueResolver + { + public int Resolve(Source src, Dest d, int source, int dest, ResolutionContext context) => source + (int)context.State; + } + [Fact] + public void Should_use_value_passed_in() + { + var config = new MapperConfiguration(cfg => + { + cfg.CreateMap() + .ForMember(d => d.Value, opt => opt.MapFrom(src => src.Value)); + }); + var dest = config.CreateMapper().Map(new Source { Value = 5 }, opt => { opt.State = 10; }); + dest.Value.ShouldBe(15); + } +} public class Context_try_get_items : AutoMapperSpecBase { protected override MapperConfiguration CreateConfiguration() => new(c => c.CreateMap().ConvertUsing((s, _, c) => diff --git a/src/UnitTests/Enumerations.cs b/src/UnitTests/Enumerations.cs index dd0ffaf58e..1d8b75a812 100644 --- a/src/UnitTests/Enumerations.cs +++ b/src/UnitTests/Enumerations.cs @@ -1,8 +1,29 @@ using System.Runtime.Serialization; using AutoMapper.UnitTests; - namespace AutoMapper.Tests; - +public class CreateProjectionEnum : AutoMapperSpecBase +{ + public class Source + { + public string Name { get; set; } + public SourceEnum Value { get; set; } + } + public class Dest + { + public string Name { get; set; } + public DestEnum Value { get; set; } + } + public enum SourceEnum { A, B } + public enum DestEnum { A, B } + protected override MapperConfiguration CreateConfiguration() => new(c => + { + c.CreateProjection().ConvertUsing(src => src == SourceEnum.A ? DestEnum.A : DestEnum.B); + c.CreateProjection(); + c.Internal().ForAllMaps(static (_, _) => { }); + }); + [Fact] + public void Should_work() => ProjectTo(new[] { new Source() }.AsQueryable()).Single().Value.ShouldBe(DestEnum.A); +} public class InvalidStringToEnum : AutoMapperSpecBase { protected override MapperConfiguration CreateConfiguration() => new(_=> { }); diff --git a/src/UnitTests/ForAllMembers.cs b/src/UnitTests/ForAllMembers.cs index e24001f4d0..b8e5890a2e 100644 --- a/src/UnitTests/ForAllMembers.cs +++ b/src/UnitTests/ForAllMembers.cs @@ -76,4 +76,28 @@ public void Should_use_resolver() dest.SomeDate.ShouldBe(source.SomeDate); dest.OtherDate.ShouldBe(source.OtherDate.AddDays(1)); } +} +public class ForAllPropertyMaps_ConvertUsing : AutoMapperSpecBase +{ + public class Well + { + public SpecialTags SpecialTags { get; set; } + } + [Flags] + public enum SpecialTags { None, SendState, NotSendZeroWhenOpen } + public class PostPutWellViewModel + { + public SpecialTags[] SpecialTags { get; set; } = Array.Empty(); + } + class EnumToArray : IValueConverter + { + public object Convert(object sourceMember, ResolutionContext context) => new[] { SpecialTags.SendState }; + } + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap(); + cfg.Internal().ForAllPropertyMaps(pm => pm.SourceType != null, (tm, mapper) => mapper.ConvertUsing(new EnumToArray())); + }); + [Fact] + public void ShouldWork() => Map(new Well()).SpecialTags.Single().ShouldBe(SpecialTags.SendState); } \ No newline at end of file diff --git a/src/UnitTests/IMappingExpression/IncludeMembers.cs b/src/UnitTests/IMappingExpression/IncludeMembers.cs index 072c88ccf4..d01f080d67 100644 --- a/src/UnitTests/IMappingExpression/IncludeMembers.cs +++ b/src/UnitTests/IMappingExpression/IncludeMembers.cs @@ -1776,4 +1776,35 @@ class Destination }); [Fact] public void Should_flatten() => Mapper.Map(new[] { default(Source) })[0].ShouldBeNull(); +} +public class IncludeMembersCascadedNullCheck : AutoMapperSpecBase +{ + public class Grandchild + { + public string C { get; set; } + } + public class Child + { + public string B { get; set; } + public Grandchild Grandchild { get; set; } + } + public class Parent + { + public string A { get; set; } + public Child Child { get; set; } + } + public class Dto + { + public string A { get; set; } + public string B { get; set; } + public string C { get; set; } + } + protected override MapperConfiguration CreateConfiguration() => new(c => + { + c.CreateMap().IncludeMembers(s => s.Child); + c.CreateMap(MemberList.None).IncludeMembers(s => s.Grandchild); + c.CreateMap(MemberList.None); + }); + [Fact] + public void Should_flatten() => Mapper.Map(new Parent { A = "a" }).A.ShouldBe("a"); } \ No newline at end of file diff --git a/src/UnitTests/InterfaceMapping.cs b/src/UnitTests/InterfaceMapping.cs index 34712f0540..6ae2010ef2 100644 --- a/src/UnitTests/InterfaceMapping.cs +++ b/src/UnitTests/InterfaceMapping.cs @@ -1,5 +1,26 @@ namespace AutoMapper.UnitTests.InterfaceMapping; - +public class InterfaceWithObjectProperty : AutoMapperSpecBase +{ + protected override MapperConfiguration CreateConfiguration() => new(cfg => cfg.CreateMap()); + public interface ISourceModel + { + object Id { get; set; } + } + public interface IDestModel + { + object Id { get; set; } + } + public class SourceModel : ISourceModel + { + public object Id { get; set; } + } + public class DestModel : IDestModel + { + public object Id { get; set; } + } + [Fact] + public void Should_work() => Mapper.Map(new SourceModel { Id = 42 }, new DestModel()).Id.ShouldBe(42); +} public class InterfaceInheritance : AutoMapperSpecBase { protected override MapperConfiguration CreateConfiguration() => new(cfg => diff --git a/src/UnitTests/Internal/CreateProxyThreading.cs b/src/UnitTests/Internal/CreateProxyThreading.cs index 84c49e2f83..5b938c16c5 100644 --- a/src/UnitTests/Internal/CreateProxyThreading.cs +++ b/src/UnitTests/Internal/CreateProxyThreading.cs @@ -5,13 +5,13 @@ namespace AutoMapper.UnitTests; public class CreateProxyThreading { [Fact] - public void Should_create_the_proxy_once() + public async Task Should_create_the_proxy_once() { var tasks = Enumerable.Range(0, 5).Select(i => Task.Factory.StartNew(() => { ProxyGenerator.GetProxyType(typeof(ISomeDto)); })).ToArray(); - Task.WaitAll(tasks); + await Task.WhenAll(tasks); } public interface ISomeDto diff --git a/src/UnitTests/Internal/GenerateSimilarType.cs b/src/UnitTests/Internal/GenerateSimilarType.cs index 0a717ff739..1afa729818 100644 --- a/src/UnitTests/Internal/GenerateSimilarType.cs +++ b/src/UnitTests/Internal/GenerateSimilarType.cs @@ -4,13 +4,13 @@ namespace AutoMapper.UnitTests; public class GenerateSimilarType { - public partial class Article + public partial record struct Article { public int Id { get; set; } public int ProductId { get; set; } public bool IsDefault { get; set; } public short NationId { get; set; } - public virtual Product Product { get; set; } + public Product Product { get; set; } } public partial class Product @@ -45,7 +45,7 @@ public void Should_work() instance.ECommercePublished = true; instance.Short = short.MaxValue; instance.Long = long.MaxValue; - var articles = new Article[] { new Article(), null, null }; + var articles = new Article[] { new Article(), default, default }; instance.Articles = articles; instance.Article = articles[0]; diff --git a/src/UnitTests/MappingInheritance/IncludedBaseMappingShouldInheritBaseMappings.cs b/src/UnitTests/MappingInheritance/IncludedBaseMappingShouldInheritBaseMappings.cs index 35579223e7..9244fb7307 100644 --- a/src/UnitTests/MappingInheritance/IncludedBaseMappingShouldInheritBaseMappings.cs +++ b/src/UnitTests/MappingInheritance/IncludedBaseMappingShouldInheritBaseMappings.cs @@ -27,6 +27,66 @@ public class OtherDto public string SubString { get; set; } } + public record class RecordObject(string DifferentBaseString) + { + } + + public record class RecordSubObject(string DifferentBaseString, string SubString) : RecordObject(DifferentBaseString) + { + } + + public record class RecordOtherObject(string BaseString) + { + } + + public record class RecordOtherSubObject(string BaseString, string SubString) : RecordOtherObject(BaseString) + { + } + + public record class RecordOtherSubObjectWithExtraParam(string BaseString, string SubString, string ExtraString) : RecordOtherObject(BaseString) + { + } + + public class ModelObjectWithConstructor + { + public ModelObjectWithConstructor(string onePrime) + { + OnePrime = onePrime; + } + + public string OnePrime { get; } + } + + public class ModelSubObjectWithConstructor : ModelObjectWithConstructor + { + public ModelSubObjectWithConstructor(string onePrime, string two) : base(onePrime) + { + Two = two; + } + + public string Two { get; } + } + + public class DtoObjectWithConstructor + { + public DtoObjectWithConstructor(string one) + { + One = one; + } + + public string One { get; } + } + + public class DtoSubObjectWithConstructorAndWrongType : DtoObjectWithConstructor + { + public DtoSubObjectWithConstructorAndWrongType(int one, string two) : base(one.ToString()) + { + Two = two; + } + + public string Two { get; } + } + [Fact] public void included_mapping_should_inherit_base_mappings_should_not_throw() { @@ -320,6 +380,44 @@ public void include_should_apply_null_substitute() dest.BaseString.ShouldBe("12345"); } + + [Fact] + public void included_mapping_should_inherit_base_constructor_mappings_should_not_throw() + { + var config = new MapperConfiguration(cfg => + { + cfg.ShouldUseConstructor = constructor => constructor.IsPublic; + cfg.CreateMap() + .ForCtorParam(nameof(RecordOtherObject.BaseString), m => m.MapFrom(s => s.DifferentBaseString)) + .Include() + .Include(); + cfg.CreateMap(); + cfg.CreateMap() + .ForCtorParam(nameof(RecordOtherSubObjectWithExtraParam.ExtraString), m => m.MapFrom(s => s.DifferentBaseString + s.SubString)); + }); + config.AssertConfigurationIsValid(); + + var mapper = config.CreateMapper(); + var dest = mapper.Map(new RecordSubObject("base", "sub")); + + dest.BaseString.ShouldBe("base"); + dest.SubString.ShouldBe("sub"); + dest.ExtraString.ShouldBe("basesub"); + } + + [Fact] + public void included_mapping_with_parameter_has_same_name_but_diffent_type_should_throw() + { + var config = new MapperConfiguration(cfg => + { + cfg.CreateMap() + .ForCtorParam("one", m => m.MapFrom(s => s.OnePrime)) + .Include(); + cfg.CreateMap(); + }); + + Assert.Throws(config.AssertConfigurationIsValid).Errors.Single().CanConstruct.ShouldBeFalse(); + } } public class OverrideDifferentMapFrom : AutoMapperSpecBase diff --git a/src/UnitTests/MappingInheritance/IncludedMappingShouldInheritBaseMappings.cs b/src/UnitTests/MappingInheritance/IncludedMappingShouldInheritBaseMappings.cs index 66eb31db63..54dc32de8e 100644 --- a/src/UnitTests/MappingInheritance/IncludedMappingShouldInheritBaseMappings.cs +++ b/src/UnitTests/MappingInheritance/IncludedMappingShouldInheritBaseMappings.cs @@ -1,5 +1,34 @@ namespace AutoMapper.UnitTests; - +public class IncludeBaseIndirectBase : AutoMapperSpecBase +{ + public class FooBaseBase + { + } + public class FooBase : FooBaseBase + { + } + public class Foo : FooBase + { + } + public class FooDtoBaseBase + { + public DateTime Date { get; set; } + } + public class FooDtoBase : FooDtoBaseBase + { + } + public class FooDto : FooDtoBase + { + } + protected override MapperConfiguration CreateConfiguration() => new(c => + { + c.CreateMap().IncludeBase(); + c.CreateMap().IncludeBase(); + c.CreateMap().ForMember(d => d.Date, o => o.MapFrom(s => DateTime.MaxValue)); + }); + [Fact] + public void Should_work() => Map(new Foo()).Date.ShouldBe(DateTime.MaxValue); +} public class ReadonlyCollectionPropertiesOverride : AutoMapperSpecBase { protected override MapperConfiguration CreateConfiguration() => new(cfg => diff --git a/src/UnitTests/NullBehavior.cs b/src/UnitTests/NullBehavior.cs index 7d29dca688..59faf94037 100644 --- a/src/UnitTests/NullBehavior.cs +++ b/src/UnitTests/NullBehavior.cs @@ -1,4 +1,22 @@ namespace AutoMapper.UnitTests.NullBehavior; +public class NullDestinationType : AutoMapperSpecBase +{ + protected override MapperConfiguration CreateConfiguration() => new(c => { }); + [Fact] + public void Should_require_destination_object() + { + new Action(() => Mapper.Map("", null, null)).ShouldThrow().ParamName.ShouldBe("destinationType"); + new Action(() => Mapper.Map("", null, null, _=>{ })).ShouldThrow().ParamName.ShouldBe("destinationType"); + Mapper.Map("", "", null, null).ShouldBe(""); + Mapper.Map("", null, null, typeof(string)).ShouldBe(""); + Mapper.Map("", "", null, null, _ => { }).ShouldBe(""); + Mapper.Map("", null, null, typeof(string), _=>{ }).ShouldBe(""); + Mapper.Map("").ShouldBe(""); + Mapper.Map("", default(string)).ShouldBe(""); + Mapper.Map("", _ => { }).ShouldBe(""); + Mapper.Map("", default(string), _ => { }).ShouldBe(""); + } +} public class NullToExistingDestination : AutoMapperSpecBase { protected override MapperConfiguration CreateConfiguration() => new(c => c.CreateMap().DisableCtorValidation()); diff --git a/src/UnitTests/Projection/ConstructorTests.cs b/src/UnitTests/Projection/ConstructorTests.cs index 74156d00c5..aa811a4ead 100644 --- a/src/UnitTests/Projection/ConstructorTests.cs +++ b/src/UnitTests/Projection/ConstructorTests.cs @@ -199,6 +199,508 @@ public void Should_construct_correctly() } } public class NestedConstructors : AutoMapperSpecBase +{ + public class A + { + public int Id { get; set; } + public B B { get; set; } + } + public class B + { + public int Id { get; set; } + } + public class DtoA + { + public DtoB B { get; } + public DtoA(DtoB b) => B = b; + } + public class DtoB + { + public int Id { get; } + public DtoB(int id) => Id = id; + } + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateProjection(); + cfg.CreateProjection(); + }); + [Fact] + public void Should_project_ok() => + ProjectTo(new[] { new A { B = new B { Id = 3 } } }.AsQueryable()).FirstOrDefault().B.Id.ShouldBe(3); +} + +public class ConstructorLetClauseWithIheritance : AutoMapperSpecBase +{ + class Source + { + public IList Items { get; set; } + } + class SourceA : Source + { + public string A { get; set; } + } + class SourceB : Source + { + public string B { get; set; } + } + + class SourceItem + { + public IList Values { get; set; } + } + class SourceValue + { + public int Value1 { get; set; } + public int Value2 { get; set; } + } + class Destination + { + public Destination(DestinationItem item) => Item = item; + public DestinationItem Item { get; } + } + class DestinationA : Destination + { + public DestinationA(DestinationItem item, string a) : base(item) => A = a; + public string A { get; } + } + class DestinationB : Destination + { + public DestinationB(DestinationItem item, string b) : base(item) => B = b; + public string B { get; } + } + + class DestinationValue + { + public DestinationValue(int value1, int value2) + { + Value1 = value1; + Value2 = value2; + } + public int Value1 { get; } + public int Value2 { get; } + } + class DestinationItem + { + public DestinationItem(DestinationValue destinationValue) + { + Value1 = destinationValue.Value1; + Value2 = destinationValue.Value2; + } + public int Value1 { get; } + public int Value2 { get; } + public IList Values { get; set; } + } + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap() + .ForCtorParam("item", o => o.MapFrom(s => s.Items.FirstOrDefault())) + .Include() + .Include(); + cfg.CreateMap() + .ForCtorParam("item", o => o.MapFrom(s => s.Items.FirstOrDefault())) + .ForCtorParam("a", o => o.MapFrom(s => s.A)); + cfg.CreateMap() + .ForCtorParam("item", o => o.MapFrom(s => s.Items.FirstOrDefault())) + .ForCtorParam("b", o => o.MapFrom(s => s.B)); + + cfg.CreateMap().ForCtorParam("destinationValue", o => o.MapFrom(s => s.Values.FirstOrDefault())); + cfg.CreateMap(); + }); + [Fact] + public void Should_construct_correctly() + { + var query = new[] { + new SourceA + { + A = "a", + Items = new[] + { + new SourceItem { Values = new[] { new SourceValue { Value1 = 1, Value2 = 2 } } } + } + }, + new SourceB + { + B = "b", + Items = new[] + { + new SourceItem { Values = new[] { new SourceValue { Value1 = 1, Value2 = 2 } } } + } + }, + new Source + { + Items = new[] + { + new SourceItem { Values = new[] { new SourceValue { Value1 = 1, Value2 = 2 } } } + } + } + }.AsQueryable().ProjectTo(Configuration); + + var list = query.ToList(); + var first = list.First(); + first.Item.Value1.ShouldBe(1); + first.Item.Value2.ShouldBe(2); + var firstValue = first.Item.Values.Single(); + firstValue.Value1.ShouldBe(1); + firstValue.Value2.ShouldBe(2); + + list.OfType().Any(a => a.A == "a").ShouldBeTrue(); + list.OfType().Any(a => a.B == "b").ShouldBeTrue(); + } +} +public class ConstructorToStringWithIheritance : AutoMapperSpecBase +{ + class Source + { + public int Value { get; set; } + } + class SourceA : Source + { + public string A { get; set; } + } + class SourceB : Source + { + public string B { get; set; } + } + class Destination + { + public Destination(string value) => Value = value; + public string Value { get; } + } + class DestinationA : Destination + { + public DestinationA(string value, string a) : base(value) => A = a; + public string A { get; } + } + class DestinationB : Destination + { + public DestinationB(string value, string b) : base(value) => B = b; + public string B { get; } + } + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap() + .Include(); + cfg.CreateMap(); + cfg.CreateMap(); + }); + [Fact] + public void Should_construct_correctly() + { + var list = new[] + { + new Source { Value = 5 }, + new SourceA { Value = 5, A = "a" }, + new SourceB { Value = 5, B = "b" } + }.AsQueryable().ProjectTo(Configuration); + + list.ShouldAllBe(p => p.Value == "5"); + list.OfType().Any(p => p.A == "a"); + list.OfType().Any(p => p.B == "b"); + } +} +public class ConstructorMapFromWithIheritance : AutoMapperSpecBase +{ + class Source + { + public int Value { get; set; } + } + class SourceA : Source + { + public string A { get; set; } + } + class SourceB : Source + { + public string B { get; set; } + } + record Destination(bool Value) + { + } + record DestinationA(bool Value, bool HasA) : Destination(Value) + { + } + record DestinationB(bool Value, bool HasB) : Destination(Value) + { + } + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap() + .ForCtorParam(nameof(Destination.Value), o => o.MapFrom(s => s.Value == 5)) + .Include() + .Include(); + cfg.CreateMap() + .ForCtorParam(nameof(Destination.Value), o => o.MapFrom(s => s.Value == 5)) + .ForCtorParam(nameof(DestinationA.HasA), o => o.MapFrom(s => s.A == "a")); + cfg.CreateMap() + .ForCtorParam(nameof(Destination.Value), o => o.MapFrom(s => s.Value == 5)) + .ForCtorParam(nameof(DestinationB.HasB), o => o.MapFrom(s => s.B == "b")); + }); + [Fact] + public void Should_construct_correctly() + { + var list = new[] + { + new Source { Value = 5 }, + new SourceA { Value = 5, A = "a" }, + new SourceB { Value = 5, B = "b" } + }.AsQueryable().ProjectTo(Configuration); + + list.All(p => p.Value).ShouldBeTrue(); + list.OfType().Any(p => p.HasA).ShouldBeTrue(); + list.OfType().Any(p => p.HasB).ShouldBeTrue(); + } +} +public class ConstructorIncludeMembersWithIheritance : AutoMapperSpecBase +{ + class SourceWrapper + { + public Source Source { get; set; } + } + class SourceWrapperA : SourceWrapper + { + public SourceA SourceA { get; set; } + } + class SourceWrapperB : SourceWrapper + { + public SourceB SourceB { get; set; } + } + class Source + { + public int Value { get; set; } + } + class SourceA + { + public string A { get; set; } + } + class SourceB + { + public string B { get; set; } + } + class Destination + { + public Destination(string value) => Value = value; + public string Value { get; } + } + + class DestinationA : Destination + { + public DestinationA(string value, string a) : base(value) => A = a; + public string A { get; } + } + class DestinationB : Destination + { + public DestinationB(string value, string b) : base(value) => B = b; + public string B { get; } + } + + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap() + .IncludeMembers(s => s.Source) + .Include() + .Include(); + cfg.CreateMap() + .IncludeMembers(s => s.Source, s => s.SourceA); + cfg.CreateMap() + .IncludeMembers(s => s.Source, s => s.SourceB); + cfg.CreateMap(); + cfg.CreateMap(MemberList.None); + cfg.CreateMap(MemberList.None); + cfg.CreateMap(MemberList.None); + cfg.CreateMap(MemberList.None); + }); + [Fact] + public void Should_construct_correctly() + { + var list = new[] + { + new SourceWrapper { Source = new Source { Value = 5 } }, + new SourceWrapperA { Source = new Source { Value = 5 }, SourceA = new SourceA() { A = "a" } }, + new SourceWrapperB { Source = new Source { Value = 5 }, SourceB = new SourceB() { B = "b" } } + }.AsQueryable().ProjectTo(Configuration); + + list.All(p => p.Value == "5").ShouldBeTrue(); + list.OfType().Any(p => p.A == "a").ShouldBeTrue(); + list.OfType().Any(p => p.B == "b").ShouldBeTrue(); + } +} +public class ConstructorsWithCollectionsWithIheritance : AutoMapperSpecBase +{ + class Addresses + { + public int Id { get; set; } + public string Address { get; set; } + public ICollection Users { get; set; } + } + class Users + { + public int Id { get; set; } + public Addresses FkAddress { get; set; } + } + class UsersA : Users + { + public string A { get; set; } + } + class UsersB : Users + { + public string B { get; set; } + } + class AddressDto + { + public int Id { get; } + public string Address { get; } + public AddressDto(int id, string address) + { + Id = id; + Address = address; + } + } + class UserDto + { + public int Id { get; set; } + public AddressDto AddressDto { get; set; } + } + class UserADto : UserDto + { + public string A { get; set; } + } + class UserBDto : UserDto + { + public string B { get; set; } + } + + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap() + .ForMember(d => d.AddressDto, e => e.MapFrom(s => s.FkAddress)) + .Include() + .Include(); + + cfg.CreateMap(); + cfg.CreateMap(); + + cfg.CreateMap().ConstructUsing(a => new AddressDto(a.Id, a.Address)); + }); + [Fact] + public void Should_work() + { + var list = ProjectTo(new[] + { + new Users { FkAddress = new Addresses { Address = "address" } }, + new UsersA { A = "a", FkAddress = new Addresses { Address = "address" } }, + new UsersB { B = "b", FkAddress = new Addresses { Address = "address" } } + }.AsQueryable()).ToList(); + + list.All(p => p.AddressDto.Address == "address").ShouldBeTrue(); + list.OfType().Any(p => p.A == "a").ShouldBeTrue(); + list.OfType().Any(p => p.B == "b").ShouldBeTrue(); + } +} +public class ConstructorTestsWithIheritance : AutoMapperSpecBase +{ + private Dest[] _dest; + + public class Source + { + public int Value { get; set; } + } + public class SourceA : Source + { + public string A { get; set; } + } + public class SourceB : Source + { + public string B { get; set; } + } + + public class Dest + { + public Dest() + { + + } + public Dest(int other) + { + Other = other; + } + + public int Value { get; set; } + [IgnoreMap] + public int Other { get; set; } + } + public class DestA : Dest + { + public DestA() : base() + { + + } + public DestA(int other, string otherA) : base(other) + { + OtherA = otherA; + } + [IgnoreMap] + public string OtherA { get; set; } + } + public class DestB : Dest + { + public DestB() : base() + { + + } + public DestB(int other, string otherB) : base(other) + { + OtherB = otherB; + } + [IgnoreMap] + public string OtherB { get; set; } + } + + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.AddIgnoreMapAttribute(); + cfg.CreateMap() + .ConstructUsing(src => new Dest(src.Value + 10)) + .Include() + .Include(); + + cfg.CreateMap() + .ConstructUsing(src => new DestA(src.Value + 10, src.A + "a")); + + cfg.CreateMap() + .ConstructUsing(src => new DestB(src.Value + 10, src.B + "b")); + }); + + protected override void Because_of() + { + var values = new[] + { + new Source() + { + Value = 5 + }, + new SourceA() + { + Value = 5, + A = "a" + }, + new SourceB() + { + Value = 5, + B = "b" + } + }.AsQueryable(); + + _dest = values.ProjectTo(Configuration).ToArray(); + } + + [Fact] + public void Should_construct_correctly() + { + _dest.All(p => p.Other == 15).ShouldBeTrue(); + _dest.OfType().Any(p => p.OtherA == "aa").ShouldBeTrue(); + _dest.OfType().Any(p => p.OtherB == "bb").ShouldBeTrue(); + } +} +public class NestedConstructorsWithIheritance : AutoMapperSpecBase { public class A { diff --git a/src/UnitTests/Projection/ProjectCollectionListTest.cs b/src/UnitTests/Projection/ProjectCollectionListTest.cs index c6521aa244..c9264d709d 100644 --- a/src/UnitTests/Projection/ProjectCollectionListTest.cs +++ b/src/UnitTests/Projection/ProjectCollectionListTest.cs @@ -87,3 +87,26 @@ public override bool Equals(object obj) } } } +public class MapProjection : AutoMapperSpecBase +{ + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateProjection(); + cfg.CreateMap(); + }); + [Fact] + public void ShouldNotMap() => new Action(() => Map(new Customer())).ShouldThrow().Message.ShouldBe("CreateProjection works with ProjectTo, not with Map."); + public class Customer + { + public IList
Addresses { get; set; } + } + public record class Address(string Street); + public class CustomerDto + { + public IList Addresses { get; set; } + } + public class AddressDto + { + public string Street { get; set; } + } +} \ No newline at end of file diff --git a/src/UnitTests/ReverseMapping.cs b/src/UnitTests/ReverseMapping.cs index 5eb946c964..606bc770ad 100644 --- a/src/UnitTests/ReverseMapping.cs +++ b/src/UnitTests/ReverseMapping.cs @@ -442,6 +442,13 @@ public void Should_create_a_map_with_the_reverse_items() { _source.Value.ShouldBe(10); } + + [Fact] + public void Should_not_initialize_details_on_initial_mapping() + { + var map = FindTypeMapFor(); + map.HasDetails.ShouldBeFalse(); + } } public class When_validating_only_against_source_members_and_source_matches : AutoMapperSpecBase @@ -673,4 +680,31 @@ public void Should_reverse_map_ok() source.Value.ShouldBe(1337); source.StringValue.ShouldBe("StringValue2"); } +} + +public class When_validating_reverse_mapping_classes_with_missing_properties : AutoMapperSpecBase +{ + public class Source + { + public int SomeValue { get; set; } + public int SomeValue2 { get; set; } + } + + public class Destination + { + public int SomeValue { get; set; } + } + + protected override MapperConfiguration CreateConfiguration() => new(cfg => + { + cfg.CreateMap(MemberList.Destination) + .ReverseMap() + .ValidateMemberList(MemberList.Destination); + }); + + [Fact] + public void Should_throw_a_configuration_validation_error() + { + typeof(AutoMapperConfigurationException).ShouldBeThrownBy(AssertConfigurationIsValid); + } } \ No newline at end of file diff --git a/src/UnitTests/TypeConverters.cs b/src/UnitTests/TypeConverters.cs index d05f334c8c..c18e51895e 100644 --- a/src/UnitTests/TypeConverters.cs +++ b/src/UnitTests/TypeConverters.cs @@ -1,5 +1,26 @@ namespace AutoMapper.UnitTests.CustomMapping; - +public class StringToEnumConverter : AutoMapperSpecBase +{ + class Source + { + public string Enum { get; set; } + } + class Destination + { + public ConsoleColor Enum { get; set; } + } + protected override MapperConfiguration CreateConfiguration() => new(c => + { + c.CreateMap().ConvertUsing(s => ConsoleColor.DarkCyan); + c.CreateMap(); + }); + [Fact] + public void Should_work() + { + Map("").ShouldBe(ConsoleColor.DarkCyan); + Map(new Source()).Enum.ShouldBe(ConsoleColor.DarkCyan); + } +} public class NullableConverter : AutoMapperSpecBase { public enum GreekLetters diff --git a/src/UnitTests/ValueTypes.cs b/src/UnitTests/ValueTypes.cs index 83a6db6e41..54890c7eae 100644 --- a/src/UnitTests/ValueTypes.cs +++ b/src/UnitTests/ValueTypes.cs @@ -197,4 +197,12 @@ public void Should_still_map_value_type() { _destination.Value1.ShouldBe(10); } +} +public class ValueTypeDestinationPreserveReferences : AutoMapperSpecBase +{ + record Source(List List); + record struct Destination(List List); + protected override MapperConfiguration CreateConfiguration() => new(cfg => cfg.CreateMap()); + [Fact] + public void ShouldWork() => Map(new Source(new() { new Source(null) })).List.Single().List.ShouldBeEmpty(); } \ No newline at end of file