Skip to content

Existing target object mappers do not nest as expected #1484

Description

@OliverFreemanSIG

Describe the bug
When using nested mappers within a mapper that has an existing target object mapping method, the calls to the nested mapper do not use the nested mapper's existing target object mapper. If the nested mapper has an existing target object mapping method and the calling mapper's object has a non-null value for the nested object, it should use the nested mapper's existing target object mapping method.

Note: this is important to fix so that the tracked entity is changed when using EF Core rather than creating a new entity

Declaration code

using Riok.Mapperly.Abstractions;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

public sealed record HouseDto
{
    public int Id { get; init; }

    public required int NumWindows { get; init; }

    public DoorDto? Door { get; init; }
}

public sealed record DoorDto
{
    public int Id { get; init; }

    public required bool IsWood { get; init; }
}


public sealed class HouseEntity
{
    [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; init; }

    public required int NumWindows { get; set; }

    [ForeignKey(nameof(Door))]
    public int? DoorId { get; private set; }
    public DoorEntity? Door { get; set; }
}

public sealed class DoorEntity
{
    [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; init; }

    public required bool IsWood { get; set; }
}

[Mapper]
public sealed partial class HouseMapper
{
    [UseMapper]
    private readonly DoorMapper _doorMapper;

    public HouseMapper(DoorMapper doorMapper)
    {
        _doorMapper = doorMapper;
    }

    public partial HouseEntity DtoToEntity(HouseDto dto);

    public partial void DtoToEntity(HouseDto dto, HouseEntity entity);
}

[Mapper]
public sealed partial class DoorMapper
{
    public partial DoorEntity DtoToEntity(DoorDto dto);

    public partial void DtoToEntity(DoorDto dto, DoorEntity entity);
}

Actual relevant generated code

// <auto-generated />
#nullable enable
public sealed partial class HouseMapper
{
    [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "3.6.0.0")]
    public partial global::HouseEntity DtoToEntity(global::HouseDto dto)
    {
        var target = new global::HouseEntity()
        {
            Id = dto.Id,
            NumWindows = dto.NumWindows,
        };
        if (dto.Door != null)
        {
            target.Door = _doorMapper.DtoToEntity(dto.Door);
        }
        else
        {
            target.Door = null;
        }
        return target;
    }

    [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "3.6.0.0")]
    public partial void DtoToEntity(global::HouseDto dto, global::HouseEntity entity)
    {
        if (dto.Door != null)
        {
            entity.Door = _doorMapper.DtoToEntity(dto.Door);
        }
        else
        {
            entity.Door = null;
        }
        entity.NumWindows = dto.NumWindows;
    }
}

Expected relevant generated code

// <auto-generated />
#nullable enable
public sealed partial class HouseMapper
{
    ...
    public partial void DtoToEntity(global::HouseDto dto, global::HouseEntity entity)
    {
-       if (dto.Door != null)
+       if (dto.Door != null && entity.Door != null)
+       {
+           _doorMapper.DtoToEntity(dto.Door, entity.Door);
+       }
+       else if (dto.Door != null)
        {
            entity.Door = _doorMapper.DtoToEntity(dto.Door);
        }
        else
        {
            entity.Door = null;
        }
        entity.NumWindows = dto.NumWindows;
    }
}

Environment:

  • Mapperly Version: 3.6.0
  • Nullable reference types: enabled
  • .NET Version: 8.0.2
  • Target Framework: .net8.0
  • Compiler Version: 4.9.0-3.24121.1 (a98c90d5)
  • C# Language Version: 12.0
  • IDE: Visual Studio 2022
  • OS: Windows 10

Additional Info
It seems like this problem also exists when you don't specify the nested object's mapper. The generated code for mapping the nested object never uses the existing nested object on the target object. Instead, it always creates a new nested object. This should also be fixed

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions