從朋友那兒聽到了用 AutoMapper 把串列成員物件攤平成一組串列的問題,發現了 ConvertUsing 的好用,這邊就紀錄一下。

事情是這樣的,首先有一個 Parent 類別,其中包含著兩個成員:Id 和串列的 Child 類別,而 Child 類別則只有一個成員 Val,如下:

public class Parent
{
    public int Id { get; set; }
    public IEnumerable<Child> Children { get; set; }
}

public class Child
{
    public double Val { get; set; }
}

另外還有一個 Target 類別,包含 IdVal 兩個成員:

public class Target
{
    public int Id { get; set; }
    public double Val { get; set; }
}

現在的目標是:將一個有著 Child 串列的 Parent 映射成 Target 串列

也就是說,假設我們的來源是這樣子:

var boo = new Parent
{
    Id = 1,
    Children = new List<Child>
    {
        new Child { Val = 1 },
        new Child { Val = 2 },
    }
};

希望可以變成這樣子:

var expect = new List<Target>
{
    new Target { Id = 1, Val = 1 },
    new Target { Id = 1, Val = 2 },
};

我之前遇到的時候,會直覺地將 Child 直接 Map 到 Target,再對 Target 做個 Foreach 來補上 Parent 的 Id。

這次和朋友討論時,提到了另一個角度:雖然這樣的做法相當直覺快速,但其實並不能保證後續維護的人使用這組 Mappings 時,都知道這裡要補資料;況且此處的對應關係的確是 ParentList<Target>,並非 ChildTarget 而已,直覺上就怪怪的。若要解決這個問題,可能就要再包裝一層,把 Mapper 隔離出去做個轉換器之類的。

但想想又覺得 AutoMapper 不可能沒提供這個場景能使用的方法才對,最後餵狗發現 AutoMapper 確實有提供 ConvertUsing 來讓我們客製化轉換過程,這邊就紀錄一下。

在我們註冊映射關係的時候,可以用 ConvertUsing 來直接定義轉換的過程。以這個例子來說,就可以:

var config = new MapperConfiguration(cfg =>
{
    cfg
        .CreateMap<Parent, IEnumerable<Target>>()
        .ConvertUsing(parent => parent.Children.Select(child => new Target
        {
            Id = parent.Id,
            Val = child.Val
        }));
});

像這樣告訴 AutoMapper:「從 Parent 轉換到 IEnumerable<Target> 的時候,幫我用 Children 來 Select 出 Target」

註冊後就可以順利進行轉換了,讓我們直接用 Linqpad 試試:

Image

如果是比較複雜的狀況,也可以用實作型別轉換器ITypeConverter)的方式來處理。以這個例子來說,就可以:

// c實作從 Parent 轉換到 IEnumerable<Target> 的 ITypeConverter
public class ParentToTargetsTypeConverter : ITypeConverter<Parent, IEnumerable<Target>>
{
    // 轉換方法本體
    public IEnumerable<Target> Convert(
        Parent parent,
        IEnumerable<Target> targets,
        ResolutionContext context)
    {
        targets = parent.Children.Select(child => new Target
        {
            Id = parent.Id,
            Val = child.Val
        });
        return targets;
    }
}

接著再把轉換器註冊到 ConvertUsing 上,丟實體或是泛型都行:

var config = new MapperConfiguration(cfg =>
{
        //cfg
        //    .CreateMap<Parent, IEnumerable<Target>>()
        //    .ConvertUsing(new ParentToTargetsTypeConverter());

        // OR

        cfg
            .CreateMap<Parent, IEnumerable<Target>>()
            .ConvertUsing<ParentToTargetsTypeConverter>();
});

測試轉換:

Image

之後當遇到型別轉換的過程並非單純欄位一對一的時候,就可以使用 ConvertUsing 的方式來自行處理。

最後附上測試用的 Linqpad Code 方便我之後回來複製

void Main()
{
    Sut().Dump();
}

public IEnumerable<Target> Sut()
{
    var boo = new Parent
    {
        Id = 1,
        Children = new List<Child>
        {
            new Child { Val = 1 },
            new Child { Val = 2 },
        }
    };

    var mapper = CreateMapperConfig().CreateMapper();
    var result = mapper.Map<Parent, IEnumerable<Target>>(boo);
    return result;
}

public MapperConfiguration CreateMapperConfig()
{
    var config = new MapperConfiguration(cfg =>
    {
        // 1. 使用 Lambda
        
        //cfg
        //    .CreateMap<Parent, IEnumerable<Target>>()
        //    .ConvertUsing(parent => parent.Children.Select(child => new Target
        //    {
        //        Id = parent.Id,
        //        Val = child.Val
        //    }));
        
        // 2. 使用 TypeConverter
        
        //cfg
        //    .CreateMap<Parent, IEnumerable<Target>>()
        //    .ConvertUsing(new ParentToTargetsTypeConverter());

        cfg
            .CreateMap<Parent, IEnumerable<Target>>()
            .ConvertUsing<ParentToTargetsTypeConverter>();
    });
    return config;
}

public class ParentToTargetsTypeConverter : ITypeConverter<Parent, IEnumerable<Target>>
{
    public IEnumerable<Target> Convert(
        Parent parent,
        IEnumerable<Target> targets,
        ResolutionContext context)
    {
        targets = parent.Children.Select(child => new Target
        {
            Id = parent.Id,
            Val = child.Val
        });
        return targets;
    }
}

public class Parent
{
    public int Id { get; set; }
    public IEnumerable<Child> Children { get; set; }
}

public class Child
{
    public double Val { get; set; }
}

public class Target
{
    public int Id { get; set; }
    public double Val { get; set; }
}

參考資料

相關文章