Skip to content

Commit

Permalink
feat: Remove runtime dependency on Riok.Mapperly.Abstractions
Browse files Browse the repository at this point in the history
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 riok#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.
  • Loading branch information
latonz committed Sep 4, 2023
1 parent a63035b commit d231ac3
Show file tree
Hide file tree
Showing 80 changed files with 1,561 additions and 328 deletions.
3 changes: 3 additions & 0 deletions Directory.Build.props
@@ -1,6 +1,9 @@
<Project>

<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<DebugSymbols>true</DebugSymbols>
<DebugType>embedded</DebugType>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<Version>0.0.1-dev</Version>
Expand Down
14 changes: 14 additions & 0 deletions Riok.Mapperly.sln
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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}
Expand Down
Expand Up @@ -2,10 +2,6 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<DebugSymbols>true</DebugSymbols>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
</PropertyGroup>
Expand Down
24 changes: 15 additions & 9 deletions docs/docs/contributing/architecture.md
Expand Up @@ -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

Expand All @@ -24,19 +29,20 @@ 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.
To create a mapping from one type to another, the `DescriptorBuilder` loops through a set of `*MappingBuilder`s.
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,
Expand Down
1 change: 1 addition & 0 deletions docs/docs/contributing/common-tasks.md
Expand Up @@ -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.
14 changes: 12 additions & 2 deletions docs/docs/getting-started/installation.mdx
Expand Up @@ -12,26 +12,36 @@ 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).

<!-- do not indent this, it won't work, https://stackoverflow.com/a/67579641/3302887 -->

<Tabs>
<TabItem value="csproj" label="PackageReference" default>
<CodeBlock language="xml">{`<PackageReference Include="Riok.Mapperly" Version="${useDocusaurusContext().siteConfig.customFields.mapperlyVersion}" />`}</CodeBlock>
<CodeBlock language="xml">{`<PackageReference Include="Riok.Mapperly" Version="${useDocusaurusContext().siteConfig.customFields.mapperlyVersion}" PrivateAssets="all" ExcludeAssets="runtime" />`}</CodeBlock>
</TabItem>
<TabItem value="dontet-cli" label=".NET CLI">

```bash
dotnet add package Riok.Mapperly
```

Make sure to set `PrivateAssets="all" ExcludeAssets="runtime"` on the added `PackageReference`.

</TabItem>
<TabItem value="pkg-manager" label="Package Manager">

```powershell
Install-Package Riok.Mapperly
```

Make sure to set `PrivateAssets="all" ExcludeAssets="runtime"` on the added `PackageReference`.

</TabItem>
</Tabs>

:::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).
:::
1 change: 1 addition & 0 deletions docs/docs/intro.md
Expand Up @@ -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
Expand Down
2 changes: 0 additions & 2 deletions samples/Riok.Mapperly.Sample/Riok.Mapperly.Sample.csproj
Expand Up @@ -2,8 +2,6 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>

<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
</PropertyGroup>

Expand Down
1 change: 0 additions & 1 deletion src/Directory.Build.props
Expand Up @@ -4,7 +4,6 @@

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<DebugType>embedded</DebugType>
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
<EmbedUntrackedSources>true</EmbedUntrackedSources>

Expand Down
4 changes: 0 additions & 4 deletions src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt
Expand Up @@ -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, TTarget>(TSource source, TTarget target) -> void
Riok.Mapperly.Abstractions.ReferenceHandling.Internal.PreserveReferenceHandler.TryGetReference<TSource, TTarget>(TSource source, out TTarget? target) -> bool
Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler
Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler.SetReference<TSource, TTarget>(TSource source, TTarget target) -> void
Riok.Mapperly.Abstractions.ReferenceHandling.IReferenceHandler.TryGetReference<TSource, TTarget>(TSource source, out TTarget? target) -> bool
Expand Down

This file was deleted.

Expand Up @@ -2,10 +2,12 @@
<PropertyGroup>
<IsPackable>false</IsPackable>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<MeziantouPolyfill_IncludedPolyfills>T:System.Diagnostics.CodeAnalysis.NotNullWhenAttribute</MeziantouPolyfill_IncludedPolyfills>
</PropertyGroup>

