-
-
Notifications
You must be signed in to change notification settings - Fork 215
Description
Please do the checklist before filing an issue:
- I have read the documentation, including the FAQ
- I can reproduce the bug using the latest prerelease version (tested with 4.2.1; have not yet verified against v5.0.0-next.3)
- I have searched existing discussion and issue to avoid duplicates
Describe the bug
When UseDeepCloning = true, Mapperly selects copy constructors (explicit or record-synthesized) over constructors marked with [MapperConstructor]. This causes [MapProperty(Use = ...)] annotations to be silently ignored, since the copy constructor bypasses property-by-property mapping entirely.
[MapperConstructor] should be the definitive override for constructor selection, but it is not respected in at least two cases:
- Class with explicit copy constructor —
[MapperConstructor]on the parameterless constructor is ignored; the copy constructor is used instead. - Record with synthesized copy constructor —
[MapperConstructor]on a parameterless constructor is ignored; the record's compiler-generated copy constructor (viaUnsafeAccessor) is used instead.
Declaration code
Case 1 — Class:
public class Document
{
[MapperConstructor]
public Document() { }
public Document(Document other)
{
Title = other.Title;
AuthorId = other.AuthorId;
}
public required string Title { get; init; }
public string? AuthorId { get; init; }
}
[Mapper(UseDeepCloning = true)]
public partial class DocumentCloner
{
[MapProperty(nameof(Document.AuthorId), nameof(Document.AuthorId), Use = nameof(RemapId))]
[MapProperty(nameof(Document.Title), nameof(Document.Title), Use = nameof(Transform))]
public partial Document Clone(Document source);
[UserMapping(Default = false)]
private string? RemapId(string? id) => id is null ? null : $"remapped-{id}";
[UserMapping(Default = false)]
private string Transform(string value) => $"transformed-{value}";
}Case 2 — Record (same mapper):
public record Document
{
[MapperConstructor]
public Document() { }
public required string Title { get; init; }
public string? AuthorId { get; init; }
}Actual relevant generated code
Case 1 (class):
public partial Document Clone(Document source)
{
return new Document(source); // Copy constructor used — [MapperConstructor] and [MapProperty] ignored
}Case 2 (record):
public partial Document Clone(Document source)
{
return UnsafeAccessor.CreateDocument(source); // Record copy constructor via UnsafeAccessor — same problem
}Expected relevant generated code
Both cases should respect [MapperConstructor] and produce:
public partial Document Clone(Document source)
{
var target = new Document() // [MapperConstructor] constructor used
{
Title = Transform(source.Title), // [MapProperty] applied
AuthorId = RemapId(source.AuthorId) // [MapProperty] applied
};
return target;
}Reported relevant diagnostics
- No diagnostics reported — the bug is silent. No warnings that
[MapperConstructor]or[MapProperty]annotations are being ignored.
Environment (please complete the following information):
- Mapperly Version: 4.2.1
- Nullable reference types: enabled
- .NET Version: .NET 8.0
- Target Framework: net8.0
- C# Language Version: preview
- IDE: JetBrains Rider
- OS: Windows 11
Additional context
This makes it impossible to use [MapProperty(Use = ...)] for property transformations during deep cloning on any type that has a copy constructor. The workaround is to remove the copy constructor entirely, which is not possible for records (synthesized copy constructor cannot be removed).
A possible fix: when [MapperConstructor] is present on a constructor, it should take absolute priority over any copy constructor, including record-synthesized ones. At minimum, a diagnostic warning should be emitted when [MapProperty] annotations are present but will be ignored due to copy constructor selection.