Pochen
Pochen

Reputation: 2939

Automapper - Get all entries in resolver

I'm working on a project heavily relying on Automapper, and most of the times we are mapping complete sets of data into a set view models, for example

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

In the mapping setup we are using Custom resolvers thanks to IMemberValueResolver. The parameters and accessible data in a Resolve and ResolveStatic-method, is only the current entity being mapped. Is it possible to access the complete source (ListOfOjectA) in this case, inside the resolver?

So far I am adding the ListOfOjectA into MappingOperationsOptions.Items and use them from context.Items, but this is a work around that is not easy to work with and does not scale well.

I hope I made my question relatively clear.

Upvotes: 1

Views: 2031

Answers (3)

pfx
pfx

Reputation: 23244

If you prefer not to use the ResolutionContext, you can set up a mapping via an intermediate object holding both the current source item as also the full source list.
Use a lightweight value type, eg. a Tuple or ValueTuple.

The mapping below uses a ValueTuple (but can also be expressed using a Tuple).
Note that the intent and prerequisites of this mapping are quite explicit; it indicates that 2 input/source elements are required: ObjectA and IEnumerable<ObjectA> (passed via a ValueTuple).

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

At the time of mapping, you project the source list into one of corresponding ValueTuples.
Prefer to keep the flow streaming using only 1 current ValueTuple.

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

A custom IValueResolver receives both the current input item and the full list via the ValueTuple source argument.

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}";
    }
}

Full example.

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))
        );

Upvotes: 0

Funk
Funk

Reputation: 11211

It's worth pointing out that you're not really mapping ObjectA to ViewModelA. Rather (ObjectA, List<ObjectA>) to ViewModelA, as you can't seem to define ViewModelA without List<ObjectA>.

To simulate, say ObjectA has an Index property as well as the number of Pages it contains.

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

And for ViewModelA we want to resolve StartPage, based on the properties of the previous ObjectA's.

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

We can clean up your current approach using extension methods.

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()];
}

Using these methods we no longer need to handle the context's Items collection directly.

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;
    }
}

If you want to reuse CustomResolver on different classes, you can abstract the properties it operates on into an interface.

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; }
}

This way the resolver is no longer bound to a concrete implementation. We can now even use the interface as a type parameter.

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;
    }
}

All we need to do is transform our List<ObjectA> before mapping.

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}"));

Once you code to an interface, the sourceMember in the CustomResolver becomes redundant. We can now get it through the passed source. Allowing for one final refactor, as we derive from IValueResolver instead of 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;
    }
}

Updating the signature.

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

How far you take it is entirely up to you, but you can improve code reuse by introducing abstractions.

Upvotes: 1

Sead Avdic
Sead Avdic

Reputation: 118

You can map a collection of items from a dto collection or other class in a way under.

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

And your profile class constructor can be written in a way. This is a only a example. You do not need to accept a list in your resolver, you can do it for one object and map to list from outside.

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

Upvotes: 0

Related Questions