Please do the checklist before filing an issue:
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 (via UnsafeAccessor) 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.
Please do the checklist before filing an issue:
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:[MapperConstructor]on the parameterless constructor is ignored; the copy constructor is used instead.[MapperConstructor]on a parameterless constructor is ignored; the record's compiler-generated copy constructor (viaUnsafeAccessor) is used instead.Declaration code
Case 1 — Class:
Case 2 — Record (same mapper):
Actual relevant generated code
Case 1 (class):
Case 2 (record):
Expected relevant generated code
Both cases should respect
[MapperConstructor]and produce:Reported relevant diagnostics
[MapperConstructor]or[MapProperty]annotations are being ignored.Environment (please complete the following information):
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.