Reputation: 2939
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
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 ValueTuple
s.
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
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
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