Skip to content

Bug Report: Mapperly generates incomplete mapping for List<T> derived types #2152

@gkardava

Description

@gkardava

Mapperly generates incorrect mapping code when mapping from a custom type that derives from List to List. Instead of copying collection items, the generated mapper only copies the Capacity property, resulting in an empty collection with reserved capacity.
Environment

Mapperly Version: 4.3.1.0
Target Framework: [NET 8.0]

Source Code
Custom Collection Type:
Custom collection type derived from List

public class ScJsonList<T> : List<T>
{
}

Source Model:

public class SCWalletDefinition
{
    public int Id { get; set; }
    public string Name { get; set; }
    // ... other properties
    public ScJsonList<string> Currencies { get; set; }
}

Target Model:

public class WalletDefinitionView
{
    public int Id { get; set; }
    public string Name { get; set; }
    // ... other properties
    public List<string> Currencies { get; set; }
}
// Mapper Configuration:
[assembly: MapperDefaults(EnabledConversions = MappingConversionType.None)]

```csharp

public static partial class SCWalletDefinitionMapper
{
    public static partial WalletDefinitionView ToWalletDefinitionView(
        this SCWalletDefinition req);
}

Project Configuration:

<PropertyGroup>
  <WarningsAsErrors>RMG020;RMG012;RMG037;RMG038;...</WarningsAsErrors>
</PropertyGroup>

Generated Code (Incorrect)

public static partial class SCWalletDefinitionMapper
{
    [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "4.3.1.0")]
    public static partial global::Dto.Response.WalletDefinitionView ToWalletDefinitionView(this global::DbRepo.Models.SCWalletDefinition req)
    {
        var target = new global::Dto.Response.WalletDefinitionView();
        target.Id = req.Id;
        target.Name = req.Name;
// ...
        target.Currencies = MapToListOfString(req.Currencies);
        return target;
    }

    [global::System.CodeDom.Compiler.GeneratedCode("Riok.Mapperly", "4.3.1.0")]
    private static global::System.Collections.Generic.List<string> MapToListOfString(global::DbRepo.DapperMapping.ScJsonList<string> source)
    {
        var target = new global::System.Collections.Generic.List<string>();
        target.Capacity = source.Capacity; 
        return target; 
    }
}

Expected Behavior
Since ScJsonList derives from List, the mapping should either:

Recognize the inheritance and copy all items:

private static List<string> MapToListOfString(ScJsonList<string> source)
{
    return new List<string>(source); // Uses List<T> copy constructor
}

Or:

private static List<string> MapToListOfString(ScJsonList<string> source)
{
    var target = new List<string>(source.Count);
    foreach (var item in source)
    {
        target.Add(item);
    }
    return target;
}

Emit a warning when mapping between derived and base collection types with EnabledConversions = MappingConversionType.None, such as:

RMG038: Could not create complete mapping for member 'List Currencies'. Derived collection type 'ScJsonList' requires explicit conversion or enabled collection conversions.

Respect enabled conversions - perhaps the MappingConversionType.None setting is preventing proper collection handling, but this should still generate a warning rather than silently producing broken code.

Actual Behavior

No warnings or errors are emitted during build (even with RMG020, RMG012, RMG037, RMG038 set as errors)
Generated code successfully compiles
Runtime behavior: Currencies property contains an empty list (Count = 0) even when source has items
Silent data loss occurs without any indication

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