Skip to content

Commit

Permalink
feat: support private constructors (#1405)
Browse files Browse the repository at this point in the history
  • Loading branch information
latonz authored Aug 8, 2024
1 parent 2654e9a commit 1c8b383
Show file tree
Hide file tree
Showing 89 changed files with 1,270 additions and 700 deletions.
115 changes: 0 additions & 115 deletions docs/docs/configuration/private-member-mapping.md

This file was deleted.

114 changes: 114 additions & 0 deletions docs/docs/configuration/private-members.mdx
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);
}
```
5 changes: 5 additions & 0 deletions src/Riok.Mapperly.Abstractions/MapperAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,11 @@ public class MapperAttribute : Attribute
/// </summary>
public MemberVisibility IncludedMembers { get; set; } = MemberVisibility.AllAccessible;

/// <summary>
/// Determines the access level of constructors that Mapperly will take into account.
/// </summary>
public MemberVisibility IncludedConstructors { get; set; } = MemberVisibility.AllAccessible;

/// <summary>
/// Controls the priority of constructors used in mapping.
/// When <c>true</c>, a parameterless constructor is prioritized over constructors with parameters.
Expand Down
2 changes: 2 additions & 0 deletions src/Riok.Mapperly.Abstractions/PublicAPI.Shipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -186,3 +186,5 @@ Riok.Mapperly.Abstractions.MappingTargetAttribute
Riok.Mapperly.Abstractions.MappingTargetAttribute.MappingTargetAttribute() -> void
Riok.Mapperly.Abstractions.MapPropertyAttribute.MapPropertyAttribute(string! source, string![]! target) -> void
Riok.Mapperly.Abstractions.MapPropertyAttribute.MapPropertyAttribute(string![]! source, string! target) -> void
Riok.Mapperly.Abstractions.MapperAttribute.IncludedConstructors.get -> Riok.Mapperly.Abstractions.MemberVisibility
Riok.Mapperly.Abstractions.MapperAttribute.IncludedConstructors.set -> void
3 changes: 3 additions & 0 deletions src/Riok.Mapperly/Configuration/MapperConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ public record MapperConfiguration
/// </summary>
public MemberVisibility? IncludedMembers { get; init; }

/// <inheritdoc cref="MapperAttribute.IncludedConstructors"/>
public MemberVisibility? IncludedConstructors { get; init; }

/// <summary>
/// Controls the priority of constructors used in mapping.
/// When <c>true</c>, a parameterless constructor is prioritized over constructors with parameters.
Expand Down
3 changes: 3 additions & 0 deletions src/Riok.Mapperly/Configuration/MapperConfigurationMerger.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ public static MapperAttribute Merge(MapperConfiguration mapperConfiguration, Map
mapper.IncludedMembers =
mapperConfiguration.IncludedMembers ?? defaultMapperConfiguration.IncludedMembers ?? mapper.IncludedMembers;

mapper.IncludedConstructors =
mapperConfiguration.IncludedConstructors ?? defaultMapperConfiguration.IncludedConstructors ?? mapper.IncludedConstructors;

mapper.PreferParameterlessConstructors =
mapperConfiguration.PreferParameterlessConstructors
?? defaultMapperConfiguration.PreferParameterlessConstructors
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ MapperConfiguration defaultMapperConfiguration
{
_dataAccessor = dataAccessor;
_types = types;

var mapperConfiguration = _dataAccessor.AccessSingle<MapperAttribute, MapperConfiguration>(mapperSymbol);
var mapper = MapperConfigurationMerger.Merge(mapperConfiguration, defaultMapperConfiguration);

Expand Down
24 changes: 24 additions & 0 deletions src/Riok.Mapperly/Descriptors/Constructors/IInstanceConstructor.cs
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 src/Riok.Mapperly/Descriptors/Constructors/InstanceConstructor.cs
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);
}
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);
}
}
Loading

0 comments on commit 1c8b383

Please sign in to comment.