Automapper - 获取解析器中的所有条目

Automapper - Get all entries in resolver

我正在做一个严重依赖 Automapper 的项目,大多数时候我们将完整的数据集映射到一组视图模型中,例如

IEnumerable<ObjectA> ListOfObjectA = MockupOfObjectA;
IEnumerable<ViewModelA> = Mapper.Map<IEnumerable<ObjectA>>(ListOfOjectA)

由于 IMemberValueResolver,我们在映射设置中使用自定义解析器。 Resolve 和 ResolveStatic 方法中的参数和可访问数据只是被映射的当前实体。在这种情况下,是否可以在解析器内部访问完整的源代码 (ListOfOjectA)?

到目前为止,我正在将 ListOfOjectA 添加到 MappingOperationsOptions.Items 中并从 context.Items 开始使用它们,但这是一种不容易使用且无法很好扩展的解决方法。

希望我的问题说得比较清楚。

您可以通过以下方式映射来自 dto 集合或其他 class 的项目集合。

public Order Convert(OrderDto orderDto)
{
    var order = new Order { OrderLines = new OrderLines() };
    order.OrderLines = Mapper.Map<List<OrderLine>>(orderDto.Positions);
    return order;
}

而你的配置文件class构造函数可以这样写。这只是一个例子。您不需要在解析器中接受列表,您可以为一个对象执行此操作并从外部映射到列表。

    public Profile()
    {
      CreateMap<PositionDto, OrderLine>()
        .ForMember(dest => dest, opt => opt.ResolveUsing<OrderResolver>());
    }
  }
}

值得指出的是,您并不是真正将 ObjectA 映射到 ViewModelA。而是 (ObjectA, List<ObjectA>) 到 ViewModelA,因为如果没有 List<ObjectA>.

,您似乎无法定义 ViewModelA

为了模拟,假设 ObjectA 有一个 Index 属性 以及它包含的 Pages 的数量。

public class ObjectA
{
    public int Index { get; set; }
    public int Pages { get; set; }
    public string MyProperty { get; set; }
}

而对于 ViewModelA,我们要根据之前 ObjectA 的属性解析 StartPage

public class ViewModelA
{
    public int StartPage { get; set; }
    public string MyProperty { get; set; }
}

我们可以使用扩展方法清理您当前的方法。

public static class AutoMapperExt
{
    public static TDestination MapWithSource<TSource, TDestination>(this IMapper mapper, TSource source)
        => mapper.Map<TSource, TDestination>(source, opts => opts.Items[typeof(TSource).ToString()] = source);

    public static TSource GetSource<TSource>(this ResolutionContext context)
        => (TSource)context.Items[typeof(TSource).ToString()];
}

使用这些方法,我们不再需要直接处理上下文的 Items 集合。

class Program
{
    static void Main(string[] args)
    {
        var config =
            new MapperConfiguration(cfg =>
                cfg.CreateMap<ObjectA, ViewModelA>()
                   .ForMember(dest => dest.StartPage, opt => opt.MapFrom<CustomResolver, int>(src => src.Index))
            );
        var mapper = config.CreateMapper();

        var source = new List<ObjectA>
        {
            new ObjectA { Index = 0, Pages = 3, MyProperty = "Foo" },
            new ObjectA { Index = 1, Pages = 2, MyProperty = "Bar" },
            new ObjectA { Index = 2, Pages = 1, MyProperty = "Foz" },
        };

        var result = mapper.MapWithSource<List<ObjectA>, List<ViewModelA>>(source);

        result.ForEach(o => Console.WriteLine(o.StartPage)); // prints 1,4,6
        Console.ReadKey();
    }
}

public class CustomResolver : IMemberValueResolver<object, object, int, int>
{
    public int Resolve(object source, object destination, int sourceMember, int destMember, ResolutionContext context)
    {
        var index = sourceMember;
        var list = context.GetSource<List<ObjectA>>();

        var pages = 1;
        for (int i = 0; i < index; i++)
        {
            pages += list[i].Pages;
        }

        return pages;
    }
}

如果你想在不同的 类 上重用 CustomResolver,你可以将它操作的属性抽象到一个接口中。

public interface IHavePages
{
    int Index { get; }
    int Pages { get; }
}

public class ObjectA : IHavePages
{
    public int Index { get; set; }
    public int Pages { get; set; }
    public string MyProperty { get; set; }
}

