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 5, 2023
1 parent 4cd4e3a commit 752c00b
Show file tree
Hide file tree
Showing 81 changed files with 1,601 additions and 328 deletions.
1 change: 1 addition & 0 deletions .editorconfig
Expand Up @@ -211,6 +211,7 @@ indent_size = 2
# markdown
[*.{md,mdx}]
indent_size = unset
trim_trailing_whitespace = false

# Verify settings
[*.{received,verified}.*]
Expand Down
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.
13 changes: 11 additions & 2 deletions docs/docs/getting-started/installation.mdx
Expand Up @@ -12,26 +12,35 @@ 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 that projects referencing this project do not also get a reference to "Riok.Mapperly".
`ExcludeAssets="runtime"` ensures that the Mapperly .dll files are not copied to the build output (they are not needed 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,6 +2,7 @@
<PropertyGroup>
<IsPackable>false</IsPackable>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<MeziantouPolyfill_IncludedPolyfills>T:System.Diagnostics.CodeAnalysis.NotNullWhenAttribute</MeziantouPolyfill_IncludedPolyfills>
</PropertyGroup>

<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
92 changes: 92 additions & 0 deletions src/Riok.Mapperly.Templates/PreserveReferenceHandler.cs
@@ -0,0 +1,92 @@
using System;
using System.Collections.Generic;
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();

// disable nullability since older target frameworks
// may not support the NotNullWhenAttribute
#nullable disable
/// <inheritdoc cref="IReferenceHandler.TryGetReference{TSource,TTarget}"/>
public bool TryGetReference<TSource, TTarget>(TSource source, out TTarget target)
where TSource : notnull
where TTarget : notnull
{
var refHolder = GetReferenceHolder<TSource, TTarget>();
return refHolder.TryGetRef(source, out target);
}

#nullable enable

/// <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);

// disable nullability since older target frameworks
// may not support the NotNullWhenAttribute
#nullable disable
public bool TryGetRef<TSource, TTarget>(TSource source, 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;
}

#nullable enable

public void SetRef<TSource, TTarget>(TSource source, TTarget target)
where TSource : notnull
where TTarget : notnull
{
_references[source] = target;
}
}
}
}

0 comments on commit 752c00b

Please sign in to comment.