Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Automapping using open generics and including the source in a ForMember statement

I've recently upgraded from Automapper 4.2.1 to 5.1.1 and am having issues with a previously valid mapping involving open generics.

Previously, within the automapper configuration, I had the following open generic mapping configuration

CreateMap(typeof(IPager<>), typeof(ModelPager<>))
    .ForMember("Items", e => e.MapFrom(o => (IEnumerable) o));

This works in Automapper 4 but fails in 5 with a InvalidOperaionException when attempting to map via IMapper.Map<TDestination>(source). It appears to fail when executing the mapping of the Items ForMember operation with an exception message of "Sequence contains no matching element"

As reflected in the example implementation code below IPager<TSource> implements IEnumerable<TSource>, and the Items property of ModelPager<TDestination> is an IEnumerable<TDestination> so the cast should be valid. and there exist a valid mapping for each TSource to TDestination

CreateMap<TSource, TDestination>();

IPager interface

public interface IPager<out TItem> : IEnumerable<TItem>
{
    int CurrentPage { get; }

    int PageCount { get; }

    int PageSize { get; }

    int TotalItems { get; }
}

IPager implementation

public class Pager<TItem> : IPager<TItem>
{
    private readonly IEnumerable<TItem> _items;

    public Pager(IEnumerable<TItem> items,
                 int currentPage,
                 int pageSize,
                 int totalItems)
    {
        /// ... logic ... 
        this._items = items ?? Enumerable.Empty<TItem>();
        this.CurrentPage = currentPage;
        this.PageSize = pageSize;
        this.TotalItems = totalItems;
    }

    public int CurrentPage { get; }

    public int PageCount => (this.TotalItems + this.PageSize - 1) / this.PageSize;

    public int PageSize { get; }

    public int TotalItems { get; }

    IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();

    public IEnumerator<TItem> GetEnumerator() => this._items.GetEnumerator();
}

ModelPager

public class ModelPager<TItem>
{
    public int CurrentPage { get; set; }

    public IEnumerable<TItem> Items { get; set; }

    public int PageCount { get; set; }

    public int PageSize { get; set; }

    public int TotalItems { get; set; }
}

What is the proper way to map this in Automapper 5 without either abandoning open generics by explicitly mapping each possible mapping, or by using a custom open generic type converter that would require me to manually map all properties and use reflection to resolve the open types for assignment?

like image 300
rheone Avatar asked Aug 16 '16 19:08

rheone


1 Answers

Given this looks to be a bug (AutoMapper #1624), a work around can be done with a custom open generic TypeConverter that does not require reflection.

The mapping should be changed to something along the lines of

CreateMap(typeof(IPager<>), typeof(ModelPager<>))
    .ConvertUsing(typeof(PagerToModelPagerConverter<,>));

with a custom ITypeConverter

public class PagerToModelPagerConverter<TSource, TDestination> : ITypeConverter<IPager<TSource>, ModelPager<TDestination>>
{
    public ModelPager<TDestination> Convert(IPager<TSource> source,
                                            ModelPager<TDestination> destination,
                                            ResolutionContext context)
    {
        var list = source.ToList(); // avoid redundant iterations
        var itemMapping = context.Mapper.Map<IEnumerable<TSource>, IEnumerable<TDestination>>(list);

        var modelPager = new ModelPager<TDestination>
                         {
                             CurrentPage = source.CurrentPage,
                             Items = itemMapping,
                             PageCount = source.PageCount,
                             PageSize = source.PageSize,
                             TotalItems = source.TotalItems
                         };

        return modelPager;
    }
like image 109
rheone Avatar answered Sep 18 '22 16:09

rheone