From 2897535995ce6e04c37515877c82718f32e90b73 Mon Sep 17 00:00:00 2001 From: latonz Date: Fri, 1 Sep 2023 11:02:36 +0200 Subject: [PATCH] feat: Remove runtime dependency on Riok.Mapperly.Abstractions Creates a new Riok.Mapperly.Templates project with templates. Some Mapperly features may require such a template (currently only the reference handling). If such a template is required by a used Mapperly feature, Mapperly emites the template in a namespace which includes the name of the assembly of the target project. This is done to prevent collisions when using InternalsVisibleTo (see #685). Currently the only themplate is the PreserveReferenceHandler which is emitted if reference handling is enabled for any mapper and the mapper does have mapper method definitions without a reference handling parameter and Mapperly needs to instantiate a IReferenceHandler. --- Directory.Build.props | 3 + Riok.Mapperly.sln | 14 ++ .../Riok.Mapperly.Benchmarks.csproj | 4 - docs/docs/contributing/architecture.md | 24 ++-- docs/docs/contributing/common-tasks.md | 1 + docs/docs/getting-started/installation.mdx | 14 +- docs/docs/intro.md | 1 + .../Riok.Mapperly.Sample.csproj | 2 - src/Directory.Build.props | 1 - .../PublicAPI.Shipped.txt | 4 - .../Internal/PreserveReferenceHandler.cs | 63 --------- .../Riok.Mapperly.Abstractions.csproj | 1 + src/Riok.Mapperly.Templates/.editorconfig | 3 + .../PreserveReferenceHandler.cs | 83 ++++++++++++ .../Riok.Mapperly.Templates.csproj | 36 +++++ .../Descriptors/DescriptorBuilder.cs | 17 +++ .../Descriptors/MapperDescriptor.cs | 6 + .../Descriptors/Mappings/MethodMapping.cs | 2 + .../UserDefinedExistingTargetMethodMapping.cs | 6 +- .../UserDefinedNewInstanceMethodMapping.cs | 7 +- ...inedNewInstanceRuntimeTargetTypeMapping.cs | 5 +- src/Riok.Mapperly/Emit/SourceEmitter.cs | 6 +- .../Emit/Syntax/SyntaxFactoryHelper.New.cs | 4 +- .../Emit/Syntax/SyntaxFactoryHelper.cs | 14 +- src/Riok.Mapperly/Helpers/FileNameBuilder.cs | 7 +- .../Helpers/ImmutableEquatableArray.cs | 8 -- .../IncrementalValuesProviderExtensions.cs | 12 ++ src/Riok.Mapperly/MapperGenerator.cs | 16 ++- src/Riok.Mapperly/MapperGeneratorStepNames.cs | 3 +- .../Output/MapperAndDiagnostics.cs | 11 +- src/Riok.Mapperly/Riok.Mapperly.csproj | 10 +- .../Templates/TemplateContent.cs | 3 + src/Riok.Mapperly/Templates/TemplateReader.cs | 38 ++++++ .../Templates/TemplateReference.cs | 8 ++ test/Directory.Build.props | 5 +- .../Riok.Mapperly.Abstractions.Tests.csproj | 4 - .../BaseMapperTest.cs | 4 - .../Riok.Mapperly.IntegrationTests.csproj | 9 +- ...erTest.SnapshotGeneratedSource.verified.cs | 2 +- .../PreserveReferenceHandlerTest.cs | 4 +- .../Riok.Mapperly.Templates.Tests.csproj | 7 + .../IncrementalGeneratorRunReasons.cs | 53 ++++++++ .../IncrementalGeneratorTemplatesTest.cs | 30 ++++ .../Generator/IncrementalGeneratorTest.cs | 128 +++--------------- .../IncrementalGeneratorTestHelper.cs | 65 +++++++++ .../Mapping/GenericTest.cs | 4 +- .../Mapping/ReferenceHandlingTest.cs | 4 +- .../Mapping/RuntimeTargetTypeMappingTest.cs | 4 +- .../Mapping/UseMapperTest.cs | 4 +- .../Mapping/UseStaticMapperTest.cs | 4 +- .../Riok.Mapperly.Tests.csproj | 4 - ...bledReferenceHandling#Mapper.g.verified.cs | 6 +- ...nal.PreserveReferenceHandler.g.verified.cs | 84 ++++++++++++ ...eHandlingAndParameter#Mapper.g.verified.cs | 18 +-- ...gTest.ArrayShouldWork#Mapper.g.verified.cs | 2 +- ...nal.PreserveReferenceHandler.g.verified.cs | 84 ++++++++++++ ....EnumerableShouldWork#Mapper.g.verified.cs | 2 +- ...nal.PreserveReferenceHandler.g.verified.cs | 84 ++++++++++++ ...ingInstanceShouldWork#Mapper.g.verified.cs | 2 +- ...nal.PreserveReferenceHandler.g.verified.cs | 84 ++++++++++++ ...dPropertiesShouldWork#Mapper.g.verified.cs | 4 +- ...nal.PreserveReferenceHandler.g.verified.cs | 84 ++++++++++++ ...jectFactoryShouldWork#Mapper.g.verified.cs | 2 +- ...nal.PreserveReferenceHandler.g.verified.cs | 84 ++++++++++++ ...eTargetTypeShouldWork#Mapper.g.verified.cs | 2 +- ...nal.PreserveReferenceHandler.g.verified.cs | 84 ++++++++++++ ...ndlingTest.ShouldWork#Mapper.g.verified.cs | 2 +- ...nal.PreserveReferenceHandler.g.verified.cs | 84 ++++++++++++ ...enceHandlerShouldWork#Mapper.g.verified.cs | 2 +- ...nal.PreserveReferenceHandler.g.verified.cs | 84 ++++++++++++ ...enceHandlerShouldWork#Mapper.g.verified.cs | 2 +- ...nal.PreserveReferenceHandler.g.verified.cs | 84 ++++++++++++ ...renceHandlerParameter#Mapper.g.verified.cs | 18 +-- ...erenceHandlingEnabled#Mapper.g.verified.cs | 6 +- ...nal.PreserveReferenceHandler.g.verified.cs | 84 ++++++++++++ ...est.ReferenceHandling#Mapper.g.verified.cs | 7 +- ...ingEnabledNoParameter#Mapper.g.verified.cs | 7 +- ...est.ReferenceHandling#Mapper.g.verified.cs | 7 +- ...ingEnabledNoParameter#Mapper.g.verified.cs | 7 +- 79 files changed, 1465 insertions(+), 327 deletions(-) delete mode 100644 src/Riok.Mapperly.Abstractions/ReferenceHandling/Internal/PreserveReferenceHandler.cs create mode 100644 src/Riok.Mapperly.Templates/.editorconfig create mode 100644 src/Riok.Mapperly.Templates/PreserveReferenceHandler.cs create mode 100644 src/Riok.Mapperly.Templates/Riok.Mapperly.Templates.csproj create mode 100644 src/Riok.Mapperly/Templates/TemplateContent.cs create mode 100644 src/Riok.Mapperly/Templates/TemplateReader.cs create mode 100644 src/Riok.Mapperly/Templates/TemplateReference.cs rename test/{Riok.Mapperly.Abstractions.Tests/ReferenceHandling/Internal => Riok.Mapperly.Templates.Tests}/PreserveReferenceHandlerTest.cs (85%) create mode 100644 test/Riok.Mapperly.Templates.Tests/Riok.Mapperly.Templates.Tests.csproj create mode 100644 test/Riok.Mapperly.Tests/Generator/IncrementalGeneratorRunReasons.cs create mode 100644 test/Riok.Mapperly.Tests/Generator/IncrementalGeneratorTemplatesTest.cs create mode 100644 test/Riok.Mapperly.Tests/Generator/IncrementalGeneratorTestHelper.cs create mode 100644 test/Riok.Mapperly.Tests/_snapshots/GenericTest.WithGenericSourceAndTargetAndEnabledReferenceHandling#MapperlyInternal.PreserveReferenceHandler.g.verified.cs create mode 100644 test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ArrayShouldWork#MapperlyInternal.PreserveReferenceHandler.g.verified.cs create mode 100644 test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.EnumerableShouldWork#MapperlyInternal.PreserveReferenceHandler.g.verified.cs create mode 100644 test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ExistingInstanceShouldWork#MapperlyInternal.PreserveReferenceHandler.g.verified.cs create mode 100644 test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ManuallyMappedPropertiesShouldWork#MapperlyInternal.PreserveReferenceHandler.g.verified.cs create mode 100644 test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ObjectFactoryShouldWork#MapperlyInternal.PreserveReferenceHandler.g.verified.cs create mode 100644 test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.RuntimeTargetTypeShouldWork#MapperlyInternal.PreserveReferenceHandler.g.verified.cs create mode 100644 test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ShouldWork#MapperlyInternal.PreserveReferenceHandler.g.verified.cs create mode 100644 test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.UserImplementedWithReferenceHandlerShouldWork#MapperlyInternal.PreserveReferenceHandler.g.verified.cs create mode 100644 test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.UserImplementedWithoutReferenceHandlerShouldWork#MapperlyInternal.PreserveReferenceHandler.g.verified.cs create mode 100644 test/Riok.Mapperly.Tests/_snapshots/RuntimeTargetTypeMappingTest.WithReferenceHandlingEnabled#MapperlyInternal.PreserveReferenceHandler.g.verified.cs diff --git a/Directory.Build.props b/Directory.Build.props index 93ba43921d..5f80726258 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,9 @@ + net7.0 + true + embedded enable enable 0.0.1-dev diff --git a/Riok.Mapperly.sln b/Riok.Mapperly.sln index 1104608627..4312128ae0 100644 --- a/Riok.Mapperly.sln +++ b/Riok.Mapperly.sln @@ -25,6 +25,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Riok.Mapperly.Benchmarks", EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarks", "benchmarks", "{961BAABA-0672-48E7-A5B3-30A676146BE3}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Riok.Mapperly.Templates", "src\Riok.Mapperly.Templates\Riok.Mapperly.Templates.csproj", "{FF31D522-6A62-4466-90F7-6B297F82FCF3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Riok.Mapperly.Templates.Tests", "test\Riok.Mapperly.Templates.Tests\Riok.Mapperly.Templates.Tests.csproj", "{1500A843-37E3-4DBA-8BAB-A40CF14678ED}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -59,6 +63,14 @@ Global {F2214C71-15A7-46EB-A3AA-D02EF4B705EF}.Debug|Any CPU.Build.0 = Debug|Any CPU {F2214C71-15A7-46EB-A3AA-D02EF4B705EF}.Release|Any CPU.ActiveCfg = Release|Any CPU {F2214C71-15A7-46EB-A3AA-D02EF4B705EF}.Release|Any CPU.Build.0 = Release|Any CPU + {FF31D522-6A62-4466-90F7-6B297F82FCF3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FF31D522-6A62-4466-90F7-6B297F82FCF3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FF31D522-6A62-4466-90F7-6B297F82FCF3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FF31D522-6A62-4466-90F7-6B297F82FCF3}.Release|Any CPU.Build.0 = Release|Any CPU + {1500A843-37E3-4DBA-8BAB-A40CF14678ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1500A843-37E3-4DBA-8BAB-A40CF14678ED}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1500A843-37E3-4DBA-8BAB-A40CF14678ED}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1500A843-37E3-4DBA-8BAB-A40CF14678ED}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -71,6 +83,8 @@ Global {C3C40A0A-168F-4A66-B9F9-FC80D2F26306} = {3598BE50-28D5-4BF4-BEA7-09E5FEA2910C} {43A2E8E0-5A2C-45E9-84EF-CF934EC946FA} = {0FBD6C81-7E7A-4915-90D2-896F11C89FF0} {F2214C71-15A7-46EB-A3AA-D02EF4B705EF} = {961BAABA-0672-48E7-A5B3-30A676146BE3} + {FF31D522-6A62-4466-90F7-6B297F82FCF3} = {B65AF89A-4A3B-473C-83C8-5F0CB0EED30E} + {1500A843-37E3-4DBA-8BAB-A40CF14678ED} = {3598BE50-28D5-4BF4-BEA7-09E5FEA2910C} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {BAAC5976-BEE6-440E-8DDC-90916A1001A1} diff --git a/benchmarks/Riok.Mapperly.Benchmarks/Riok.Mapperly.Benchmarks.csproj b/benchmarks/Riok.Mapperly.Benchmarks/Riok.Mapperly.Benchmarks.csproj index 74d96d652d..a0d464ba3a 100644 --- a/benchmarks/Riok.Mapperly.Benchmarks/Riok.Mapperly.Benchmarks.csproj +++ b/benchmarks/Riok.Mapperly.Benchmarks/Riok.Mapperly.Benchmarks.csproj @@ -2,10 +2,6 @@ Exe - net7.0 - enable - enable - true false true diff --git a/docs/docs/contributing/architecture.md b/docs/docs/contributing/architecture.md index a21b727619..45c7fe2d56 100644 --- a/docs/docs/contributing/architecture.md +++ b/docs/docs/contributing/architecture.md @@ -8,14 +8,19 @@ description: The architecture of Mapperly. Mapperly is an incremental .NET source generator implementation. Source generators are explained [here](https://github.com/dotnet/roslyn/blob/main/docs/features/source-generators.cookbook.md) and [here](https://github.com/dotnet/roslyn/blob/main/docs/features/incremental-generators.md). -However, the incremental source generator of Mapperly is not yet optimal (see [#72](https://github.com/riok/mapperly/issues/72)). -## Projects +## Solution -Mapperly is structured in two projects. -`Riok.Mapperly.Abstractions` includes abstractions and attributes to be used by the application code, -this is referenced by the source generator. -`Riok.Mapperly` includes the implementation of the source generator. +- `benchmarks` Benchmarks to analyze the performance of the generated code and the source generator itself +- `build` Build scripts +- `docs` Documentation of Mapperly +- `samples` Sample implementations of Mappers using Mapperly +- `src` Source code of Mapperly + - `Riok.Mapperly` The source generator implementation + - `Riok.Mapperly.Abstractions` Abstractions and attributes to be used by the application code to configure Mapperly. + This is referenced by the source generator but is not needed at runtime. + - `Riok.Mapperly.Templates` Templates of code files which are embedded as resources into `Riok.Mapperly` and may be emitted during source generation depending on enabled features. +- `test` Unit- and integration tests of Mapperly ## Flow @@ -24,11 +29,12 @@ For each discovered `MapperAttribute` a new `DescriptorBuilder` is created. The `DescriptorBuilder` is responsible to build a `MapperDescriptor` which holds all the mappings. The `DescriptorBuilder` does this by following this process: -1. Extracting the configuration from the attribute +1. Extracting the configuration from the attributes 2. Extracting user implemented object factories 3. Extracting user implemented and user defined mapping methods. It instantiates a `User*Mapping` (eg. `UserDefinedNewInstanceMethodMapping`) for each discovered mapping method and adds it to the queue of mappings to work on. -4. For each mapping in the queue the `DescriptorBuilder` tries to build its implementation bodies. +4. Extracting external mappings +5. For each mapping in the queue the `DescriptorBuilder` tries to build its implementation bodies. This is done by a so called `*MappingBodyBuilder`. A mapping body builder tries to map each property from the source to the target. To do this, it asks the `DescriptorBuilder` to create mappings for the according types. @@ -36,7 +42,7 @@ The `DescriptorBuilder` does this by following this process: Each of the mapping builders try to create a mapping (an `ITypeMapping` implementation) for the asked type mapping by using one approach on how to map types (eg. an explicit cast is implemented by the `ExplicitCastMappingBuilder`). These mappings are queued in the queue of mappings which need the body to be built (currently body builders are only used for object to object (property-based) mappings). -5. The `SourceEmitter` emits the code described by the `MapperDescriptor` and all its mappings. +6. The `SourceEmitter` emits the code described by the `MapperDescriptor` and all its mappings. The syntax objects are created by using `SyntaxFactory` and `SyntaxFactoryHelper`. The `SyntaxFactoryHelper` tries to simplify creating formatted syntax trees. If indentation is needed, diff --git a/docs/docs/contributing/common-tasks.md b/docs/docs/contributing/common-tasks.md index 9967dc4450..1da4bad0d3 100644 --- a/docs/docs/contributing/common-tasks.md +++ b/docs/docs/contributing/common-tasks.md @@ -50,3 +50,4 @@ To support a new roslyn version via multi targeting follow these steps (see also Mapperly Mappings use Roslyn syntax trees. [RoslynQuoter](https://roslynquoter.azurewebsites.net/) and [SharpLab](https://sharplab.io/) are fantastic tools to understand and work with Roslyn syntax trees. +The `Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper` and `Microsoft.CodeAnalysis.CSharp.SyntaxFactory` classes help building these syntax trees. diff --git a/docs/docs/getting-started/installation.mdx b/docs/docs/getting-started/installation.mdx index eedfcf3590..593a611dd6 100644 --- a/docs/docs/getting-started/installation.mdx +++ b/docs/docs/getting-started/installation.mdx @@ -12,13 +12,13 @@ import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; ## Add the NuGet Package to your project -All you need to do, to install Mapperly is to add a NuGet reference pointing to the package `Riok.Mapperly`. +All you need to do, to install Mapperly is to add a NuGet reference pointing to the package [`Riok.Mapperly`](https://www.nuget.org/packages/Riok.Mapperly). - {``} + {``} @@ -26,6 +26,8 @@ All you need to do, to install Mapperly is to add a NuGet reference pointing to dotnet add package Riok.Mapperly ``` +Make sure to set `PrivateAssets="all" ExcludeAssets="runtime"` on the added `PackageReference`. + @@ -33,5 +35,13 @@ dotnet add package Riok.Mapperly Install-Package Riok.Mapperly ``` +Make sure to set `PrivateAssets="all" ExcludeAssets="runtime"` on the added `PackageReference`. + + +:::info +`PrivateAssets="all"` ensures projects referencing this project will not also get a reference to `Riok.Mapperly`. +`ExcludeAssets="runtime"` ensures the Mapperly dll files are not copied to your build output +(they are not required at runtime). +::: diff --git a/docs/docs/intro.md b/docs/docs/intro.md index 102b672cd7..66eeb02125 100644 --- a/docs/docs/intro.md +++ b/docs/docs/intro.md @@ -39,6 +39,7 @@ For example, Mapperly can report a warning when there is an added property in a - Mapperly does not use reflection - Mapperly is trimming and AoT safe - Mapperly runs at build time +- Mapperly does not have a runtime dependency - The generated mappings are amazingly fast with minimal memory overhead - The generated mapping code is readable and debuggable - No need to write and maintain boilerplate code by hand diff --git a/samples/Riok.Mapperly.Sample/Riok.Mapperly.Sample.csproj b/samples/Riok.Mapperly.Sample/Riok.Mapperly.Sample.csproj index b7c0070415..ab5fddb540 100644 --- a/samples/Riok.Mapperly.Sample/Riok.Mapperly.Sample.csproj +++ b/samples/Riok.Mapperly.Sample/Riok.Mapperly.Sample.csproj @@ -2,8 +2,6 @@ Exe - net7.0 - true diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 9b30ad6b32..aa5376caf2 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -4,7 +4,6 @@ netstandard2.0 - embedded true true diff --git a/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt b/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt index 71b8efc981..80b1edf4ec 100644 --- a/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt +++ b/src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt @@ -81,10 +81,6 @@ Riok.Mapperly.Abstractions.MappingConversionType.Tuple = 32768 -> Riok.Mapperly. Riok.Mapperly.Abstractions.MappingConversionType.Queryable = 1024 -> Riok.Mapperly.Abstractions.MappingConversionType Riok.Mapperly.Abstractions.MapperAttribute.UseReferenceHandling.get -> bool Riok.Mapperly.Abstractions.MapperAttribute.UseReferenceHandling.set -> void -Riok.Mapperly.Abstractions.ReferenceHandling.Internal.PreserveReferenceHandler -Riok.Mapperly.Abstractions.ReferenceHandling.Internal.PreserveReferenceHandler.PreserveReferenceHandler() -> void -Riok.Mapperly.Abstractions.ReferenceHandling.Internal.PreserveReferenceHandler.SetReference(TSource source, TTarget target) -> void -Riok.Mapperly.Abstractions.ReferenceHandling.Internal.PreserveReferenceHandler.TryGetReference(TSource source, out TTarget? target) -> bool Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler.SetReference(TSource source, TTarget target) -> void Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler.TryGetReference(TSource source, out TTarget? target) -> bool diff --git a/src/Riok.Mapperly.Abstractions/ReferenceHandling/Internal/PreserveReferenceHandler.cs b/src/Riok.Mapperly.Abstractions/ReferenceHandling/Internal/PreserveReferenceHandler.cs deleted file mode 100644 index efa5159eba..0000000000 --- a/src/Riok.Mapperly.Abstractions/ReferenceHandling/Internal/PreserveReferenceHandler.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System.Diagnostics.CodeAnalysis; - -namespace Riok.Mapperly.Abstractions.ReferenceHandling.Internal; - -/// -/// A implementation -/// which returns the same target object instance if encountered the same source object instance. -/// Do not use directly. Should only be used by Mapperly generated code. -/// API surface is not subject to semantic releases and may break in any release. -/// -public sealed class PreserveReferenceHandler : IReferenceHandler -{ - private readonly Dictionary<(Type, Type), ReferenceHolder> _referenceHolders = new(); - - /// - public bool TryGetReference(TSource source, [NotNullWhen(true)] out TTarget? target) - where TSource : notnull - where TTarget : notnull - { - var refHolder = GetReferenceHolder(); - return refHolder.TryGetRef(source, out target); - } - - /// - public void SetReference(TSource source, TTarget target) - where TSource : notnull - where TTarget : notnull => GetReferenceHolder().SetRef(source, target); - - private ReferenceHolder GetReferenceHolder() - { - var mapping = (typeof(TSource), typeof(TTarget)); - if (_referenceHolders.TryGetValue(mapping, out var refHolder)) - return refHolder; - - return _referenceHolders[mapping] = new(); - } - - private class ReferenceHolder - { - private readonly Dictionary _references = new(ReferenceEqualityComparer.Instance); - - public bool TryGetRef(TSource source, [NotNullWhen(true)] out TTarget? target) - where TSource : notnull - where TTarget : notnull - { - if (_references.TryGetValue(source, out var targetObj)) - { - target = (TTarget)targetObj; - return true; - } - - target = default; - return false; - } - - public void SetRef(TSource source, TTarget target) - where TSource : notnull - where TTarget : notnull - { - _references[source] = target; - } - } -} diff --git a/src/Riok.Mapperly.Abstractions/Riok.Mapperly.Abstractions.csproj b/src/Riok.Mapperly.Abstractions/Riok.Mapperly.Abstractions.csproj index 57f77bad1e..c55ede897d 100644 --- a/src/Riok.Mapperly.Abstractions/Riok.Mapperly.Abstractions.csproj +++ b/src/Riok.Mapperly.Abstractions/Riok.Mapperly.Abstractions.csproj @@ -2,6 +2,7 @@ false true + T:System.Diagnostics.CodeAnalysis.NotNullWhenAttribute diff --git a/src/Riok.Mapperly.Templates/.editorconfig b/src/Riok.Mapperly.Templates/.editorconfig new file mode 100644 index 0000000000..392159068b --- /dev/null +++ b/src/Riok.Mapperly.Templates/.editorconfig @@ -0,0 +1,3 @@ +# keep namespaces block scoped, because .NET Framework language version does not support this +[*.cs] +csharp_style_namespace_declarations = block_scoped:warning diff --git a/src/Riok.Mapperly.Templates/PreserveReferenceHandler.cs b/src/Riok.Mapperly.Templates/PreserveReferenceHandler.cs new file mode 100644 index 0000000000..c7b6d4cf38 --- /dev/null +++ b/src/Riok.Mapperly.Templates/PreserveReferenceHandler.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using Riok.Mapperly.Abstractions.ReferenceHandling; + +#nullable enable + +namespace Riok.Mapperly.Internal.AssemblyName +{ + /// + /// A implementation + /// which returns the same target object instance if encountered the same source object instance. + /// Do not use directly. Should only be used by Mapperly generated code. + /// API surface is not subject to semantic releases and may break in any release. + /// + internal sealed class PreserveReferenceHandler : IReferenceHandler + { + private readonly Dictionary<(Type, Type), ReferenceHolder> _referenceHolders = new(); + + /// + public bool TryGetReference(TSource source, [NotNullWhen(true)] out TTarget? target) + where TSource : notnull + where TTarget : notnull + { + var refHolder = GetReferenceHolder(); + return refHolder.TryGetRef(source, out target); + } + + /// + public void SetReference(TSource source, TTarget target) + where TSource : notnull + where TTarget : notnull => GetReferenceHolder().SetRef(source, target); + + private ReferenceHolder GetReferenceHolder() + { + var mapping = (typeof(TSource), typeof(TTarget)); + if (_referenceHolders.TryGetValue(mapping, out var refHolder)) + return refHolder; + + return _referenceHolders[mapping] = new(); + } + + private sealed class ReferenceEqualityComparer : IEqualityComparer + { + // cannot use System.Collections.Generic.ReferenceEqualityComparer since it is not available in netstandard2.0 + + public static readonly IEqualityComparer Instance = new ReferenceEqualityComparer(); + + private ReferenceEqualityComparer() { } + + bool IEqualityComparer.Equals(T? x, T? y) => ReferenceEquals(x, y); + + int IEqualityComparer.GetHashCode(T obj) => RuntimeHelpers.GetHashCode(obj); + } + + private class ReferenceHolder + { + private readonly Dictionary _references = new(ReferenceEqualityComparer.Instance); + + public bool TryGetRef(TSource source, [NotNullWhen(true)] out TTarget? target) + where TSource : notnull + where TTarget : notnull + { + if (_references.TryGetValue(source, out var targetObj)) + { + target = (TTarget)targetObj; + return true; + } + + target = default; + return false; + } + + public void SetRef(TSource source, TTarget target) + where TSource : notnull + where TTarget : notnull + { + _references[source] = target; + } + } + } +} diff --git a/src/Riok.Mapperly.Templates/Riok.Mapperly.Templates.csproj b/src/Riok.Mapperly.Templates/Riok.Mapperly.Templates.csproj new file mode 100644 index 0000000000..0842cd2ce3 --- /dev/null +++ b/src/Riok.Mapperly.Templates/Riok.Mapperly.Templates.csproj @@ -0,0 +1,36 @@ + + + + netstandard2.0 + + + Riok.Mapperly.Internal.AssemblyName + + + disable + + + disable + + + * + + + + + <_Parameter1>Riok.Mapperly.Templates.Tests + + + + + + + + diff --git a/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs b/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs index 26a506b508..896b1be7ce 100644 --- a/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs +++ b/src/Riok.Mapperly/Descriptors/DescriptorBuilder.cs @@ -4,9 +4,11 @@ using Riok.Mapperly.Descriptors.ExternalMappings; using Riok.Mapperly.Descriptors.MappingBodyBuilders; using Riok.Mapperly.Descriptors.MappingBuilders; +using Riok.Mapperly.Descriptors.Mappings; using Riok.Mapperly.Descriptors.ObjectFactories; using Riok.Mapperly.Helpers; using Riok.Mapperly.Symbols; +using Riok.Mapperly.Templates; namespace Riok.Mapperly.Descriptors; @@ -55,6 +57,7 @@ MapperConfiguration defaultMapperConfiguration ExtractExternalMappings(); _mappingBodyBuilder.BuildMappingBodies(cancellationToken); BuildMappingMethodNames(); + AddRequiredTemplates(); BuildReferenceHandlingParameters(); AddMappingsToDescriptor(); return (_mapperDescriptor, _diagnostics); @@ -117,6 +120,20 @@ private void BuildReferenceHandlingParameters() } } + private void AddRequiredTemplates() + { + // if reference handling is enabled and any user defined method mapping + // does not have a reference handling parameter, + // emit the preserve reference handler template which gets instantiated as reference handler. + if ( + _builderContext.MapperConfiguration.UseReferenceHandling + && _mappings.UserMappings.OfType().Any(x => !x.HasReferenceHandlingParameter()) + ) + { + _mapperDescriptor.AddRequiredTemplate(TemplateReference.PreserveReferenceHandler); + } + } + private void AddMappingsToDescriptor() { // add generated mappings to the mapper diff --git a/src/Riok.Mapperly/Descriptors/MapperDescriptor.cs b/src/Riok.Mapperly/Descriptors/MapperDescriptor.cs index fb90f72da5..f5005297f2 100644 --- a/src/Riok.Mapperly/Descriptors/MapperDescriptor.cs +++ b/src/Riok.Mapperly/Descriptors/MapperDescriptor.cs @@ -4,6 +4,7 @@ using Riok.Mapperly.Descriptors.Mappings; using Riok.Mapperly.Helpers; using Riok.Mapperly.Symbols; +using Riok.Mapperly.Templates; namespace Riok.Mapperly.Descriptors; @@ -11,6 +12,7 @@ public class MapperDescriptor { private readonly MapperDeclaration _declaration; private readonly List _methodMappings = new(); + private readonly HashSet _requiredTemplates = new(); public MapperDescriptor(MapperDeclaration declaration, UniqueNameBuilder nameBuilder) { @@ -34,10 +36,14 @@ public MapperDescriptor(MapperDeclaration declaration, UniqueNameBuilder nameBui public UniqueNameBuilder NameBuilder { get; } + public IReadOnlyCollection RequiredTemplates => _requiredTemplates; + public IReadOnlyCollection MethodTypeMappings => _methodMappings; public void AddTypeMapping(MethodMapping mapping) => _methodMappings.Add(mapping); + public void AddRequiredTemplate(TemplateReference template) => _requiredTemplates.Add(template); + private string BuildName(INamedTypeSymbol symbol) { if (symbol.ContainingType == null) diff --git a/src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs index 48d6f6b680..df6fe36ca0 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/MethodMapping.cs @@ -90,6 +90,8 @@ internal void SetMethodNameIfNeeded(Func methodNameBuilde _methodName ??= methodNameBuilder(this); } + public bool HasReferenceHandlingParameter() => ReferenceHandlerParameter.HasValue; + internal virtual void EnableReferenceHandling(INamedTypeSymbol iReferenceHandlerType) { ReferenceHandlerParameter ??= new MethodParameter( diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExistingTargetMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExistingTargetMethodMapping.cs index 59c32ac45b..5e82670dc9 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExistingTargetMethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedExistingTargetMethodMapping.cs @@ -3,6 +3,7 @@ using Riok.Mapperly.Descriptors.Mappings.ExistingTarget; using Riok.Mapperly.Helpers; using Riok.Mapperly.Symbols; +using Riok.Mapperly.Templates; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper; @@ -13,9 +14,6 @@ namespace Riok.Mapperly.Descriptors.Mappings.UserMappings; /// public class UserDefinedExistingTargetMethodMapping : MethodMapping, IUserMapping { - private const string ReferenceHandlerTypeName = - "global::Riok.Mapperly.Abstractions.ReferenceHandling.Internal.PreserveReferenceHandler"; - private readonly bool _enableReferenceHandling; private IExistingTargetMapping? _delegateMapping; @@ -66,7 +64,7 @@ public override IEnumerable BuildBody(TypeMappingBuildContext c { // var refHandler = new RefHandler(); var referenceHandlerName = ctx.NameBuilder.New(DefaultReferenceHandlerParameterName); - var createRefHandler = CreateInstance(ReferenceHandlerTypeName); + var createRefHandler = ctx.SyntaxFactory.CreateInstance(TemplateReference.PreserveReferenceHandler); yield return ctx.SyntaxFactory.DeclareLocalVariable(referenceHandlerName, createRefHandler); ctx = ctx.WithRefHandler(referenceHandlerName); } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceMethodMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceMethodMapping.cs index 4c4f88e0a4..b2c66b9325 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceMethodMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceMethodMapping.cs @@ -2,7 +2,7 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Riok.Mapperly.Helpers; using Riok.Mapperly.Symbols; -using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper; +using Riok.Mapperly.Templates; namespace Riok.Mapperly.Descriptors.Mappings.UserMappings; @@ -11,9 +11,6 @@ namespace Riok.Mapperly.Descriptors.Mappings.UserMappings; /// public class UserDefinedNewInstanceMethodMapping : MethodMapping, IDelegateUserMapping { - private const string ReferenceHandlerTypeName = - "global::Riok.Mapperly.Abstractions.ReferenceHandling.Internal.PreserveReferenceHandler"; - private readonly bool _enableReferenceHandling; public UserDefinedNewInstanceMethodMapping( @@ -47,7 +44,7 @@ public override IEnumerable BuildBody(TypeMappingBuildContext c if (_enableReferenceHandling && ReferenceHandlerParameter == null) { // new RefHandler(); - var createRefHandler = CreateInstance(ReferenceHandlerTypeName); + var createRefHandler = ctx.SyntaxFactory.CreateInstance(TemplateReference.PreserveReferenceHandler); ctx = ctx.WithRefHandler(createRefHandler); return new[] { ctx.SyntaxFactory.Return(DelegateMapping.Build(ctx)) }; } diff --git a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceRuntimeTargetTypeMapping.cs b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceRuntimeTargetTypeMapping.cs index ba127b314c..533213559c 100644 --- a/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceRuntimeTargetTypeMapping.cs +++ b/src/Riok.Mapperly/Descriptors/Mappings/UserMappings/UserDefinedNewInstanceRuntimeTargetTypeMapping.cs @@ -3,6 +3,7 @@ using Riok.Mapperly.Emit.Syntax; using Riok.Mapperly.Helpers; using Riok.Mapperly.Symbols; +using Riok.Mapperly.Templates; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; using static Riok.Mapperly.Emit.Syntax.SyntaxFactoryHelper; @@ -16,8 +17,6 @@ public abstract class UserDefinedNewInstanceRuntimeTargetTypeMapping : MethodMap { private const string IsAssignableFromMethodName = nameof(Type.IsAssignableFrom); private const string GetTypeMethodName = nameof(GetType); - private const string ReferenceHandlerTypeName = - "global::Riok.Mapperly.Abstractions.ReferenceHandling.Internal.PreserveReferenceHandler"; private readonly List _mappings = new(); private readonly bool _enableReferenceHandling; @@ -61,7 +60,7 @@ public override IEnumerable BuildBody(TypeMappingBuildContext c { // var refHandler = new RefHandler(); var referenceHandlerName = ctx.NameBuilder.New(DefaultReferenceHandlerParameterName); - var createRefHandler = CreateInstance(ReferenceHandlerTypeName); + var createRefHandler = ctx.SyntaxFactory.CreateInstance(TemplateReference.PreserveReferenceHandler); yield return ctx.SyntaxFactory.DeclareLocalVariable(referenceHandlerName, createRefHandler); ctx = ctx.WithRefHandler(referenceHandlerName); diff --git a/src/Riok.Mapperly/Emit/SourceEmitter.cs b/src/Riok.Mapperly/Emit/SourceEmitter.cs index a6d99f1a03..374333aa5b 100644 --- a/src/Riok.Mapperly/Emit/SourceEmitter.cs +++ b/src/Riok.Mapperly/Emit/SourceEmitter.cs @@ -13,7 +13,11 @@ public static class SourceEmitter public static CompilationUnitSyntax Build(MapperDescriptor descriptor, CancellationToken cancellationToken) { - var ctx = new SourceEmitterContext(descriptor.Symbol.IsStatic, descriptor.NameBuilder, new SyntaxFactoryHelper()); + var ctx = new SourceEmitterContext( + descriptor.Symbol.IsStatic, + descriptor.NameBuilder, + new SyntaxFactoryHelper(descriptor.Symbol.ContainingAssembly.Name) + ); ctx = IndentForMapper(ctx, descriptor.Symbol); var memberCtx = ctx.AddIndentation(); diff --git a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.New.cs b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.New.cs index 65f8c40b54..96dae6b696 100644 --- a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.New.cs +++ b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.New.cs @@ -1,14 +1,16 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Riok.Mapperly.Templates; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; namespace Riok.Mapperly.Emit.Syntax; public partial struct SyntaxFactoryHelper { - public static ObjectCreationExpressionSyntax CreateInstance(string typeName) + public ObjectCreationExpressionSyntax CreateInstance(TemplateReference template) { + var typeName = TemplateReader.GetTypeName(template, _assemblyName); var type = IdentifierName(typeName); return CreateObject(type, SyntaxFactory.ArgumentList()); } diff --git a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.cs b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.cs index aabb9ccec9..34eb862c8b 100644 --- a/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.cs +++ b/src/Riok.Mapperly/Emit/Syntax/SyntaxFactoryHelper.cs @@ -10,16 +10,24 @@ public readonly partial struct SyntaxFactoryHelper { public static readonly IdentifierNameSyntax VarIdentifier = IdentifierName("var").AddTrailingSpace(); - private SyntaxFactoryHelper(int indentation) + private readonly string _assemblyName; + + public SyntaxFactoryHelper(string assemblyName) + { + _assemblyName = assemblyName; + } + + private SyntaxFactoryHelper(int indentation, string assemblyName) { Indentation = indentation; + _assemblyName = assemblyName; } public int Indentation { get; } - public SyntaxFactoryHelper AddIndentation() => new(Indentation + 1); + public SyntaxFactoryHelper AddIndentation() => new(Indentation + 1, _assemblyName); - public SyntaxFactoryHelper RemoveIndentation() => new(Indentation - 1); + public SyntaxFactoryHelper RemoveIndentation() => new(Indentation - 1, _assemblyName); public static SyntaxToken Accessibility(Accessibility accessibility) { diff --git a/src/Riok.Mapperly/Helpers/FileNameBuilder.cs b/src/Riok.Mapperly/Helpers/FileNameBuilder.cs index 14e500b6a7..6843d3e580 100644 --- a/src/Riok.Mapperly/Helpers/FileNameBuilder.cs +++ b/src/Riok.Mapperly/Helpers/FileNameBuilder.cs @@ -1,12 +1,17 @@ using Riok.Mapperly.Descriptors; +using Riok.Mapperly.Templates; namespace Riok.Mapperly.Helpers; public class FileNameBuilder { private const string GeneratedFileSuffix = ".g.cs"; + private const string TemplatesGeneratedFileNamePrefix = "MapperlyInternal."; private readonly UniqueNameBuilder _uniqueNameBuilder = new(); - public string Build(MapperDescriptor mapper) => _uniqueNameBuilder.New(mapper.Name) + GeneratedFileSuffix; + internal string Build(MapperDescriptor mapper) => _uniqueNameBuilder.New(mapper.Name) + GeneratedFileSuffix; + + internal static string BuildForTemplate(TemplateReference reference) => + TemplatesGeneratedFileNamePrefix + reference + GeneratedFileSuffix; } diff --git a/src/Riok.Mapperly/Helpers/ImmutableEquatableArray.cs b/src/Riok.Mapperly/Helpers/ImmutableEquatableArray.cs index bdc0d01c44..596fb13e79 100644 --- a/src/Riok.Mapperly/Helpers/ImmutableEquatableArray.cs +++ b/src/Riok.Mapperly/Helpers/ImmutableEquatableArray.cs @@ -11,8 +11,6 @@ namespace Riok.Mapperly.Helpers; public sealed class ImmutableEquatableArray : IEquatable>, IReadOnlyList where T : IEquatable { - public static ImmutableEquatableArray Empty { get; } = new(Array.Empty()); - private readonly T[] _values; public T this[int index] => _values[index]; public int Count => _values.Length; @@ -68,12 +66,6 @@ public bool MoveNext() public static class ImmutableEquatableArray { - public static ImmutableEquatableArray Empty() - where T : IEquatable => ImmutableEquatableArray.Empty; - public static ImmutableEquatableArray ToImmutableEquatableArray(this IEnumerable values) where T : IEquatable => new(values); - - public static ImmutableEquatableArray Create(params T[] values) - where T : IEquatable => values is { Length: > 0 } ? new(values) : ImmutableEquatableArray.Empty; } diff --git a/src/Riok.Mapperly/Helpers/IncrementalValuesProviderExtensions.cs b/src/Riok.Mapperly/Helpers/IncrementalValuesProviderExtensions.cs index 7a5a3196e2..332e5a594b 100644 --- a/src/Riok.Mapperly/Helpers/IncrementalValuesProviderExtensions.cs +++ b/src/Riok.Mapperly/Helpers/IncrementalValuesProviderExtensions.cs @@ -2,6 +2,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Text; using Riok.Mapperly.Output; +using Riok.Mapperly.Templates; namespace Riok.Mapperly.Helpers; @@ -70,6 +71,17 @@ IncrementalValuesProvider mappers ); } + public static void EmitTemplates( + this IncrementalGeneratorInitializationContext context, + IncrementalValuesProvider templates + ) + { + context.RegisterImplementationSourceOutput( + templates, + static (spc, template) => spc.AddSource(template.FileName, SourceText.From(template.Content, Encoding.UTF8)) + ); + } + #if !ROSLYN4_4_OR_GREATER public static IncrementalValuesProvider WhereNotNull(this IncrementalValuesProvider source) { diff --git a/src/Riok.Mapperly/MapperGenerator.cs b/src/Riok.Mapperly/MapperGenerator.cs index e7c2d5e2dc..c5f1754ee5 100644 --- a/src/Riok.Mapperly/MapperGenerator.cs +++ b/src/Riok.Mapperly/MapperGenerator.cs @@ -8,6 +8,7 @@ using Riok.Mapperly.Helpers; using Riok.Mapperly.Output; using Riok.Mapperly.Symbols; +using Riok.Mapperly.Templates; namespace Riok.Mapperly; @@ -22,6 +23,8 @@ public void Initialize(IncrementalGeneratorInitializationContext context) #if DEBUG_SOURCE_GENERATOR DebuggerUtil.AttachDebugger(); #endif + var assemblyName = context.CompilationProvider.Select((x, _) => x.Assembly.Name); + // report compilation diagnostics var compilationDiagnostics = context.CompilationProvider.SelectMany( static (compilation, _) => BuildCompilationDiagnostics(compilation) @@ -57,6 +60,17 @@ public void Initialize(IncrementalGeneratorInitializationContext context) // output the mappers var mappers = mappersAndDiagnostics.Select(static (x, _) => x.Mapper).WithTrackingName(MapperGeneratorStepNames.BuildMappers); context.EmitMapperSource(mappers); + + // output the templates + var templates = mappersAndDiagnostics + .SelectMany(static (x, _) => x.Templates) + .Collect() + .SelectMany(static (x, _) => x.DistinctBy(tm => tm)) + .Combine(assemblyName) + .WithTrackingName(MapperGeneratorStepNames.BuildTemplates) + .Select(static (x, _) => TemplateReader.ReadContent(x.Left, x.Right)) + .WithTrackingName(MapperGeneratorStepNames.BuildTemplatesContent); + context.EmitTemplates(templates); } private static MapperAndDiagnostics? BuildDescriptor( @@ -82,7 +96,7 @@ CancellationToken cancellationToken compilationContext.FileNameBuilder.Build(descriptor), SourceEmitter.Build(descriptor, cancellationToken) ); - return new MapperAndDiagnostics(mapper, descriptorDiagnostics.ToImmutableEquatableArray()); + return new MapperAndDiagnostics(mapper, descriptorDiagnostics.ToImmutableEquatableArray(), descriptor.RequiredTemplates); } catch (OperationCanceledException) { diff --git a/src/Riok.Mapperly/MapperGeneratorStepNames.cs b/src/Riok.Mapperly/MapperGeneratorStepNames.cs index 2cb986a932..d625898d8b 100644 --- a/src/Riok.Mapperly/MapperGeneratorStepNames.cs +++ b/src/Riok.Mapperly/MapperGeneratorStepNames.cs @@ -6,5 +6,6 @@ public static class MapperGeneratorStepNames public const string BuildMapperDefaults = nameof(BuildMapperDefaults); public const string ReportDiagnostics = nameof(ReportDiagnostics); public const string BuildMappers = nameof(BuildMappers); - public const string ImplementationSourceOutput = nameof(ImplementationSourceOutput); + public const string BuildTemplates = nameof(BuildTemplates); + public const string BuildTemplatesContent = nameof(BuildTemplatesContent); } diff --git a/src/Riok.Mapperly/Output/MapperAndDiagnostics.cs b/src/Riok.Mapperly/Output/MapperAndDiagnostics.cs index fed10768d1..0a09ad3dfd 100644 --- a/src/Riok.Mapperly/Output/MapperAndDiagnostics.cs +++ b/src/Riok.Mapperly/Output/MapperAndDiagnostics.cs @@ -1,14 +1,19 @@ using Microsoft.CodeAnalysis; using Riok.Mapperly.Helpers; +using Riok.Mapperly.Templates; namespace Riok.Mapperly.Output; -public readonly record struct MapperAndDiagnostics(MapperNode Mapper, ImmutableEquatableArray Diagnostics) +public readonly record struct MapperAndDiagnostics( + MapperNode Mapper, + ImmutableEquatableArray Diagnostics, + IReadOnlyCollection Templates +) { public bool Equals(MapperAndDiagnostics other) { - return Mapper.Equals(other.Mapper) && Diagnostics.Equals(other.Diagnostics); + return Mapper.Equals(other.Mapper) && Diagnostics.Equals(other.Diagnostics) && Templates.SequenceEqual(other.Templates); } - public override int GetHashCode() => HashCode.Combine(Mapper.GetHashCode(), Diagnostics.GetHashCode()); + public override int GetHashCode() => HashCode.Combine(Mapper.GetHashCode(), Diagnostics.GetHashCode(), Templates.Count); } diff --git a/src/Riok.Mapperly/Riok.Mapperly.csproj b/src/Riok.Mapperly/Riok.Mapperly.csproj index 33081b7f5d..d00626ba96 100644 --- a/src/Riok.Mapperly/Riok.Mapperly.csproj +++ b/src/Riok.Mapperly/Riok.Mapperly.csproj @@ -33,19 +33,25 @@ - + all analyzers - + + + + + disable - - - 9.0 + 9.0 + + + + + diff --git a/test/Riok.Mapperly.IntegrationTests/_snapshots/CircularReferenceMapperTest.SnapshotGeneratedSource.verified.cs b/test/Riok.Mapperly.IntegrationTests/_snapshots/CircularReferenceMapperTest.SnapshotGeneratedSource.verified.cs index 72f0deda4c..203e6f9afa 100644 --- a/test/Riok.Mapperly.IntegrationTests/_snapshots/CircularReferenceMapperTest.SnapshotGeneratedSource.verified.cs +++ b/test/Riok.Mapperly.IntegrationTests/_snapshots/CircularReferenceMapperTest.SnapshotGeneratedSource.verified.cs @@ -6,7 +6,7 @@ public static partial class CircularReferenceMapper { public static partial global::Riok.Mapperly.IntegrationTests.Dto.CircularReferenceDto ToDto(global::Riok.Mapperly.IntegrationTests.Models.CircularReferenceObject obj) { - return MapToCircularReferenceDto(obj, new global::Riok.Mapperly.Abstractions.ReferenceHandling.Internal.PreserveReferenceHandler()); + return MapToCircularReferenceDto(obj, new global::Riok.Mapperly.Internal.Riok.Mapperly.IntegrationTests.PreserveReferenceHandler()); } private static global::Riok.Mapperly.IntegrationTests.Dto.CircularReferenceDto MapToCircularReferenceDto(global::Riok.Mapperly.IntegrationTests.Models.CircularReferenceObject source, global::Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler) diff --git a/test/Riok.Mapperly.Abstractions.Tests/ReferenceHandling/Internal/PreserveReferenceHandlerTest.cs b/test/Riok.Mapperly.Templates.Tests/PreserveReferenceHandlerTest.cs similarity index 85% rename from test/Riok.Mapperly.Abstractions.Tests/ReferenceHandling/Internal/PreserveReferenceHandlerTest.cs rename to test/Riok.Mapperly.Templates.Tests/PreserveReferenceHandlerTest.cs index 3d67b15a41..9ff7055b79 100644 --- a/test/Riok.Mapperly.Abstractions.Tests/ReferenceHandling/Internal/PreserveReferenceHandlerTest.cs +++ b/test/Riok.Mapperly.Templates.Tests/PreserveReferenceHandlerTest.cs @@ -1,7 +1,7 @@ using Riok.Mapperly.Abstractions.ReferenceHandling; -using Riok.Mapperly.Abstractions.ReferenceHandling.Internal; +using Riok.Mapperly.Internal.AssemblyName; -namespace Riok.Mapperly.Abstractions.Tests.ReferenceHandling.Internal; +namespace Riok.Mapperly.Templates.Tests; public class PreserveReferenceHandlerTest { diff --git a/test/Riok.Mapperly.Templates.Tests/Riok.Mapperly.Templates.Tests.csproj b/test/Riok.Mapperly.Templates.Tests/Riok.Mapperly.Templates.Tests.csproj new file mode 100644 index 0000000000..147f28f4f2 --- /dev/null +++ b/test/Riok.Mapperly.Templates.Tests/Riok.Mapperly.Templates.Tests.csproj @@ -0,0 +1,7 @@ + + + + + + + diff --git a/test/Riok.Mapperly.Tests/Generator/IncrementalGeneratorRunReasons.cs b/test/Riok.Mapperly.Tests/Generator/IncrementalGeneratorRunReasons.cs new file mode 100644 index 0000000000..dd6508651e --- /dev/null +++ b/test/Riok.Mapperly.Tests/Generator/IncrementalGeneratorRunReasons.cs @@ -0,0 +1,53 @@ +using Microsoft.CodeAnalysis; + +namespace Riok.Mapperly.Tests.Generator; + +internal record IncrementalGeneratorRunReasons( + IncrementalStepRunReason CompilationStep, + IncrementalStepRunReason BuildMapperDefaultsStep, + IncrementalStepRunReason BuildMappersStep, + IncrementalStepRunReason ReportDiagnosticsStep +) +{ + public static readonly IncrementalGeneratorRunReasons New = + new(IncrementalStepRunReason.New, IncrementalStepRunReason.New, IncrementalStepRunReason.New, IncrementalStepRunReason.New); + + public static readonly IncrementalGeneratorRunReasons Cached = + new( + // compilation step should always be modified as each time a new compilation is passed + IncrementalStepRunReason.Modified, + IncrementalStepRunReason.Unchanged, + IncrementalStepRunReason.Cached, + IncrementalStepRunReason.Cached + ); + + public static readonly IncrementalGeneratorRunReasons Modified = Cached with + { + BuildMapperDefaultsStep = IncrementalStepRunReason.Modified, + ReportDiagnosticsStep = IncrementalStepRunReason.Modified, + BuildMappersStep = IncrementalStepRunReason.Modified, + }; + + public static readonly IncrementalGeneratorRunReasons ModifiedDiagnostics = Cached with + { + BuildMappersStep = IncrementalStepRunReason.Unchanged, + ReportDiagnosticsStep = IncrementalStepRunReason.Modified, + }; + + public static readonly IncrementalGeneratorRunReasons ModifiedSource = Cached with + { + ReportDiagnosticsStep = IncrementalStepRunReason.Unchanged, + BuildMappersStep = IncrementalStepRunReason.Modified, + }; + + public static readonly IncrementalGeneratorRunReasons ModifiedSourceAndDiagnostics = Cached with + { + ReportDiagnosticsStep = IncrementalStepRunReason.Modified, + BuildMappersStep = IncrementalStepRunReason.Modified, + }; + + public static readonly IncrementalGeneratorRunReasons ModifiedDefaults = Cached with + { + BuildMapperDefaultsStep = IncrementalStepRunReason.Modified, + }; +} diff --git a/test/Riok.Mapperly.Tests/Generator/IncrementalGeneratorTemplatesTest.cs b/test/Riok.Mapperly.Tests/Generator/IncrementalGeneratorTemplatesTest.cs new file mode 100644 index 0000000000..cd61e9bc94 --- /dev/null +++ b/test/Riok.Mapperly.Tests/Generator/IncrementalGeneratorTemplatesTest.cs @@ -0,0 +1,30 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using static Riok.Mapperly.Tests.Generator.IncrementalGeneratorTestHelper; + +namespace Riok.Mapperly.Tests.Generator; + +public class IncrementalGeneratorTemplatesTest +{ + [Fact] + public void ModifiedMapperShouldCacheTemplate() + { + var source = TestSourceBuilder.Mapping( + "A", + "B", + TestSourceBuilderOptions.WithReferenceHandling, + "record A(int Value);", + "record B(int Value);" + ); + var syntaxTree = CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default); + var compilation1 = TestHelper.BuildCompilation(syntaxTree); + var driver1 = TestHelper.GenerateTracked(compilation1); + AssertRunReason(driver1, MapperGeneratorStepNames.BuildTemplates, IncrementalStepRunReason.New); + AssertRunReason(driver1, MapperGeneratorStepNames.BuildTemplatesContent, IncrementalStepRunReason.New); + + var compilation2 = ReplaceRecord(compilation1, "A", "record A(string Value);"); + var driver2 = driver1.RunGenerators(compilation2); + AssertRunReason(driver2, MapperGeneratorStepNames.BuildTemplates, IncrementalStepRunReason.Cached); + AssertRunReason(driver2, MapperGeneratorStepNames.BuildTemplatesContent, IncrementalStepRunReason.Cached); + } +} diff --git a/test/Riok.Mapperly.Tests/Generator/IncrementalGeneratorTest.cs b/test/Riok.Mapperly.Tests/Generator/IncrementalGeneratorTest.cs index 5949ff57e7..4e940f74a1 100644 --- a/test/Riok.Mapperly.Tests/Generator/IncrementalGeneratorTest.cs +++ b/test/Riok.Mapperly.Tests/Generator/IncrementalGeneratorTest.cs @@ -2,6 +2,7 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using static Riok.Mapperly.Tests.Generator.IncrementalGeneratorTestHelper; namespace Riok.Mapperly.Tests.Generator; @@ -16,11 +17,11 @@ public void AddingUnrelatedTypeDoesNotRegenerateOriginal() var compilation1 = TestHelper.BuildCompilation(syntaxTree); var driver1 = TestHelper.GenerateTracked(compilation1); - AssertRunReasons(driver1, RunReasons.New); + AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New); var compilation2 = compilation1.AddSyntaxTrees(TestSourceBuilder.SyntaxTree("struct MyValue {}")); var driver2 = driver1.RunGenerators(compilation2); - AssertRunReasons(driver2, RunReasons.Cached); + AssertRunReasons(driver2, IncrementalGeneratorRunReasons.Cached); } [Fact] @@ -54,8 +55,8 @@ internal partial class BarFooMapper var compilation2 = compilation1.AddSyntaxTrees(secondary); var driver2 = driver1.RunGenerators(compilation2); - AssertRunReasons(driver2, RunReasons.Cached, 0); - AssertRunReasons(driver2, RunReasons.New, 1); + AssertRunReasons(driver2, IncrementalGeneratorRunReasons.Cached, 0); + AssertRunReasons(driver2, IncrementalGeneratorRunReasons.New, 1); } [Fact] @@ -74,31 +75,22 @@ public void AppendingUnrelatedTypeDoesNotRegenerateOriginal() var compilation2 = compilation1.ReplaceSyntaxTree(compilation1.SyntaxTrees.First(), newTree); var driver2 = driver1.RunGenerators(compilation2); - AssertRunReasons(driver2, RunReasons.Cached); + AssertRunReasons(driver2, IncrementalGeneratorRunReasons.Cached); } [Fact] public void ModifyingMappedTypeDoesRegenerateOriginal() { - var source = TestSourceBuilder.MapperWithBodyAndTypes("partial B Map(A source);", "record A(int Value);", "record B(int Value);"); + var source = TestSourceBuilder.Mapping("A", "B", "record A(int Value);", "record B(int Value);"); var syntaxTree = CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default); var compilation1 = TestHelper.BuildCompilation(syntaxTree); var driver1 = TestHelper.GenerateTracked(compilation1); - AssertRunReasons(driver1, RunReasons.New); + AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New); - var recordDeclaration = syntaxTree - .GetCompilationUnitRoot() - .Members.OfType() - .Single(x => x.Identifier.Text == "A"); - var updatedRecordDeclaration = ParseMemberDeclaration("record A(string Value)")!; - - var newRoot = syntaxTree.GetCompilationUnitRoot().ReplaceNode(recordDeclaration, updatedRecordDeclaration); - var newTree = syntaxTree.WithRootAndOptions(newRoot, syntaxTree.Options); - - var compilation2 = compilation1.ReplaceSyntaxTree(compilation1.SyntaxTrees.First(), newTree); + var compilation2 = ReplaceRecord(compilation1, "A", "record A(string Value)"); var driver2 = driver1.RunGenerators(compilation2); - AssertRunReasons(driver2, RunReasons.ModifiedSource); + AssertRunReasons(driver2, IncrementalGeneratorRunReasons.ModifiedSource); } [Fact] @@ -113,7 +105,7 @@ public void ModifyingMapperDoesRegenerateOriginal() var compilation1 = TestHelper.BuildCompilation(syntaxTree); var driver1 = TestHelper.GenerateTracked(compilation1); - AssertRunReasons(driver1, RunReasons.New); + AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New); var classDeclaration = syntaxTree .GetCompilationUnitRoot() @@ -127,7 +119,7 @@ public void ModifyingMapperDoesRegenerateOriginal() var compilation2 = compilation1.ReplaceSyntaxTree(compilation1.SyntaxTrees.First(), newTree); var driver2 = driver1.RunGenerators(compilation2); - AssertRunReasons(driver2, RunReasons.ModifiedSourceAndDiagnostics); + AssertRunReasons(driver2, IncrementalGeneratorRunReasons.ModifiedSourceAndDiagnostics); } [Fact] @@ -155,7 +147,7 @@ public void ModifyingMapperDiagnosticsOnlyDoesRegenerateDiagnosticsOnly() var compilation2 = compilation1.ReplaceSyntaxTree(compilation1.SyntaxTrees.First(), newTree); var driver2 = driver1.RunGenerators(compilation2); - AssertRunReasons(driver2, RunReasons.ModifiedDiagnostics); + AssertRunReasons(driver2, IncrementalGeneratorRunReasons.ModifiedDiagnostics); } [Fact] @@ -190,7 +182,7 @@ enum E2 { Value } var compilation1 = TestHelper.BuildCompilation(syntaxTree1); var driver1 = TestHelper.GenerateTracked(compilation1); - AssertRunReasons(driver1, RunReasons.New); + AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New); var compilationUnit1 = (CompilationUnitSyntax)syntaxTree1.GetRoot(); var attribute = Attribute( @@ -202,95 +194,7 @@ enum E2 { Value } var syntaxTree2 = syntaxTree1.WithRootAndOptions(compilationUnit2, syntaxTree1.Options); var compilation2 = compilation1.ReplaceSyntaxTree(syntaxTree1, syntaxTree2); var driver2 = driver1.RunGenerators(compilation2); - AssertRunReasons(driver2, RunReasons.Modified, 0); - AssertRunReasons(driver2, RunReasons.ModifiedDefaults, 1); - } - - private static void AssertRunReasons(GeneratorDriver driver, RunReasons reasons, int mapperIndex = 0) - { - var runResult = driver.GetRunResult().Results[0]; - if (mapperIndex == 0) - { - // compilation and defaults are built access all mappers and not per mapper, - // only assert for the first mapper - AssertRunReason(runResult, MapperGeneratorStepNames.BuildCompilationContext, mapperIndex, reasons.CompilationStep); - AssertRunReason(runResult, MapperGeneratorStepNames.BuildMapperDefaults, mapperIndex, reasons.BuildMapperDefaultsStep); - } - - AssertRunReason(runResult, MapperGeneratorStepNames.ReportDiagnostics, mapperIndex, reasons.ReportDiagnosticsStep); - AssertRunReason(runResult, MapperGeneratorStepNames.BuildMappers, mapperIndex, reasons.BuildMappersStep); - AssertRunReason(runResult, MapperGeneratorStepNames.ImplementationSourceOutput, mapperIndex, reasons.SourceOutputStep); - } - - private static void AssertRunReason( - GeneratorRunResult runResult, - string stepName, - int outputIndex, - IncrementalStepRunReason expectedStepReason - ) - { - var actualStepReason = runResult.TrackedSteps[stepName].SelectMany(x => x.Outputs).ElementAt(outputIndex).Reason; - actualStepReason.Should().Be(expectedStepReason, $"step {stepName} of mapper at index {outputIndex}"); - } - - private record RunReasons( - IncrementalStepRunReason CompilationStep, - IncrementalStepRunReason BuildMapperDefaultsStep, - IncrementalStepRunReason BuildMappersStep, - IncrementalStepRunReason ReportDiagnosticsStep, - IncrementalStepRunReason SourceOutputStep - ) - { - public static readonly RunReasons New = - new( - IncrementalStepRunReason.New, - IncrementalStepRunReason.New, - IncrementalStepRunReason.New, - IncrementalStepRunReason.New, - IncrementalStepRunReason.New - ); - - public static readonly RunReasons Cached = - new( - // compilation step should always be modified as each time a new compilation is passed - IncrementalStepRunReason.Modified, - IncrementalStepRunReason.Unchanged, - IncrementalStepRunReason.Cached, - IncrementalStepRunReason.Cached, - IncrementalStepRunReason.Cached - ); - - public static readonly RunReasons Modified = Cached with - { - BuildMapperDefaultsStep = IncrementalStepRunReason.Modified, - ReportDiagnosticsStep = IncrementalStepRunReason.Modified, - BuildMappersStep = IncrementalStepRunReason.Modified, - // the input changed therefore new instead of modified - SourceOutputStep = IncrementalStepRunReason.New, - }; - - public static readonly RunReasons ModifiedDiagnostics = Cached with - { - BuildMappersStep = IncrementalStepRunReason.Unchanged, - ReportDiagnosticsStep = IncrementalStepRunReason.Modified, - }; - - public static readonly RunReasons ModifiedSource = Cached with - { - ReportDiagnosticsStep = IncrementalStepRunReason.Unchanged, - BuildMappersStep = IncrementalStepRunReason.Modified, - // the input changed therefore new instead of modified - SourceOutputStep = IncrementalStepRunReason.New, - }; - - public static readonly RunReasons ModifiedSourceAndDiagnostics = Cached with - { - ReportDiagnosticsStep = IncrementalStepRunReason.Modified, - BuildMappersStep = IncrementalStepRunReason.Modified, - // the input changed therefore new instead of modified - SourceOutputStep = IncrementalStepRunReason.New, - }; - - public static readonly RunReasons ModifiedDefaults = Cached with { BuildMapperDefaultsStep = IncrementalStepRunReason.Modified, }; + AssertRunReasons(driver2, IncrementalGeneratorRunReasons.Modified, 0); + AssertRunReasons(driver2, IncrementalGeneratorRunReasons.ModifiedDefaults, 1); } } diff --git a/test/Riok.Mapperly.Tests/Generator/IncrementalGeneratorTestHelper.cs b/test/Riok.Mapperly.Tests/Generator/IncrementalGeneratorTestHelper.cs new file mode 100644 index 0000000000..d3f179a59e --- /dev/null +++ b/test/Riok.Mapperly.Tests/Generator/IncrementalGeneratorTestHelper.cs @@ -0,0 +1,65 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Riok.Mapperly.Tests.Generator; + +internal static class IncrementalGeneratorTestHelper +{ + public static CSharpCompilation ReplaceRecord( + CSharpCompilation compilation, + string recordName, + [StringSyntax(StringSyntax.CSharp)] string newRecord + ) + { + var syntaxTree = compilation.SyntaxTrees.Single(); + var recordDeclaration = syntaxTree + .GetCompilationUnitRoot() + .Members.OfType() + .Single(x => x.Identifier.Text == recordName); + var updatedRecordDeclaration = SyntaxFactory.ParseMemberDeclaration(newRecord)!; + + var newRoot = syntaxTree.GetCompilationUnitRoot().ReplaceNode(recordDeclaration, updatedRecordDeclaration); + var newTree = syntaxTree.WithRootAndOptions(newRoot, syntaxTree.Options); + + return compilation.ReplaceSyntaxTree(compilation.SyntaxTrees.First(), newTree); + } + + public static void AssertRunReasons(GeneratorDriver driver, IncrementalGeneratorRunReasons reasons, int mapperIndex = 0) + { + var runResult = driver.GetRunResult().Results[0]; + if (mapperIndex == 0) + { + // compilation and defaults are built access all mappers and not per mapper, + // only assert for the first mapper + AssertRunReason(runResult, MapperGeneratorStepNames.BuildCompilationContext, reasons.CompilationStep, mapperIndex); + AssertRunReason(runResult, MapperGeneratorStepNames.BuildMapperDefaults, reasons.BuildMapperDefaultsStep, mapperIndex); + } + + AssertRunReason(runResult, MapperGeneratorStepNames.ReportDiagnostics, reasons.ReportDiagnosticsStep, mapperIndex); + AssertRunReason(runResult, MapperGeneratorStepNames.BuildMappers, reasons.BuildMappersStep, mapperIndex); + } + + public static void AssertRunReason( + GeneratorDriver driver, + string stepName, + IncrementalStepRunReason expectedStepReason, + int outputIndex = 0 + ) + { + var runResult = driver.GetRunResult().Results[0]; + AssertRunReason(runResult, stepName, expectedStepReason, outputIndex); + } + + private static void AssertRunReason( + GeneratorRunResult runResult, + string stepName, + IncrementalStepRunReason expectedStepReason, + int outputIndex + ) + { + var actualStepReason = runResult.TrackedSteps[stepName].SelectMany(x => x.Outputs).ElementAt(outputIndex).Reason; + actualStepReason.Should().Be(expectedStepReason, $"step {stepName} of mapper at index {outputIndex}"); + } +} diff --git a/test/Riok.Mapperly.Tests/Mapping/GenericTest.cs b/test/Riok.Mapperly.Tests/Mapping/GenericTest.cs index 1ad223d42e..c35213c8bb 100644 --- a/test/Riok.Mapperly.Tests/Mapping/GenericTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/GenericTest.cs @@ -456,8 +456,8 @@ public Task WithGenericSourceAndTargetAndEnabledReferenceHandlingAndParameter() """ partial TTarget Map(TSource source, [ReferenceHandler] IReferenceHandler refHandler); - partial B MapToB(A source); - partial D MapToD(C source); + partial B MapToB(A source, [ReferenceHandler] IReferenceHandler refHandler); + partial D MapToD(C source, [ReferenceHandler] IReferenceHandler refHandler); """, TestSourceBuilderOptions.WithReferenceHandling, "record struct A(string Value);", diff --git a/test/Riok.Mapperly.Tests/Mapping/ReferenceHandlingTest.cs b/test/Riok.Mapperly.Tests/Mapping/ReferenceHandlingTest.cs index 42a4712291..1a899091d2 100644 --- a/test/Riok.Mapperly.Tests/Mapping/ReferenceHandlingTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/ReferenceHandlingTest.cs @@ -253,7 +253,7 @@ public Task CustomHandlerAtIndex1WithExistingInstanceShouldWork() public Task UserImplementedWithoutReferenceHandlerShouldWork() { var source = TestSourceBuilder.MapperWithBodyAndTypes( - "partial B Map(A a);" + "string ToStringMod(string s) => s + \"-modified\";", + """partial B Map(A a); string ToStringMod(string s) => s + "-modified";""", TestSourceBuilderOptions.WithReferenceHandling, "class A { public A Parent { get; set; } public C Value { get; set; } }", "class B { public B Parent { get; set; } public D Value { get; set; } }", @@ -268,7 +268,7 @@ public Task UserImplementedWithoutReferenceHandlerShouldWork() public Task UserImplementedWithReferenceHandlerShouldWork() { var source = TestSourceBuilder.MapperWithBodyAndTypes( - "partial B Map(A a);" + "string ToStringMod(string s, [ReferenceHandler] IReferenceHandler _) => s + \"-modified\";", + """partial B Map(A a); string ToStringMod(string s, [ReferenceHandler] IReferenceHandler _) => s + "-modified";""", TestSourceBuilderOptions.WithReferenceHandling, "class A { public A Parent { get; set; } public C Value { get; set; } }", "class B { public B Parent { get; set; } public D Value { get; set; } }", diff --git a/test/Riok.Mapperly.Tests/Mapping/RuntimeTargetTypeMappingTest.cs b/test/Riok.Mapperly.Tests/Mapping/RuntimeTargetTypeMappingTest.cs index e15a984ff1..dbddfcb975 100644 --- a/test/Riok.Mapperly.Tests/Mapping/RuntimeTargetTypeMappingTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/RuntimeTargetTypeMappingTest.cs @@ -319,8 +319,8 @@ public Task WithReferenceHandlerParameter() """ partial object Map(object source, Type targetType, [ReferenceHandler] IReferenceHandler refHandler); - private partial B MapToB(A source); - private partial D MapToD(C source); + private partial B MapToB(A source, [ReferenceHandler] IReferenceHandler refHandler); + private partial D MapToD(C source, [ReferenceHandler] IReferenceHandler refHandler); """, TestSourceBuilderOptions.WithReferenceHandling, "class A {}", diff --git a/test/Riok.Mapperly.Tests/Mapping/UseMapperTest.cs b/test/Riok.Mapperly.Tests/Mapping/UseMapperTest.cs index 49b3eeeec9..8d8fd71501 100644 --- a/test/Riok.Mapperly.Tests/Mapping/UseMapperTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/UseMapperTest.cs @@ -90,7 +90,7 @@ public Task ReferenceHandling() """ [UseMapper] private readonly OtherMapper _otherMapper = new(); - partial B Map(A source); + partial B Map(A source, [ReferenceHandler] IReferenceHandler refHandler); """, TestSourceBuilderOptions.WithReferenceHandling, "record A(AExternal Value);", @@ -109,7 +109,7 @@ public Task ReferenceHandlingEnabledNoParameter() """ [UseMapper] private readonly OtherMapper _otherMapper = new(); - partial B Map(A source); + partial B Map(A source, [ReferenceHandler] IReferenceHandler refHandler); """, TestSourceBuilderOptions.WithReferenceHandling, "record A(AExternal Value);", diff --git a/test/Riok.Mapperly.Tests/Mapping/UseStaticMapperTest.cs b/test/Riok.Mapperly.Tests/Mapping/UseStaticMapperTest.cs index 31fb92df72..9212275ac2 100644 --- a/test/Riok.Mapperly.Tests/Mapping/UseStaticMapperTest.cs +++ b/test/Riok.Mapperly.Tests/Mapping/UseStaticMapperTest.cs @@ -121,7 +121,7 @@ record BExternal(); [UseStaticMapper] public partial class Mapper { - partial B Map(A source); + partial B Map(A source, [ReferenceHandler] IReferenceHandler refHandler); } """ ); @@ -147,7 +147,7 @@ record BExternal(); [UseStaticMapper] public partial class Mapper { - partial B Map(A source); + partial B Map(A source, [ReferenceHandler] IReferenceHandler refHandler); } """ ); diff --git a/test/Riok.Mapperly.Tests/Riok.Mapperly.Tests.csproj b/test/Riok.Mapperly.Tests/Riok.Mapperly.Tests.csproj index c0d18cf545..ab1f229429 100644 --- a/test/Riok.Mapperly.Tests/Riok.Mapperly.Tests.csproj +++ b/test/Riok.Mapperly.Tests/Riok.Mapperly.Tests.csproj @@ -5,10 +5,6 @@ - - - - diff --git a/test/Riok.Mapperly.Tests/_snapshots/GenericTest.WithGenericSourceAndTargetAndEnabledReferenceHandling#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/GenericTest.WithGenericSourceAndTargetAndEnabledReferenceHandling#Mapper.g.verified.cs index 6721a0b345..db26db4bec 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/GenericTest.WithGenericSourceAndTargetAndEnabledReferenceHandling#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/GenericTest.WithGenericSourceAndTargetAndEnabledReferenceHandling#Mapper.g.verified.cs @@ -5,7 +5,7 @@ public partial class Mapper { private partial TTarget Map(TSource source) { - var refHandler = new global::Riok.Mapperly.Abstractions.ReferenceHandling.Internal.PreserveReferenceHandler(); + var refHandler = new global::Riok.Mapperly.Internal.Tests.PreserveReferenceHandler(); return source switch { global::A x when typeof(TTarget).IsAssignableFrom(typeof(global::B)) => (TTarget)(object)MapToB1(x, refHandler), @@ -17,12 +17,12 @@ private partial TTarget Map(TSource source) private partial global::B MapToB(global::A source) { - return MapToB1(source, new global::Riok.Mapperly.Abstractions.ReferenceHandling.Internal.PreserveReferenceHandler()); + return MapToB1(source, new global::Riok.Mapperly.Internal.Tests.PreserveReferenceHandler()); } private partial global::D MapToD(global::C source) { - return MapToD1(source, new global::Riok.Mapperly.Abstractions.ReferenceHandling.Internal.PreserveReferenceHandler()); + return MapToD1(source, new global::Riok.Mapperly.Internal.Tests.PreserveReferenceHandler()); } private global::B MapToB1(global::A source, global::Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler) diff --git a/test/Riok.Mapperly.Tests/_snapshots/GenericTest.WithGenericSourceAndTargetAndEnabledReferenceHandling#MapperlyInternal.PreserveReferenceHandler.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/GenericTest.WithGenericSourceAndTargetAndEnabledReferenceHandling#MapperlyInternal.PreserveReferenceHandler.g.verified.cs new file mode 100644 index 0000000000..bf9df0ab65 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/GenericTest.WithGenericSourceAndTargetAndEnabledReferenceHandling#MapperlyInternal.PreserveReferenceHandler.g.verified.cs @@ -0,0 +1,84 @@ +//HintName: MapperlyInternal.PreserveReferenceHandler.g.cs +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using Riok.Mapperly.Abstractions.ReferenceHandling; + +#nullable enable + +namespace Riok.Mapperly.Internal.Tests +{ + /// + /// A implementation + /// which returns the same target object instance if encountered the same source object instance. + /// Do not use directly. Should only be used by Mapperly generated code. + /// API surface is not subject to semantic releases and may break in any release. + /// + internal sealed class PreserveReferenceHandler : IReferenceHandler + { + private readonly Dictionary<(Type, Type), ReferenceHolder> _referenceHolders = new(); + + /// + public bool TryGetReference(TSource source, [NotNullWhen(true)] out TTarget? target) + where TSource : notnull + where TTarget : notnull + { + var refHolder = GetReferenceHolder(); + return refHolder.TryGetRef(source, out target); + } + + /// + public void SetReference(TSource source, TTarget target) + where TSource : notnull + where TTarget : notnull => GetReferenceHolder().SetRef(source, target); + + private ReferenceHolder GetReferenceHolder() + { + var mapping = (typeof(TSource), typeof(TTarget)); + if (_referenceHolders.TryGetValue(mapping, out var refHolder)) + return refHolder; + + return _referenceHolders[mapping] = new(); + } + + private sealed class ReferenceEqualityComparer : IEqualityComparer + { + // cannot use System.Collections.Generic.ReferenceEqualityComparer since it is not available in netstandard2.0 + + public static readonly IEqualityComparer Instance = new ReferenceEqualityComparer(); + + private ReferenceEqualityComparer() { } + + bool IEqualityComparer.Equals(T? x, T? y) => ReferenceEquals(x, y); + + int IEqualityComparer.GetHashCode(T obj) => RuntimeHelpers.GetHashCode(obj); + } + + private class ReferenceHolder + { + private readonly Dictionary _references = new(ReferenceEqualityComparer.Instance); + + public bool TryGetRef(TSource source, [NotNullWhen(true)] out TTarget? target) + where TSource : notnull + where TTarget : notnull + { + if (_references.TryGetValue(source, out var targetObj)) + { + target = (TTarget)targetObj; + return true; + } + + target = default; + return false; + } + + public void SetRef(TSource source, TTarget target) + where TSource : notnull + where TTarget : notnull + { + _references[source] = target; + } + } + } +} diff --git a/test/Riok.Mapperly.Tests/_snapshots/GenericTest.WithGenericSourceAndTargetAndEnabledReferenceHandlingAndParameter#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/GenericTest.WithGenericSourceAndTargetAndEnabledReferenceHandlingAndParameter#Mapper.g.verified.cs index 0d6ea93c1b..f349354e87 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/GenericTest.WithGenericSourceAndTargetAndEnabledReferenceHandlingAndParameter#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/GenericTest.WithGenericSourceAndTargetAndEnabledReferenceHandlingAndParameter#Mapper.g.verified.cs @@ -7,24 +7,14 @@ private partial TTarget Map(TSource source, global::Riok.Mappe { return source switch { - global::A x when typeof(TTarget).IsAssignableFrom(typeof(global::B)) => (TTarget)(object)MapToB1(x, refHandler), - global::C x when typeof(TTarget).IsAssignableFrom(typeof(global::D)) => (TTarget)(object)MapToD1(x, refHandler), + global::A x when typeof(TTarget).IsAssignableFrom(typeof(global::B)) => (TTarget)(object)MapToB(x, refHandler), + global::C x when typeof(TTarget).IsAssignableFrom(typeof(global::D)) => (TTarget)(object)MapToD(x, refHandler), null => throw new System.ArgumentNullException(nameof(source)), _ => throw new System.ArgumentException($"Cannot map {source.GetType()} to {typeof(TTarget)} as there is no known type mapping", nameof(source)), }; } - private partial global::B MapToB(global::A source) - { - return MapToB1(source, new global::Riok.Mapperly.Abstractions.ReferenceHandling.Internal.PreserveReferenceHandler()); - } - - private partial global::D MapToD(global::C source) - { - return MapToD1(source, new global::Riok.Mapperly.Abstractions.ReferenceHandling.Internal.PreserveReferenceHandler()); - } - - private global::B MapToB1(global::A source, global::Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler) + private partial global::B MapToB(global::A source, global::Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler) { if (refHandler.TryGetReference(source, out var existingTargetReference)) return existingTargetReference; @@ -34,7 +24,7 @@ private partial TTarget Map(TSource source, global::Riok.Mappe return target; } - private global::D MapToD1(global::C source, global::Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler) + private partial global::D MapToD(global::C source, global::Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler) { if (refHandler.TryGetReference(source, out var existingTargetReference)) return existingTargetReference; diff --git a/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ArrayShouldWork#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ArrayShouldWork#Mapper.g.verified.cs index a2d64fc5cf..284e4acef3 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ArrayShouldWork#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ArrayShouldWork#Mapper.g.verified.cs @@ -5,7 +5,7 @@ public partial class Mapper { private partial global::B Map(global::A source) { - return MapToB(source, new global::Riok.Mapperly.Abstractions.ReferenceHandling.Internal.PreserveReferenceHandler()); + return MapToB(source, new global::Riok.Mapperly.Internal.Tests.PreserveReferenceHandler()); } private global::B MapToB(global::A source, global::Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler) diff --git a/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ArrayShouldWork#MapperlyInternal.PreserveReferenceHandler.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ArrayShouldWork#MapperlyInternal.PreserveReferenceHandler.g.verified.cs new file mode 100644 index 0000000000..bf9df0ab65 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ArrayShouldWork#MapperlyInternal.PreserveReferenceHandler.g.verified.cs @@ -0,0 +1,84 @@ +//HintName: MapperlyInternal.PreserveReferenceHandler.g.cs +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using Riok.Mapperly.Abstractions.ReferenceHandling; + +#nullable enable + +namespace Riok.Mapperly.Internal.Tests +{ + /// + /// A implementation + /// which returns the same target object instance if encountered the same source object instance. + /// Do not use directly. Should only be used by Mapperly generated code. + /// API surface is not subject to semantic releases and may break in any release. + /// + internal sealed class PreserveReferenceHandler : IReferenceHandler + { + private readonly Dictionary<(Type, Type), ReferenceHolder> _referenceHolders = new(); + + /// + public bool TryGetReference(TSource source, [NotNullWhen(true)] out TTarget? target) + where TSource : notnull + where TTarget : notnull + { + var refHolder = GetReferenceHolder(); + return refHolder.TryGetRef(source, out target); + } + + /// + public void SetReference(TSource source, TTarget target) + where TSource : notnull + where TTarget : notnull => GetReferenceHolder().SetRef(source, target); + + private ReferenceHolder GetReferenceHolder() + { + var mapping = (typeof(TSource), typeof(TTarget)); + if (_referenceHolders.TryGetValue(mapping, out var refHolder)) + return refHolder; + + return _referenceHolders[mapping] = new(); + } + + private sealed class ReferenceEqualityComparer : IEqualityComparer + { + // cannot use System.Collections.Generic.ReferenceEqualityComparer since it is not available in netstandard2.0 + + public static readonly IEqualityComparer Instance = new ReferenceEqualityComparer(); + + private ReferenceEqualityComparer() { } + + bool IEqualityComparer.Equals(T? x, T? y) => ReferenceEquals(x, y); + + int IEqualityComparer.GetHashCode(T obj) => RuntimeHelpers.GetHashCode(obj); + } + + private class ReferenceHolder + { + private readonly Dictionary _references = new(ReferenceEqualityComparer.Instance); + + public bool TryGetRef(TSource source, [NotNullWhen(true)] out TTarget? target) + where TSource : notnull + where TTarget : notnull + { + if (_references.TryGetValue(source, out var targetObj)) + { + target = (TTarget)targetObj; + return true; + } + + target = default; + return false; + } + + public void SetRef(TSource source, TTarget target) + where TSource : notnull + where TTarget : notnull + { + _references[source] = target; + } + } + } +} diff --git a/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.EnumerableShouldWork#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.EnumerableShouldWork#Mapper.g.verified.cs index ba0d71b531..9514a0eda1 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.EnumerableShouldWork#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.EnumerableShouldWork#Mapper.g.verified.cs @@ -5,7 +5,7 @@ public partial class Mapper { private partial global::B Map(global::A source) { - return MapToB(source, new global::Riok.Mapperly.Abstractions.ReferenceHandling.Internal.PreserveReferenceHandler()); + return MapToB(source, new global::Riok.Mapperly.Internal.Tests.PreserveReferenceHandler()); } private global::B MapToB(global::A source, global::Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler) diff --git a/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.EnumerableShouldWork#MapperlyInternal.PreserveReferenceHandler.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.EnumerableShouldWork#MapperlyInternal.PreserveReferenceHandler.g.verified.cs new file mode 100644 index 0000000000..bf9df0ab65 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.EnumerableShouldWork#MapperlyInternal.PreserveReferenceHandler.g.verified.cs @@ -0,0 +1,84 @@ +//HintName: MapperlyInternal.PreserveReferenceHandler.g.cs +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using Riok.Mapperly.Abstractions.ReferenceHandling; + +#nullable enable + +namespace Riok.Mapperly.Internal.Tests +{ + /// + /// A implementation + /// which returns the same target object instance if encountered the same source object instance. + /// Do not use directly. Should only be used by Mapperly generated code. + /// API surface is not subject to semantic releases and may break in any release. + /// + internal sealed class PreserveReferenceHandler : IReferenceHandler + { + private readonly Dictionary<(Type, Type), ReferenceHolder> _referenceHolders = new(); + + /// + public bool TryGetReference(TSource source, [NotNullWhen(true)] out TTarget? target) + where TSource : notnull + where TTarget : notnull + { + var refHolder = GetReferenceHolder(); + return refHolder.TryGetRef(source, out target); + } + + /// + public void SetReference(TSource source, TTarget target) + where TSource : notnull + where TTarget : notnull => GetReferenceHolder().SetRef(source, target); + + private ReferenceHolder GetReferenceHolder() + { + var mapping = (typeof(TSource), typeof(TTarget)); + if (_referenceHolders.TryGetValue(mapping, out var refHolder)) + return refHolder; + + return _referenceHolders[mapping] = new(); + } + + private sealed class ReferenceEqualityComparer : IEqualityComparer + { + // cannot use System.Collections.Generic.ReferenceEqualityComparer since it is not available in netstandard2.0 + + public static readonly IEqualityComparer Instance = new ReferenceEqualityComparer(); + + private ReferenceEqualityComparer() { } + + bool IEqualityComparer.Equals(T? x, T? y) => ReferenceEquals(x, y); + + int IEqualityComparer.GetHashCode(T obj) => RuntimeHelpers.GetHashCode(obj); + } + + private class ReferenceHolder + { + private readonly Dictionary _references = new(ReferenceEqualityComparer.Instance); + + public bool TryGetRef(TSource source, [NotNullWhen(true)] out TTarget? target) + where TSource : notnull + where TTarget : notnull + { + if (_references.TryGetValue(source, out var targetObj)) + { + target = (TTarget)targetObj; + return true; + } + + target = default; + return false; + } + + public void SetRef(TSource source, TTarget target) + where TSource : notnull + where TTarget : notnull + { + _references[source] = target; + } + } + } +} diff --git a/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ExistingInstanceShouldWork#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ExistingInstanceShouldWork#Mapper.g.verified.cs index c29596e50c..01a652c5c3 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ExistingInstanceShouldWork#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ExistingInstanceShouldWork#Mapper.g.verified.cs @@ -5,7 +5,7 @@ public partial class Mapper { private partial void Map(global::A source, global::B target) { - var refHandler = new global::Riok.Mapperly.Abstractions.ReferenceHandling.Internal.PreserveReferenceHandler(); + var refHandler = new global::Riok.Mapperly.Internal.Tests.PreserveReferenceHandler(); target.Parent = MapToB(source.Parent, refHandler); target.Value = MapToD(source.Value, refHandler); } diff --git a/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ExistingInstanceShouldWork#MapperlyInternal.PreserveReferenceHandler.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ExistingInstanceShouldWork#MapperlyInternal.PreserveReferenceHandler.g.verified.cs new file mode 100644 index 0000000000..bf9df0ab65 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ExistingInstanceShouldWork#MapperlyInternal.PreserveReferenceHandler.g.verified.cs @@ -0,0 +1,84 @@ +//HintName: MapperlyInternal.PreserveReferenceHandler.g.cs +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using Riok.Mapperly.Abstractions.ReferenceHandling; + +#nullable enable + +namespace Riok.Mapperly.Internal.Tests +{ + /// + /// A implementation + /// which returns the same target object instance if encountered the same source object instance. + /// Do not use directly. Should only be used by Mapperly generated code. + /// API surface is not subject to semantic releases and may break in any release. + /// + internal sealed class PreserveReferenceHandler : IReferenceHandler + { + private readonly Dictionary<(Type, Type), ReferenceHolder> _referenceHolders = new(); + + /// + public bool TryGetReference(TSource source, [NotNullWhen(true)] out TTarget? target) + where TSource : notnull + where TTarget : notnull + { + var refHolder = GetReferenceHolder(); + return refHolder.TryGetRef(source, out target); + } + + /// + public void SetReference(TSource source, TTarget target) + where TSource : notnull + where TTarget : notnull => GetReferenceHolder().SetRef(source, target); + + private ReferenceHolder GetReferenceHolder() + { + var mapping = (typeof(TSource), typeof(TTarget)); + if (_referenceHolders.TryGetValue(mapping, out var refHolder)) + return refHolder; + + return _referenceHolders[mapping] = new(); + } + + private sealed class ReferenceEqualityComparer : IEqualityComparer + { + // cannot use System.Collections.Generic.ReferenceEqualityComparer since it is not available in netstandard2.0 + + public static readonly IEqualityComparer Instance = new ReferenceEqualityComparer(); + + private ReferenceEqualityComparer() { } + + bool IEqualityComparer.Equals(T? x, T? y) => ReferenceEquals(x, y); + + int IEqualityComparer.GetHashCode(T obj) => RuntimeHelpers.GetHashCode(obj); + } + + private class ReferenceHolder + { + private readonly Dictionary _references = new(ReferenceEqualityComparer.Instance); + + public bool TryGetRef(TSource source, [NotNullWhen(true)] out TTarget? target) + where TSource : notnull + where TTarget : notnull + { + if (_references.TryGetValue(source, out var targetObj)) + { + target = (TTarget)targetObj; + return true; + } + + target = default; + return false; + } + + public void SetRef(TSource source, TTarget target) + where TSource : notnull + where TTarget : notnull + { + _references[source] = target; + } + } + } +} diff --git a/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ManuallyMappedPropertiesShouldWork#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ManuallyMappedPropertiesShouldWork#Mapper.g.verified.cs index 76880f955c..9f397dec45 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ManuallyMappedPropertiesShouldWork#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ManuallyMappedPropertiesShouldWork#Mapper.g.verified.cs @@ -5,12 +5,12 @@ public partial class Mapper { private partial global::B MapToB(global::A source) { - return MapToB2(source, new global::Riok.Mapperly.Abstractions.ReferenceHandling.Internal.PreserveReferenceHandler()); + return MapToB2(source, new global::Riok.Mapperly.Internal.Tests.PreserveReferenceHandler()); } private partial global::B MapToB1(global::A source) { - return MapToB3(source, new global::Riok.Mapperly.Abstractions.ReferenceHandling.Internal.PreserveReferenceHandler()); + return MapToB3(source, new global::Riok.Mapperly.Internal.Tests.PreserveReferenceHandler()); } private global::B MapToB2(global::A source, global::Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler) diff --git a/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ManuallyMappedPropertiesShouldWork#MapperlyInternal.PreserveReferenceHandler.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ManuallyMappedPropertiesShouldWork#MapperlyInternal.PreserveReferenceHandler.g.verified.cs new file mode 100644 index 0000000000..bf9df0ab65 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ManuallyMappedPropertiesShouldWork#MapperlyInternal.PreserveReferenceHandler.g.verified.cs @@ -0,0 +1,84 @@ +//HintName: MapperlyInternal.PreserveReferenceHandler.g.cs +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using Riok.Mapperly.Abstractions.ReferenceHandling; + +#nullable enable + +namespace Riok.Mapperly.Internal.Tests +{ + /// + /// A implementation + /// which returns the same target object instance if encountered the same source object instance. + /// Do not use directly. Should only be used by Mapperly generated code. + /// API surface is not subject to semantic releases and may break in any release. + /// + internal sealed class PreserveReferenceHandler : IReferenceHandler + { + private readonly Dictionary<(Type, Type), ReferenceHolder> _referenceHolders = new(); + + /// + public bool TryGetReference(TSource source, [NotNullWhen(true)] out TTarget? target) + where TSource : notnull + where TTarget : notnull + { + var refHolder = GetReferenceHolder(); + return refHolder.TryGetRef(source, out target); + } + + /// + public void SetReference(TSource source, TTarget target) + where TSource : notnull + where TTarget : notnull => GetReferenceHolder().SetRef(source, target); + + private ReferenceHolder GetReferenceHolder() + { + var mapping = (typeof(TSource), typeof(TTarget)); + if (_referenceHolders.TryGetValue(mapping, out var refHolder)) + return refHolder; + + return _referenceHolders[mapping] = new(); + } + + private sealed class ReferenceEqualityComparer : IEqualityComparer + { + // cannot use System.Collections.Generic.ReferenceEqualityComparer since it is not available in netstandard2.0 + + public static readonly IEqualityComparer Instance = new ReferenceEqualityComparer(); + + private ReferenceEqualityComparer() { } + + bool IEqualityComparer.Equals(T? x, T? y) => ReferenceEquals(x, y); + + int IEqualityComparer.GetHashCode(T obj) => RuntimeHelpers.GetHashCode(obj); + } + + private class ReferenceHolder + { + private readonly Dictionary _references = new(ReferenceEqualityComparer.Instance); + + public bool TryGetRef(TSource source, [NotNullWhen(true)] out TTarget? target) + where TSource : notnull + where TTarget : notnull + { + if (_references.TryGetValue(source, out var targetObj)) + { + target = (TTarget)targetObj; + return true; + } + + target = default; + return false; + } + + public void SetRef(TSource source, TTarget target) + where TSource : notnull + where TTarget : notnull + { + _references[source] = target; + } + } + } +} diff --git a/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ObjectFactoryShouldWork#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ObjectFactoryShouldWork#Mapper.g.verified.cs index 228215e318..e3aef2a6ed 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ObjectFactoryShouldWork#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ObjectFactoryShouldWork#Mapper.g.verified.cs @@ -5,7 +5,7 @@ public partial class Mapper { private partial global::B Map(global::A a) { - return MapToB(a, new global::Riok.Mapperly.Abstractions.ReferenceHandling.Internal.PreserveReferenceHandler()); + return MapToB(a, new global::Riok.Mapperly.Internal.Tests.PreserveReferenceHandler()); } private global::B MapToB(global::A source, global::Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler) diff --git a/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ObjectFactoryShouldWork#MapperlyInternal.PreserveReferenceHandler.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ObjectFactoryShouldWork#MapperlyInternal.PreserveReferenceHandler.g.verified.cs new file mode 100644 index 0000000000..bf9df0ab65 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ObjectFactoryShouldWork#MapperlyInternal.PreserveReferenceHandler.g.verified.cs @@ -0,0 +1,84 @@ +//HintName: MapperlyInternal.PreserveReferenceHandler.g.cs +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using Riok.Mapperly.Abstractions.ReferenceHandling; + +#nullable enable + +namespace Riok.Mapperly.Internal.Tests +{ + /// + /// A implementation + /// which returns the same target object instance if encountered the same source object instance. + /// Do not use directly. Should only be used by Mapperly generated code. + /// API surface is not subject to semantic releases and may break in any release. + /// + internal sealed class PreserveReferenceHandler : IReferenceHandler + { + private readonly Dictionary<(Type, Type), ReferenceHolder> _referenceHolders = new(); + + /// + public bool TryGetReference(TSource source, [NotNullWhen(true)] out TTarget? target) + where TSource : notnull + where TTarget : notnull + { + var refHolder = GetReferenceHolder(); + return refHolder.TryGetRef(source, out target); + } + + /// + public void SetReference(TSource source, TTarget target) + where TSource : notnull + where TTarget : notnull => GetReferenceHolder().SetRef(source, target); + + private ReferenceHolder GetReferenceHolder() + { + var mapping = (typeof(TSource), typeof(TTarget)); + if (_referenceHolders.TryGetValue(mapping, out var refHolder)) + return refHolder; + + return _referenceHolders[mapping] = new(); + } + + private sealed class ReferenceEqualityComparer : IEqualityComparer + { + // cannot use System.Collections.Generic.ReferenceEqualityComparer since it is not available in netstandard2.0 + + public static readonly IEqualityComparer Instance = new ReferenceEqualityComparer(); + + private ReferenceEqualityComparer() { } + + bool IEqualityComparer.Equals(T? x, T? y) => ReferenceEquals(x, y); + + int IEqualityComparer.GetHashCode(T obj) => RuntimeHelpers.GetHashCode(obj); + } + + private class ReferenceHolder + { + private readonly Dictionary _references = new(ReferenceEqualityComparer.Instance); + + public bool TryGetRef(TSource source, [NotNullWhen(true)] out TTarget? target) + where TSource : notnull + where TTarget : notnull + { + if (_references.TryGetValue(source, out var targetObj)) + { + target = (TTarget)targetObj; + return true; + } + + target = default; + return false; + } + + public void SetRef(TSource source, TTarget target) + where TSource : notnull + where TTarget : notnull + { + _references[source] = target; + } + } + } +} diff --git a/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.RuntimeTargetTypeShouldWork#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.RuntimeTargetTypeShouldWork#Mapper.g.verified.cs index b5f352be11..b8fbb29b7c 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.RuntimeTargetTypeShouldWork#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.RuntimeTargetTypeShouldWork#Mapper.g.verified.cs @@ -5,7 +5,7 @@ public partial class Mapper { public partial object Map(object source, global::System.Type destinationType) { - var refHandler = new global::Riok.Mapperly.Abstractions.ReferenceHandling.Internal.PreserveReferenceHandler(); + var refHandler = new global::Riok.Mapperly.Internal.Tests.PreserveReferenceHandler(); return source switch { global::A x when destinationType.IsAssignableFrom(typeof(global::B)) => MapToB(x, refHandler), diff --git a/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.RuntimeTargetTypeShouldWork#MapperlyInternal.PreserveReferenceHandler.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.RuntimeTargetTypeShouldWork#MapperlyInternal.PreserveReferenceHandler.g.verified.cs new file mode 100644 index 0000000000..bf9df0ab65 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.RuntimeTargetTypeShouldWork#MapperlyInternal.PreserveReferenceHandler.g.verified.cs @@ -0,0 +1,84 @@ +//HintName: MapperlyInternal.PreserveReferenceHandler.g.cs +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using Riok.Mapperly.Abstractions.ReferenceHandling; + +#nullable enable + +namespace Riok.Mapperly.Internal.Tests +{ + /// + /// A implementation + /// which returns the same target object instance if encountered the same source object instance. + /// Do not use directly. Should only be used by Mapperly generated code. + /// API surface is not subject to semantic releases and may break in any release. + /// + internal sealed class PreserveReferenceHandler : IReferenceHandler + { + private readonly Dictionary<(Type, Type), ReferenceHolder> _referenceHolders = new(); + + /// + public bool TryGetReference(TSource source, [NotNullWhen(true)] out TTarget? target) + where TSource : notnull + where TTarget : notnull + { + var refHolder = GetReferenceHolder(); + return refHolder.TryGetRef(source, out target); + } + + /// + public void SetReference(TSource source, TTarget target) + where TSource : notnull + where TTarget : notnull => GetReferenceHolder().SetRef(source, target); + + private ReferenceHolder GetReferenceHolder() + { + var mapping = (typeof(TSource), typeof(TTarget)); + if (_referenceHolders.TryGetValue(mapping, out var refHolder)) + return refHolder; + + return _referenceHolders[mapping] = new(); + } + + private sealed class ReferenceEqualityComparer : IEqualityComparer + { + // cannot use System.Collections.Generic.ReferenceEqualityComparer since it is not available in netstandard2.0 + + public static readonly IEqualityComparer Instance = new ReferenceEqualityComparer(); + + private ReferenceEqualityComparer() { } + + bool IEqualityComparer.Equals(T? x, T? y) => ReferenceEquals(x, y); + + int IEqualityComparer.GetHashCode(T obj) => RuntimeHelpers.GetHashCode(obj); + } + + private class ReferenceHolder + { + private readonly Dictionary _references = new(ReferenceEqualityComparer.Instance); + + public bool TryGetRef(TSource source, [NotNullWhen(true)] out TTarget? target) + where TSource : notnull + where TTarget : notnull + { + if (_references.TryGetValue(source, out var targetObj)) + { + target = (TTarget)targetObj; + return true; + } + + target = default; + return false; + } + + public void SetRef(TSource source, TTarget target) + where TSource : notnull + where TTarget : notnull + { + _references[source] = target; + } + } + } +} diff --git a/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ShouldWork#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ShouldWork#Mapper.g.verified.cs index a47835dc7d..a0c1750541 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ShouldWork#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ShouldWork#Mapper.g.verified.cs @@ -5,7 +5,7 @@ public partial class Mapper { private partial global::B Map(global::A source) { - return MapToB(source, new global::Riok.Mapperly.Abstractions.ReferenceHandling.Internal.PreserveReferenceHandler()); + return MapToB(source, new global::Riok.Mapperly.Internal.Tests.PreserveReferenceHandler()); } private global::B MapToB(global::A source, global::Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler) diff --git a/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ShouldWork#MapperlyInternal.PreserveReferenceHandler.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ShouldWork#MapperlyInternal.PreserveReferenceHandler.g.verified.cs new file mode 100644 index 0000000000..bf9df0ab65 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.ShouldWork#MapperlyInternal.PreserveReferenceHandler.g.verified.cs @@ -0,0 +1,84 @@ +//HintName: MapperlyInternal.PreserveReferenceHandler.g.cs +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using Riok.Mapperly.Abstractions.ReferenceHandling; + +#nullable enable + +namespace Riok.Mapperly.Internal.Tests +{ + /// + /// A implementation + /// which returns the same target object instance if encountered the same source object instance. + /// Do not use directly. Should only be used by Mapperly generated code. + /// API surface is not subject to semantic releases and may break in any release. + /// + internal sealed class PreserveReferenceHandler : IReferenceHandler + { + private readonly Dictionary<(Type, Type), ReferenceHolder> _referenceHolders = new(); + + /// + public bool TryGetReference(TSource source, [NotNullWhen(true)] out TTarget? target) + where TSource : notnull + where TTarget : notnull + { + var refHolder = GetReferenceHolder(); + return refHolder.TryGetRef(source, out target); + } + + /// + public void SetReference(TSource source, TTarget target) + where TSource : notnull + where TTarget : notnull => GetReferenceHolder().SetRef(source, target); + + private ReferenceHolder GetReferenceHolder() + { + var mapping = (typeof(TSource), typeof(TTarget)); + if (_referenceHolders.TryGetValue(mapping, out var refHolder)) + return refHolder; + + return _referenceHolders[mapping] = new(); + } + + private sealed class ReferenceEqualityComparer : IEqualityComparer + { + // cannot use System.Collections.Generic.ReferenceEqualityComparer since it is not available in netstandard2.0 + + public static readonly IEqualityComparer Instance = new ReferenceEqualityComparer(); + + private ReferenceEqualityComparer() { } + + bool IEqualityComparer.Equals(T? x, T? y) => ReferenceEquals(x, y); + + int IEqualityComparer.GetHashCode(T obj) => RuntimeHelpers.GetHashCode(obj); + } + + private class ReferenceHolder + { + private readonly Dictionary _references = new(ReferenceEqualityComparer.Instance); + + public bool TryGetRef(TSource source, [NotNullWhen(true)] out TTarget? target) + where TSource : notnull + where TTarget : notnull + { + if (_references.TryGetValue(source, out var targetObj)) + { + target = (TTarget)targetObj; + return true; + } + + target = default; + return false; + } + + public void SetRef(TSource source, TTarget target) + where TSource : notnull + where TTarget : notnull + { + _references[source] = target; + } + } + } +} diff --git a/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.UserImplementedWithReferenceHandlerShouldWork#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.UserImplementedWithReferenceHandlerShouldWork#Mapper.g.verified.cs index 8215ce4d20..cf1155ef2b 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.UserImplementedWithReferenceHandlerShouldWork#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.UserImplementedWithReferenceHandlerShouldWork#Mapper.g.verified.cs @@ -5,7 +5,7 @@ public partial class Mapper { private partial global::B Map(global::A a) { - return MapToB(a, new global::Riok.Mapperly.Abstractions.ReferenceHandling.Internal.PreserveReferenceHandler()); + return MapToB(a, new global::Riok.Mapperly.Internal.Tests.PreserveReferenceHandler()); } private global::B MapToB(global::A source, global::Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler) diff --git a/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.UserImplementedWithReferenceHandlerShouldWork#MapperlyInternal.PreserveReferenceHandler.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.UserImplementedWithReferenceHandlerShouldWork#MapperlyInternal.PreserveReferenceHandler.g.verified.cs new file mode 100644 index 0000000000..bf9df0ab65 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.UserImplementedWithReferenceHandlerShouldWork#MapperlyInternal.PreserveReferenceHandler.g.verified.cs @@ -0,0 +1,84 @@ +//HintName: MapperlyInternal.PreserveReferenceHandler.g.cs +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using Riok.Mapperly.Abstractions.ReferenceHandling; + +#nullable enable + +namespace Riok.Mapperly.Internal.Tests +{ + /// + /// A implementation + /// which returns the same target object instance if encountered the same source object instance. + /// Do not use directly. Should only be used by Mapperly generated code. + /// API surface is not subject to semantic releases and may break in any release. + /// + internal sealed class PreserveReferenceHandler : IReferenceHandler + { + private readonly Dictionary<(Type, Type), ReferenceHolder> _referenceHolders = new(); + + /// + public bool TryGetReference(TSource source, [NotNullWhen(true)] out TTarget? target) + where TSource : notnull + where TTarget : notnull + { + var refHolder = GetReferenceHolder(); + return refHolder.TryGetRef(source, out target); + } + + /// + public void SetReference(TSource source, TTarget target) + where TSource : notnull + where TTarget : notnull => GetReferenceHolder().SetRef(source, target); + + private ReferenceHolder GetReferenceHolder() + { + var mapping = (typeof(TSource), typeof(TTarget)); + if (_referenceHolders.TryGetValue(mapping, out var refHolder)) + return refHolder; + + return _referenceHolders[mapping] = new(); + } + + private sealed class ReferenceEqualityComparer : IEqualityComparer + { + // cannot use System.Collections.Generic.ReferenceEqualityComparer since it is not available in netstandard2.0 + + public static readonly IEqualityComparer Instance = new ReferenceEqualityComparer(); + + private ReferenceEqualityComparer() { } + + bool IEqualityComparer.Equals(T? x, T? y) => ReferenceEquals(x, y); + + int IEqualityComparer.GetHashCode(T obj) => RuntimeHelpers.GetHashCode(obj); + } + + private class ReferenceHolder + { + private readonly Dictionary _references = new(ReferenceEqualityComparer.Instance); + + public bool TryGetRef(TSource source, [NotNullWhen(true)] out TTarget? target) + where TSource : notnull + where TTarget : notnull + { + if (_references.TryGetValue(source, out var targetObj)) + { + target = (TTarget)targetObj; + return true; + } + + target = default; + return false; + } + + public void SetRef(TSource source, TTarget target) + where TSource : notnull + where TTarget : notnull + { + _references[source] = target; + } + } + } +} diff --git a/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.UserImplementedWithoutReferenceHandlerShouldWork#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.UserImplementedWithoutReferenceHandlerShouldWork#Mapper.g.verified.cs index 8d2dfd427f..ce40d441a7 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.UserImplementedWithoutReferenceHandlerShouldWork#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.UserImplementedWithoutReferenceHandlerShouldWork#Mapper.g.verified.cs @@ -5,7 +5,7 @@ public partial class Mapper { private partial global::B Map(global::A a) { - return MapToB(a, new global::Riok.Mapperly.Abstractions.ReferenceHandling.Internal.PreserveReferenceHandler()); + return MapToB(a, new global::Riok.Mapperly.Internal.Tests.PreserveReferenceHandler()); } private global::B MapToB(global::A source, global::Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler) diff --git a/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.UserImplementedWithoutReferenceHandlerShouldWork#MapperlyInternal.PreserveReferenceHandler.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.UserImplementedWithoutReferenceHandlerShouldWork#MapperlyInternal.PreserveReferenceHandler.g.verified.cs new file mode 100644 index 0000000000..bf9df0ab65 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/ReferenceHandlingTest.UserImplementedWithoutReferenceHandlerShouldWork#MapperlyInternal.PreserveReferenceHandler.g.verified.cs @@ -0,0 +1,84 @@ +//HintName: MapperlyInternal.PreserveReferenceHandler.g.cs +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using Riok.Mapperly.Abstractions.ReferenceHandling; + +#nullable enable + +namespace Riok.Mapperly.Internal.Tests +{ + /// + /// A implementation + /// which returns the same target object instance if encountered the same source object instance. + /// Do not use directly. Should only be used by Mapperly generated code. + /// API surface is not subject to semantic releases and may break in any release. + /// + internal sealed class PreserveReferenceHandler : IReferenceHandler + { + private readonly Dictionary<(Type, Type), ReferenceHolder> _referenceHolders = new(); + + /// + public bool TryGetReference(TSource source, [NotNullWhen(true)] out TTarget? target) + where TSource : notnull + where TTarget : notnull + { + var refHolder = GetReferenceHolder(); + return refHolder.TryGetRef(source, out target); + } + + /// + public void SetReference(TSource source, TTarget target) + where TSource : notnull + where TTarget : notnull => GetReferenceHolder().SetRef(source, target); + + private ReferenceHolder GetReferenceHolder() + { + var mapping = (typeof(TSource), typeof(TTarget)); + if (_referenceHolders.TryGetValue(mapping, out var refHolder)) + return refHolder; + + return _referenceHolders[mapping] = new(); + } + + private sealed class ReferenceEqualityComparer : IEqualityComparer + { + // cannot use System.Collections.Generic.ReferenceEqualityComparer since it is not available in netstandard2.0 + + public static readonly IEqualityComparer Instance = new ReferenceEqualityComparer(); + + private ReferenceEqualityComparer() { } + + bool IEqualityComparer.Equals(T? x, T? y) => ReferenceEquals(x, y); + + int IEqualityComparer.GetHashCode(T obj) => RuntimeHelpers.GetHashCode(obj); + } + + private class ReferenceHolder + { + private readonly Dictionary _references = new(ReferenceEqualityComparer.Instance); + + public bool TryGetRef(TSource source, [NotNullWhen(true)] out TTarget? target) + where TSource : notnull + where TTarget : notnull + { + if (_references.TryGetValue(source, out var targetObj)) + { + target = (TTarget)targetObj; + return true; + } + + target = default; + return false; + } + + public void SetRef(TSource source, TTarget target) + where TSource : notnull + where TTarget : notnull + { + _references[source] = target; + } + } + } +} diff --git a/test/Riok.Mapperly.Tests/_snapshots/RuntimeTargetTypeMappingTest.WithReferenceHandlerParameter#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/RuntimeTargetTypeMappingTest.WithReferenceHandlerParameter#Mapper.g.verified.cs index 0f7c71898a..9514e0962e 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/RuntimeTargetTypeMappingTest.WithReferenceHandlerParameter#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/RuntimeTargetTypeMappingTest.WithReferenceHandlerParameter#Mapper.g.verified.cs @@ -7,24 +7,14 @@ private partial object Map(object source, global::System.Type targetType, global { return source switch { - global::A x when targetType.IsAssignableFrom(typeof(global::B)) => MapToB1(x, refHandler), - global::C x when targetType.IsAssignableFrom(typeof(global::D)) => MapToD1(x, refHandler), + global::A x when targetType.IsAssignableFrom(typeof(global::B)) => MapToB(x, refHandler), + global::C x when targetType.IsAssignableFrom(typeof(global::D)) => MapToD(x, refHandler), null => throw new System.ArgumentNullException(nameof(source)), _ => throw new System.ArgumentException($"Cannot map {source.GetType()} to {targetType} as there is no known type mapping", nameof(source)), }; } - private partial global::B MapToB(global::A source) - { - return MapToB1(source, new global::Riok.Mapperly.Abstractions.ReferenceHandling.Internal.PreserveReferenceHandler()); - } - - private partial global::D MapToD(global::C source) - { - return MapToD1(source, new global::Riok.Mapperly.Abstractions.ReferenceHandling.Internal.PreserveReferenceHandler()); - } - - private global::B MapToB1(global::A source, global::Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler) + private partial global::B MapToB(global::A source, global::Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler) { if (refHandler.TryGetReference(source, out var existingTargetReference)) return existingTargetReference; @@ -33,7 +23,7 @@ private partial object Map(object source, global::System.Type targetType, global return target; } - private global::D MapToD1(global::C source, global::Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler) + private partial global::D MapToD(global::C source, global::Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler) { if (refHandler.TryGetReference(source, out var existingTargetReference)) return existingTargetReference; diff --git a/test/Riok.Mapperly.Tests/_snapshots/RuntimeTargetTypeMappingTest.WithReferenceHandlingEnabled#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/RuntimeTargetTypeMappingTest.WithReferenceHandlingEnabled#Mapper.g.verified.cs index cc6aab415e..75719c613c 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/RuntimeTargetTypeMappingTest.WithReferenceHandlingEnabled#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/RuntimeTargetTypeMappingTest.WithReferenceHandlingEnabled#Mapper.g.verified.cs @@ -5,7 +5,7 @@ public partial class Mapper { private partial object Map(object source, global::System.Type targetType) { - var refHandler = new global::Riok.Mapperly.Abstractions.ReferenceHandling.Internal.PreserveReferenceHandler(); + var refHandler = new global::Riok.Mapperly.Internal.Tests.PreserveReferenceHandler(); return source switch { global::A x when targetType.IsAssignableFrom(typeof(global::B)) => MapToB1(x, refHandler), @@ -17,12 +17,12 @@ private partial object Map(object source, global::System.Type targetType) private partial global::B MapToB(global::A source) { - return MapToB1(source, new global::Riok.Mapperly.Abstractions.ReferenceHandling.Internal.PreserveReferenceHandler()); + return MapToB1(source, new global::Riok.Mapperly.Internal.Tests.PreserveReferenceHandler()); } private partial global::D MapToD(global::C source) { - return MapToD1(source, new global::Riok.Mapperly.Abstractions.ReferenceHandling.Internal.PreserveReferenceHandler()); + return MapToD1(source, new global::Riok.Mapperly.Internal.Tests.PreserveReferenceHandler()); } private global::B MapToB1(global::A source, global::Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler) diff --git a/test/Riok.Mapperly.Tests/_snapshots/RuntimeTargetTypeMappingTest.WithReferenceHandlingEnabled#MapperlyInternal.PreserveReferenceHandler.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/RuntimeTargetTypeMappingTest.WithReferenceHandlingEnabled#MapperlyInternal.PreserveReferenceHandler.g.verified.cs new file mode 100644 index 0000000000..bf9df0ab65 --- /dev/null +++ b/test/Riok.Mapperly.Tests/_snapshots/RuntimeTargetTypeMappingTest.WithReferenceHandlingEnabled#MapperlyInternal.PreserveReferenceHandler.g.verified.cs @@ -0,0 +1,84 @@ +//HintName: MapperlyInternal.PreserveReferenceHandler.g.cs +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using Riok.Mapperly.Abstractions.ReferenceHandling; + +#nullable enable + +namespace Riok.Mapperly.Internal.Tests +{ + /// + /// A implementation + /// which returns the same target object instance if encountered the same source object instance. + /// Do not use directly. Should only be used by Mapperly generated code. + /// API surface is not subject to semantic releases and may break in any release. + /// + internal sealed class PreserveReferenceHandler : IReferenceHandler + { + private readonly Dictionary<(Type, Type), ReferenceHolder> _referenceHolders = new(); + + /// + public bool TryGetReference(TSource source, [NotNullWhen(true)] out TTarget? target) + where TSource : notnull + where TTarget : notnull + { + var refHolder = GetReferenceHolder(); + return refHolder.TryGetRef(source, out target); + } + + /// + public void SetReference(TSource source, TTarget target) + where TSource : notnull + where TTarget : notnull => GetReferenceHolder().SetRef(source, target); + + private ReferenceHolder GetReferenceHolder() + { + var mapping = (typeof(TSource), typeof(TTarget)); + if (_referenceHolders.TryGetValue(mapping, out var refHolder)) + return refHolder; + + return _referenceHolders[mapping] = new(); + } + + private sealed class ReferenceEqualityComparer : IEqualityComparer + { + // cannot use System.Collections.Generic.ReferenceEqualityComparer since it is not available in netstandard2.0 + + public static readonly IEqualityComparer Instance = new ReferenceEqualityComparer(); + + private ReferenceEqualityComparer() { } + + bool IEqualityComparer.Equals(T? x, T? y) => ReferenceEquals(x, y); + + int IEqualityComparer.GetHashCode(T obj) => RuntimeHelpers.GetHashCode(obj); + } + + private class ReferenceHolder + { + private readonly Dictionary _references = new(ReferenceEqualityComparer.Instance); + + public bool TryGetRef(TSource source, [NotNullWhen(true)] out TTarget? target) + where TSource : notnull + where TTarget : notnull + { + if (_references.TryGetValue(source, out var targetObj)) + { + target = (TTarget)targetObj; + return true; + } + + target = default; + return false; + } + + public void SetRef(TSource source, TTarget target) + where TSource : notnull + where TTarget : notnull + { + _references[source] = target; + } + } + } +} diff --git a/test/Riok.Mapperly.Tests/_snapshots/UseMapperTest.ReferenceHandling#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/UseMapperTest.ReferenceHandling#Mapper.g.verified.cs index f13d7d4b5f..a36e911dd0 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/UseMapperTest.ReferenceHandling#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/UseMapperTest.ReferenceHandling#Mapper.g.verified.cs @@ -3,12 +3,7 @@ #nullable enable public partial class Mapper { - private partial global::B Map(global::A source) - { - return MapToB(source, new global::Riok.Mapperly.Abstractions.ReferenceHandling.Internal.PreserveReferenceHandler()); - } - - private global::B MapToB(global::A source, global::Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler) + private partial global::B Map(global::A source, global::Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler) { if (refHandler.TryGetReference(source, out var existingTargetReference)) return existingTargetReference; diff --git a/test/Riok.Mapperly.Tests/_snapshots/UseMapperTest.ReferenceHandlingEnabledNoParameter#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/UseMapperTest.ReferenceHandlingEnabledNoParameter#Mapper.g.verified.cs index 7f67704952..5bd6c98315 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/UseMapperTest.ReferenceHandlingEnabledNoParameter#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/UseMapperTest.ReferenceHandlingEnabledNoParameter#Mapper.g.verified.cs @@ -3,12 +3,7 @@ #nullable enable public partial class Mapper { - private partial global::B Map(global::A source) - { - return MapToB(source, new global::Riok.Mapperly.Abstractions.ReferenceHandling.Internal.PreserveReferenceHandler()); - } - - private global::B MapToB(global::A source, global::Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler) + private partial global::B Map(global::A source, global::Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler) { if (refHandler.TryGetReference(source, out var existingTargetReference)) return existingTargetReference; diff --git a/test/Riok.Mapperly.Tests/_snapshots/UseStaticMapperTest.ReferenceHandling#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/UseStaticMapperTest.ReferenceHandling#Mapper.g.verified.cs index b6a32f857b..48cef839f5 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/UseStaticMapperTest.ReferenceHandling#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/UseStaticMapperTest.ReferenceHandling#Mapper.g.verified.cs @@ -3,12 +3,7 @@ #nullable enable public partial class Mapper { - private partial global::B Map(global::A source) - { - return MapToB(source, new global::Riok.Mapperly.Abstractions.ReferenceHandling.Internal.PreserveReferenceHandler()); - } - - private global::B MapToB(global::A source, global::Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler) + private partial global::B Map(global::A source, global::Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler) { if (refHandler.TryGetReference(source, out var existingTargetReference)) return existingTargetReference; diff --git a/test/Riok.Mapperly.Tests/_snapshots/UseStaticMapperTest.ReferenceHandlingEnabledNoParameter#Mapper.g.verified.cs b/test/Riok.Mapperly.Tests/_snapshots/UseStaticMapperTest.ReferenceHandlingEnabledNoParameter#Mapper.g.verified.cs index 0dc674e992..5a9bb3c474 100644 --- a/test/Riok.Mapperly.Tests/_snapshots/UseStaticMapperTest.ReferenceHandlingEnabledNoParameter#Mapper.g.verified.cs +++ b/test/Riok.Mapperly.Tests/_snapshots/UseStaticMapperTest.ReferenceHandlingEnabledNoParameter#Mapper.g.verified.cs @@ -3,12 +3,7 @@ #nullable enable public partial class Mapper { - private partial global::B Map(global::A source) - { - return MapToB(source, new global::Riok.Mapperly.Abstractions.ReferenceHandling.Internal.PreserveReferenceHandler()); - } - - private global::B MapToB(global::A source, global::Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler) + private partial global::B Map(global::A source, global::Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler refHandler) { if (refHandler.TryGetReference(source, out var existingTargetReference)) return existingTargetReference;