<ItemGroup>
<Using Include="Riok.Mapperly.Abstractions" />
<Using Include="Riok.Mapperly.Abstractions.ReferenceHandling" />
</ItemGroup>

<ItemGroup>
Expand Down
3 changes: 3 additions & 0 deletions 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
89 changes: 89 additions & 0 deletions src/Riok.Mapperly.Templates/PreserveReferenceHandler.cs
@@ -0,0 +1,89 @@
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
{
/// <summary>
/// A <see cref="IReferenceHandler"/> 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.
/// </summary>
internal sealed class PreserveReferenceHandler : IReferenceHandler
{
private readonly Dictionary<(Type, Type), ReferenceHolder> _referenceHolders = new();

/// <inheritdoc cref="IReferenceHandler.TryGetReference{TSource,TTarget}"/>
#if NETSTANDARD2_0 || NETFRAMEWORK || NETCOREAPP2X
#nullable disable
public bool TryGetReference<TSource, TTarget>(TSource source, out TTarget target)
#nullable enable
#else
public bool TryGetReference<TSource, TTarget>(TSource source, [NotNullWhen(true)] out TTarget? target)
#endif
where TSource : notnull
where TTarget : notnull
{
var refHolder = GetReferenceHolder<TSource, TTarget>();
return refHolder.TryGetRef(source, out target);
}

/// <inheritdoc cref="IReferenceHandler.SetReference{TSource,TTarget}"/>
public void SetReference<TSource, TTarget>(TSource source, TTarget target)
where TSource : notnull
where TTarget : notnull => GetReferenceHolder<TSource, TTarget>().SetRef(source, target);

private ReferenceHolder GetReferenceHolder<TSource, TTarget>()
{
var mapping = (typeof(TSource), typeof(TTarget));
if (_referenceHolders.TryGetValue(mapping, out var refHolder))
return refHolder;

return _referenceHolders[mapping] = new();
}

private sealed class ReferenceEqualityComparer<T> : IEqualityComparer<T>
{
// cannot use System.Collections.Generic.ReferenceEqualityComparer since it is not available in netstandard2.0

public static readonly IEqualityComparer<T> Instance = new ReferenceEqualityComparer<T>();

private ReferenceEqualityComparer() { }

bool IEqualityComparer<T>.Equals(T? x, T? y) => ReferenceEquals(x, y);

int IEqualityComparer<T>.GetHashCode(T obj) => RuntimeHelpers.GetHashCode(obj);
}

private class ReferenceHolder
{
private readonly Dictionary<object, object> _references = new(ReferenceEqualityComparer<object>.Instance);

public bool TryGetRef<TSource, TTarget>(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, TTarget>(TSource source, TTarget target)
where TSource : notnull
where TTarget : notnull
{
_references[source] = target;
}
}
}
}
42 changes: 42 additions & 0 deletions src/Riok.Mapperly.Templates/Riok.Mapperly.Templates.csproj
@@ -0,0 +1,42 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>

<!--
AssemblyName is replaced by the source generator
with the name of the assembly of the target project.
This is done to reduce type name collisions when
InternalsVisibleTo is used in another project,
which needs the same generated templated type.
-->
<RootNamespace>Riok.Mapperly.Internal.AssemblyName</RootNamespace>

<!--
disable nullable and enable it for each file individually,
as the target application may not have nullables enabled
-->
<Nullable>disable</Nullable>

<!-- disable global usings as the target application may not have them enabled -->
<ImplicitUsings>disable</ImplicitUsings>

<!--
do not use polyfills for the templates,
since we only copy the cs files in the directory and we do not know
whether the polyfills would be required in the target application.
-->
<MeziantouPolyfill_ExcludedPolyfills>*</MeziantouPolyfill_ExcludedPolyfills>
</PropertyGroup>

<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
<_Parameter1>Riok.Mapperly.Templates.Tests</_Parameter1>
</AssemblyAttribute>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Riok.Mapperly.Abstractions\Riok.Mapperly.Abstractions.csproj"/>
</ItemGroup>

</Project>

0 comments on commit d231ac3

Please sign in to comment.