Skip to content

[MapperConstructor] does not override copy constructor selection in UseDeepCloning mode #2196

@YoelDruxman

Description

@YoelDruxman

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:

  1. Class with explicit copy constructor[MapperConstructor] on the parameterless constructor is ignored; the copy constructor is used instead.
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions