.NET 8 zero overhead private member mapping
See original GitHub issuePreviously, Mapperly hasn’t had the option to access or set private members. Although private/hidden members have always been visible to source generators, there hasn’t been a suitable way of accessing them. While workarounds such as Reflection.Emit
, Linq.Expressions
and reflection are available, upon closer inspection, they are insufficient for our needs due to poor performance and incompatibilities in AOT usage. For these reasons, Mapperly has never added private member mapping.
With the release of .NET 8, the UnsafeAccessorAttribute
will be added. This supports code that accesses internal methods, constructors, fields, and properties with zero overhead while being AOT compatible. By applying it to an extern static method and configuring it, the runtime will attempt to find the corresponding field or method, to which the call will be forwarded.
Using the new attribute, Mapperly could add support for private member mapping, init-only/private setter mapping, and support private constructors.
Private member mapping
Enabling
Private member mapping could be enabled by default, although this would likely lead to nasty unexpected behaviour while breaking backwards compatibility. Instead I suggest that a property EnablePrivateMapping
be added to [Mapper]
and a method attribute [EnablePrivateMapping]
be added. Alternatively private member mapping could always be enabled but only for explicit MapProperty
mappings.
Generated code
The private access methods can be added as additional methods to the mapping class. These could be implemented like a MethodMapping
and used like so: SetId(target, GetId(source))
.
Alternatively, to make the code easier to read, the private accessors could be added to a file scoped static accessor class. This class way the methods could be implemented as extensions methods, with the resulting code reading left to right. target.SetId(source.GetId())
.
target.Tires.SetSpareWheel(source.Tires.GetSpareWheel())
vs SetSpareWheel(target.Tires, GetSpareWheel(source.Tires))
Example
Mapper
public class Car
{
private Tire _spareWheel { get; set; }
// ...
}
public class CarDto
{
private TireDto _spareWheel { get; set; }
// ...
}
[Mapper(EnumMappingStrategy = EnumMappingStrategy.ByName)]
public static partial class CarMapper
{
[MapProperty("_spareWheel", "_spareWheel")]
[MapProperty(nameof(Car.Manufacturer), nameof(CarDto.Producer))] // Map property with a different name in the target type
public static partial CarDto MapCarToDto(Car car);
}
Generated Code
static file class
{
[UnsafeAccessor(UnsafeAccessorKind.Method, Name="set__spareWheel")]
public static extern Tire GetSpareWheel(this Car source)
[UnsafeAccessor(UnsafeAccessorKind.Method, Name="set__spareWheel")]
public static extern void SetSpareWheel(this CarDto target, TireDto tireDto)
}
public static partial class CarMapper
{
public static partial CarDto MapCarToDto(Car car)
{
target.SetSpareWheel(MapToTireDto(car.GetSpareWheel()));
// ...
}
}
Issue Analytics
- State:
- Created 2 months ago
- Comments:5 (3 by maintainers)
Top GitHub Comments
@TimothyMakkison the idea of the Accessible flag is to control whether
UnsafeAccessorAttribute
s are used or only members which are accessible by the mapper are considered.I don’t think InternalsVisibleTo affects a lot of people and there is an easy workaround with ignore attributes. IMO the enum would help for sure… We could introduce it and just report a diagnostic if the Accessible flag is not set until Mapperly supports the new Roslyn version and
UnsafeAccessorAttribute
.We’ll release #597 as breaking change as it is actually a breaking change and we want to conform to the semantic release specification. I prepared #611 and #612 for this. The upgrade procedere for most of the users should be just as simple as upgrading to a new feature release.
Yeah that makes this much easier 😅
I might try the separate static
UnsafeAccessor
class first. I think that extension methods will be more idiomatic and easier to read.👍
Do you think that private/internal mapping should be enabled by default? Wouldn’t this be a massive breaking change?