使用 AutoMapper 转换为只读集合

Convert to readonly collection with AutoMapper

我需要找到一种方法将任意值列表转换为另一个列表

如果目标类型是 ICollection<>,AutoMapper 可以工作,因为它正在创建一个实例并用 Add 填充它,但我的类型是不可变列表 'a list

因此,如果我创建整数列表:

let ints = [1; 2; 3]

并尝试使用

将其映射到ResizeArray<int64>List<T>的同义词)
mapper.Map<ResizeArray<int64>>(ints)

它会工作,但如果我尝试将它映射到 int64 list

mapper.Map<int64 list>

那就失败了。

我找到了一个可以成功转换的解决方案,但它只适用于明确定义的类型

let cfg = MapperConfiguration(
            fun c ->
                c.CreateMap<int, int64>() |> ignore
                c.CreateMap<int list, int64 list>()
                 .ConvertUsing(
                    fun source _ (cfg: ResolutionContext) ->
                        source
                        |> Seq.map cfg.Mapper.Map<int, int64>
                        |> Seq.toList))

所以问题是:如何编写类型转换器将 'a list 转换为 'b list 而无需显式定义这些类型的所有可能组合?

我终于找到了解决办法。我只需要查看此 ReadOnlyCollection 映射器

的源代码

解决方案并不完美,因为集合项转换并插入 System.Collections.Generic.List,然后转换为 Microsoft.FSharp.Collections.FSharpList,这会产生一些开销。但至少它在工作

using System.Collections.Generic;
using System.Linq.Expressions;
using static System.Linq.Expressions.Expression;
using AutoMapper.Mappers;
using AutoMapper.Internal;
using static AutoMapper.Internal.CollectionMapperExpressionFactory;
using Microsoft.FSharp.Collections;

    public class SeqToFSharpListMapper : EnumerableMapperBase
    {
        public override bool IsMatch(TypePair context)
            => context.SourceType.IsEnumerableType() 
            && context.DestinationType.FullName.StartsWith("Microsoft.FSharp.Collections.FSharpList`1");

        public override Expression MapExpression(IConfigurationProvider configurationProvider, ProfileMap profileMap, IMemberMap memberMap,
                                                 Expression sourceExpression, Expression destExpression, Expression contextExpression)
        {
            var listType = typeof(List<>).MakeGenericType(ElementTypeHelper.GetElementType(destExpression.Type));
            var list = MapCollectionExpression(configurationProvider, profileMap, memberMap, sourceExpression, Default(listType), contextExpression, typeof(List<>), MapItemExpr);

            return Call(typeof(ListModule).GetMethod(nameof(ListModule.OfSeq)).MakeGenericMethod(destExpression.Type.GenericTypeArguments[0]), list);
        }
    }

和 F#

    override _.MapExpression (configurationProvider, profileMap, memberMap, sourceExpression, destExpression, contextExpression) =
        let listType = typedefof<System.Collections.Generic.List<_>>.MakeGenericType(ElementTypeHelper.GetElementType destExpression.Type)
        let list = MapCollectionExpression(configurationProvider, profileMap, memberMap, 
                                           sourceExpression, Default(listType), contextExpression,
                                           typedefof<System.Collections.Generic.List<_>>,
                                           MapItem(fun c p s d ctx i -> MapItemExpr(c, p, s, d, ctx, &i))) // compiler require explicit lambda
        upcast Call(typedefof<obj list>.Assembly // don't want to use AssemblyQualifiedName
                        .GetType("Microsoft.FSharp.Collections.ListModule") // have to use this trick because we can't access ListModule through typeof
                        .GetMethod("OfSeq")
                        .MakeGenericMethod(destExpression.Type.GenericTypeArguments.[0]),
                    list)