-
-
Notifications
You must be signed in to change notification settings - Fork 148
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: support private constructors (#1405)
- Loading branch information
Showing
89 changed files
with
1,270 additions
and
700 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
--- | ||
sidebar_position: 14 | ||
description: Private members | ||
--- | ||
|
||
import Tabs from '@theme/Tabs'; | ||
import TabItem from '@theme/TabItem'; | ||
|
||
# Private members | ||
|
||
As of .NET 8.0, Mapperly supports mapping members that are normally inaccessible like `private` or `protected` properties. | ||
This is made possible by using the [UnsafeAccessorAttribute](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.unsafeaccessorattribute) which lets Mapperly access normally inaccessible members with zero overhead while being completely AOT safe. | ||
|
||
By default `IncludedMembers` and `IncludedConstructors` is set to `MemberVisibility.AllAccessible` which will configure Mapperly to map members of all accessibility levels as long as they are ordinarily accessible. | ||
To enable unsafe accessor usage, set `IncludedMembers` and/or `IncludedConstructors` to `MemberVisibility.All`. | ||
Mapperly will then try to map members of all accessibilities, including ones that are not usually visible to external types. | ||
|
||
`IncludedConstructors` can be used separately from `IncludedMembers`. | ||
This allows you to use inaccessible constructors but only map accessible members or vice versa. | ||
|
||
<Tabs> | ||
<TabItem value="declaration" label="Declaration" default> | ||
```csharp | ||
// highlight-start | ||
[Mapper( | ||
IncludedMembers = MemberVisibility.All, | ||
IncludedConstructors = MemberVisibility.All)] | ||
// highlight-end | ||
public partial class FruitMapper | ||
{ | ||
public partial FruitDto ToDto(Fruit source); | ||
} | ||
|
||
public class Fruit | ||
{ | ||
private bool _isSeeded; | ||
|
||
public string Name { get; set; } | ||
|
||
private int Sweetness { get; set; } | ||
} | ||
|
||
public class FruitDto | ||
{ | ||
private FruitDto() {} | ||
|
||
private bool _isSeeded; | ||
|
||
public string Name { get; set; } | ||
|
||
private int Sweetness { get; set; } | ||
} | ||
``` | ||
|
||
</TabItem> | ||
<TabItem label="Generated code" value="generated"> | ||
Mapperly generates a file scoped class containing an accessor method for each member which cannot be accessed directly. | ||
Mapperly then uses these methods to create the instance, get and set the members as needed. | ||
Note that this uses zero reflection and is as performant as using an ordinary property or field. | ||
|
||
```csharp | ||
public partial class FruitMapper | ||
{ | ||
private partial global::FruitDto ToDto(global::Fruit source) | ||
{ | ||
var target = UnsafeAccessor.CreateFruitDto(); | ||
target.GetIsSeeded1() = source.GetIsSeeded(); | ||
target.Name = source.Name; | ||
target.SetSweetness(source.GetSweetness()); | ||
return target; | ||
} | ||
} | ||
|
||
static file class UnsafeAccessor | ||
{ | ||
[global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Constructor)] | ||
public static extern global::FruitDto CreateFruitDto(this global::FruitDto target); | ||
|
||
[global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Field, Name = "_isSeeded")] | ||
public static extern ref bool GetSeeded(this global::Fruit target); | ||
|
||
[global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Field, Name = "_isSeeded")] | ||
public static extern ref bool GetSeeded1(this global::FruitDto target); | ||
|
||
[global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_Sweetness")] | ||
public static extern int GetSweetness(this global::Fruit source); | ||
|
||
[global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_Sweetness")] | ||
public static extern void SetSweetness(this global::FruitDto target, int value); | ||
} | ||
``` | ||
|
||
</TabItem> | ||
</Tabs> | ||
|
||
## Controlling member accessibility | ||
|
||
In addition to mapping inaccessible members, | ||
`MemberVisbility` can be used to control which members are considered, depending on their accessibility modifier. | ||
For instance `MemberVisibility.Private | MemberVisibility.Protected` will cause Mapperly to only consider private and protected members, | ||
generating an unsafe accessor if needed. | ||
|
||
`IncludedConstructors` can be used separately from `IncludedMembers`. | ||
This allows you to use inaccessible constructors but only map accessible members or vice versa. | ||
|
||
```csharp | ||
// highlight-start | ||
[Mapper(IncludedMembers = MemberVisibility.Private | MemberVisibility.Protected)] | ||
// highlight-end | ||
public partial class FruitMapper | ||
{ | ||
public partial FruitDto ToDto(Fruit source); | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
24 changes: 24 additions & 0 deletions
24
src/Riok.Mapperly/Descriptors/Constructors/IInstanceConstructor.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
using Microsoft.CodeAnalysis.CSharp.Syntax; | ||
using Riok.Mapperly.Descriptors.Mappings; | ||
|
||
namespace Riok.Mapperly.Descriptors.Constructors; | ||
|
||
/// <summary> | ||
/// An instance constructor represents code-to-be-generated | ||
/// which creates a new object instance. | ||
/// This could happen by calling a C# instance constructor, | ||
/// an unsafe accessor or by calling an object factory. | ||
/// </summary> | ||
public interface IInstanceConstructor | ||
{ | ||
/// <summary> | ||
/// Whether this constructor supports object initialization blocks to initialize properties. | ||
/// </summary> | ||
bool SupportsObjectInitializer { get; } | ||
|
||
ExpressionSyntax CreateInstance( | ||
TypeMappingBuildContext ctx, | ||
IEnumerable<ArgumentSyntax> args, | ||
InitializerExpressionSyntax? initializer = null | ||
); | ||
} |
17 changes: 17 additions & 0 deletions
17
src/Riok.Mapperly/Descriptors/Constructors/InstanceConstructor.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
using Microsoft.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.CSharp.Syntax; | ||
using Riok.Mapperly.Descriptors.Mappings; | ||
using Riok.Mapperly.Emit.Syntax; | ||
|
||
namespace Riok.Mapperly.Descriptors.Constructors; | ||
|
||
public class InstanceConstructor(INamedTypeSymbol type) : IInstanceConstructor | ||
{ | ||
public bool SupportsObjectInitializer => true; | ||
|
||
public ExpressionSyntax CreateInstance( | ||
TypeMappingBuildContext ctx, | ||
IEnumerable<ArgumentSyntax> args, | ||
InitializerExpressionSyntax? initializer = null | ||
) => SyntaxFactoryHelper.CreateInstance(type, args).WithInitializer(initializer); | ||
} |
72 changes: 72 additions & 0 deletions
72
src/Riok.Mapperly/Descriptors/Constructors/InstanceConstructorExtensions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
using Microsoft.CodeAnalysis.CSharp.Syntax; | ||
using Riok.Mapperly.Descriptors.Mappings; | ||
using Riok.Mapperly.Descriptors.Mappings.MemberMappings; | ||
using Riok.Mapperly.Emit; | ||
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; | ||
|
||
namespace Riok.Mapperly.Descriptors.Constructors; | ||
|
||
public static class InstanceConstructorExtensions | ||
{ | ||
public static ExpressionSyntax CreateInstance(this IInstanceConstructor ctor, TypeMappingBuildContext ctx) => | ||
ctor.CreateInstance(ctx, []); | ||
|
||
public static ExpressionSyntax CreateInstance( | ||
this IInstanceConstructor ctor, | ||
TypeMappingBuildContext ctx, | ||
IEnumerable<ExpressionSyntax> args | ||
) => ctor.CreateInstance(ctx, args.Select(Argument)); | ||
|
||
public static IEnumerable<StatementSyntax> CreateTargetInstance( | ||
this IInstanceConstructor ctor, | ||
TypeMappingBuildContext ctx, | ||
IMapping mapping, | ||
string targetVariableName, | ||
bool enableReferenceHandling, | ||
IReadOnlyCollection<ConstructorParameterMapping> ctorParametersMappings, | ||
IReadOnlyCollection<MemberAssignmentMapping>? initMemberMappings = null | ||
) | ||
{ | ||
if (enableReferenceHandling) | ||
{ | ||
// TryGetReference | ||
yield return ReferenceHandlingSyntaxFactoryHelper.TryGetReference(ctx, mapping); | ||
} | ||
|
||
// new T(ctorArgs) { ... }; | ||
var objectCreationExpression = ctor.CreateInstance(ctx, ctorParametersMappings, initMemberMappings); | ||
|
||
// var target = new T() { ... }; | ||
yield return ctx.SyntaxFactory.DeclareLocalVariable(targetVariableName, objectCreationExpression); | ||
|
||
// set the reference as soon as it is created, | ||
// as property mappings could refer to the same instance. | ||
if (enableReferenceHandling) | ||
{ | ||
// SetReference | ||
yield return ctx.SyntaxFactory.ExpressionStatement( | ||
ReferenceHandlingSyntaxFactoryHelper.SetReference(mapping, ctx, IdentifierName(targetVariableName)) | ||
); | ||
} | ||
} | ||
|
||
public static ExpressionSyntax CreateInstance( | ||
this IInstanceConstructor ctor, | ||
TypeMappingBuildContext ctx, | ||
IReadOnlyCollection<ConstructorParameterMapping> ctorParametersMappings, | ||
IReadOnlyCollection<MemberAssignmentMapping>? initMemberMappings = null | ||
) | ||
{ | ||
InitializerExpressionSyntax? initializer = null; | ||
if (initMemberMappings is { Count: > 0 }) | ||
{ | ||
var initPropertiesContext = ctx.AddIndentation(); | ||
var initMappings = initMemberMappings.Select(x => x.BuildExpression(initPropertiesContext, null)).ToArray(); | ||
initializer = ctx.SyntaxFactory.ObjectInitializer(initMappings); | ||
} | ||
|
||
// new T(ctorArgs) { ... }; | ||
var ctorArgs = ctorParametersMappings.Select(x => x.BuildArgument(ctx)).ToArray(); | ||
return ctor.CreateInstance(ctx, ctorArgs, initializer); | ||
} | ||
} |
Oops, something went wrong.