这样解析器就不再绑定到具体的实现。我们现在甚至可以将接口用作类型参数。

public class CustomResolver : IMemberValueResolver<IHavePages, object, int, int>
{
    public int Resolve(IHavePages source, object destination, int sourceMember, int destMember, ResolutionContext context)
    {
        var hasPages = source;
        var index = sourceMember;
        var list = context.GetSource<List<IHavePages>>();

        var pages = 1;
        for (int i = 0; i < index; i++)
        {
            pages += list[i].Pages;
        }

        return pages;
    }
}

我们需要做的就是在映射之前转换我们的List<ObjectA>

var listOfObjectA = new List<ObjectA>
{
    new ObjectA { Index = 0, Pages = 3, MyProperty = "Foo" },
    new ObjectA { Index = 1, Pages = 2, MyProperty = "Bar" },
    new ObjectA { Index = 2, Pages = 1, MyProperty = "Foz" },
};
var source = listOfObjectA.OfType<IHavePages>().ToList();
var result = mapper.MapWithSource<List<IHavePages>, List<ViewModelA>>(source);

// AutoMapper still maps properties that aren't part of the interface
result.ForEach(o => Console.WriteLine($"{o.StartPage} - {o.MyProperty}"));

一旦你对接口进行编码,CustomResolver 中的 sourceMember 就变得多余了。我们现在可以通过传过来的source来获取了。允许最后一次重构,因为我们从 IValueResolver 而不是 IMemberValueResolver.

派生
public class CustomResolver : IValueResolver<IHavePages, object, int>
{
    public int Resolve(IHavePages source, object destination, int destMember, ResolutionContext context)
    {
        var list = context.GetSource<List<IHavePages>>();

        var pages = 1;
        for (int i = 0; i < source.Index; i++)
        {
            pages += list[i].Pages;
        }

        return pages;
    }
}

正在更新签名。

cfg.CreateMap<ObjectA, ViewModelA>()
   .ForMember(dest => dest.StartPage, opt => opt.MapFrom<CustomResolver>())

你能走多远完全取决于你,但你可以通过引入抽象来提高代码重用。

如果您不想使用 ResolutionContext,您可以通过包含当前源项目和完整源列表的中间对象设置映射。
使用轻量级值类型,例如。一个 TupleValueTuple.

下面的映射使用 ValueTuple(但也可以使用 Tuple 表示)。
请注意,此映射的意图和先决条件非常明确;它表示需要 2 个 input/source 元素:ObjectAIEnumerable<ObjectA> (通过 ValueTuple 传递).

Mapper.Initialize(cfg =>
    cfg.CreateMap<(ObjectA, IEnumerable<ObjectA>), ViewModelA>()
       .ForMember(
           dest => dest.Name,
           opt => opt.MapFrom<CustomResolver>()
       ));            

在映射时,您将源列表投影到相应的 ValueTuple 之一。
更喜欢仅使用 1 个电流来保持流量 ValueTuple

var viewModels = 
    Mapper.Map<IEnumerable<ViewModelA>>(
        ListOfObjectA.Select(o => (o, ListOfObjectA))
        );

自定义 IValueResolver 通过 ValueTuple 源参数接收当前输入项和完整列表。

public class CustomResolver :
    IValueResolver<
        (ObjectA Item, IEnumerable<ObjectA> List),
        ViewModelA,
        String
        >
{
    public string Resolve(
        (ObjectA Item, IEnumerable<ObjectA> List) source,
        ViewModelA destination, 
        string destMember, 
        ResolutionContext context
        )
    {
        /* Retrieve something via the list. */
        var suffix = source.List.Count().ToString(); 
        return $"{source.Item.Name} {suffix}";
    }
}

完整示例。

IEnumerable<ObjectA> ListOfObjectA = new List<ObjectA> {
    new ObjectA { Name = "One" },
    new ObjectA { Name = "Two" },
    new ObjectA { Name = "Three" }
    };

Mapper.Initialize(cfg =>
    cfg.CreateMap<(ObjectA, IEnumerable<ObjectA>), ViewModelA>()
        .ForMember(
            dest => dest.Name,
            opt => opt.MapFrom<CustomResolver>()
        ));                

var viewModels = 
    Mapper.Map<IEnumerable<ViewModelA>>(
        ListOfObjectA.Select(o => (o, ListOfObjectA))
        );