2

I've got a recursive model class with the following definition:

public class ItemFilterBlockGroup
{
    public ItemFilterBlockGroup(string groupName, ItemFilterBlockGroup parent, bool advanced = false)
    {
        GroupName = groupName;
        ParentGroup = parent;
        Advanced = advanced;
        ChildGroups = new List<ItemFilterBlockGroup>();
    }

    public string GroupName { get; private set; }
    public bool Advanced { get; private set; }
    public ItemFilterBlockGroup ParentGroup { get; private set; }
    public List<ItemFilterBlockGroup> ChildGroups { get; private set; }
}

It has a property called ChildGroups which is a list of itself - this is used to build up a hierarchical model. What I'm trying to do is map this model to view models, but conditionally. Sometimes (depending on a UI setting) I want to include only Child objects with Advanced = false, and sometimes I want to include all models.

Currently I'm achieving this with a nasty hack that involves Mapper.Reset() and runtime re-definition of the maps - this is obviously not good and presents multiple problems:

Mapper.Reset();
if (showAdvanced)
{
    Mapper.CreateMap<ItemFilterBlockGroup, ItemFilterBlockGroupViewModel>();
}
else
{
    Mapper.CreateMap<ItemFilterBlockGroup, ItemFilterBlockGroupViewModel>()
        .ForMember(dest => dest.ChildGroups,
            opts => opts.MapFrom(from => from.ChildGroups.Where(c => c.Advanced == false)));
}

var mappedViewModels = Mapper.Map<ObservableCollection<ItemFilterBlockGroupViewModel>>(blockGroups);

Given an example input hierarchy of models:

Root (Advanced = False)
+-Child 1 (Advanced = True)
+-Child 2 (Advanced = False)
+-Child 3 (Advanced = False)
  +-Child 3 Sub Child 1 (Advanced = False)
  +-Child 3 Sub Child 2 (Advanced = True)

The first CreateMap definition returns this hierarchy untouched, and the second CreateMap definition (with the Advanced parameter) returns this modified hierarchy (all Advanced = true models and their children are excluded from mapping):

Root (Advanced = False)
+-Child 2 (Advanced = False)
+-Child 3 (Advanced = False)
  +-Child 3 Sub Child 1 (Advanced = False)

How can I parameterise the showAdvanced condition and achieve the same result with a single CreateMap definition? I've searched a lot for the right solution, tried ResolveUsing, CustomResolvers, to no avail.

XVar
  • 436
  • 4
  • 15

3 Answers3

1

you can use custom converters something like given below , you can customize your mapping setup.

Create a convert class

  internal class AccountConverter : TypeConverter<PD.IAccount, OD.IAccount>
{
    protected override OD.IAccount ConvertCore(PD.IAccount source)
    {
        var result = new Account()
        {
            CustomerNumber = source.CustomerNumber,
            EAccount = source.EAccount,
            EmailAddress = source.EmailAddress
        };

        return result;
    }
}

Then add mapping like this.

Mapper.CreateMap<PD.IAccount, OD.IAccount>()
  .ConvertUsing(new AccountConverter());
Prashant
  • 460
  • 3
  • 12
  • How does this help me map conditionally at runtime? I need a single map that can behave differently depending on an input parameter (the showAdvanced boolean in my question example). – XVar Jul 01 '15 at 18:13
  • what you can do you add a this show advance in the "ItemFilterBlockGroup" class which is the source object in automapper and then you can put an if condition in the " ConvertCore" method of the typeconverter and check if its true do this otherwise do this. does that make sense. internal class AccountConverter : TypeConverter { protected override OD.IAccount ConvertCore(PD.IAccount source) { if(source.showAdvanced) ..... – Prashant Jul 01 '15 at 18:27
  • So you're suggesting I pollute my object model with data that's only relevant to my views? That's not something I'm willing to do. – XVar Jul 01 '15 at 18:34
  • there is one more way of doing the change in automapper but that too is not very elegant , something like whats given below. if that too does not work for you then there is no cleaner way of doing this in automapper , you need to have a conditional mapping like posted by "XtremeBytes" above. --- bool showAdvanced = false; var tt = Mapper.Map(a1, e => e.AfterMap((a, b) => b.Name =showAdvanced ? "test" : a.Name)); --- – Prashant Jul 01 '15 at 19:19
1

You can use context option Items collection to pass values into map function at runtime:

var showAdvanced = true;
var mappedViewModels = Mapper.Map<ObservableCollection<ItemFilterBlockGroupViewModel>>(
    blockGroups, 
    options => options.Items["includeAdvanced"] = showAdvanced);

And use them anywhere where context is available to construct destination object. Within ResolveUsing or ConstructUsing methods for example:

Mapper.CreateMap<ItemFilterBlockGroup, ItemFilterBlockGroupViewModel>()
    .ForMember(destination => destination.ChildGroups, options => options.ResolveUsing(
        (resolution) =>
        {
            var includeAdvanced = (bool)resolution.Context.Options.Items["includeAdvanced"];
            var source = (ItemFilterBlockGroup)resolution.Context.SourceValue;
            if(includeAdvanced)
                return source.ChildGroups;
            else
                return source.ChildGroups.Where(c => c.Advanced == false);               
         }));


If usage of weakly typed dictionary values to pass flag argument look not so pretty to you I suggest to encapsulate this logic within two separated methods, as in Martin Fowler article example.

Leonid Vasilev
  • 11,910
  • 4
  • 36
  • 50
  • This looks much more along the lines of what I was thinking - I'm having some trouble understanding how to construct the view models though. Because the view models are the same type that's being mapped, I can't use Mapper.Map<> within the ConstructUsing call. AutoMapper seems to deal with this kind of recursion fine normally, but it seems by using ConstructUsing I'd be losing this functionality. At which point I may as well not use AutoMapper at all and just write a recursive method to do the mapping. – XVar Jul 01 '15 at 21:03
  • What I'd really like to do is something like this: `Mapper.CreateMap().ForMember(dest => dest.ChildGroups, opts => opts.MapFrom(from => (bool)context.Options.Items["showAdvanced"] ? from.ChildGroups : from.ChildGroups.Where(c => c.Advanced == false)))` But I don't think it's possible to access Options.Items from within ForMember. – XVar Jul 01 '15 at 21:03
  • 1
    @XVar, yeah, I was thinking about it. Try updated version of map creation code – Leonid Vasilev Jul 01 '15 at 21:12
0

Well you want to control the logic using a outside parameter that is not part of the object. I bielieve the best way would be to use one mapping, and filter the input object based on your flag. Something like this

            var blockGroupsTemp;
            if (showAdvanced) 
                blockGroupsTemp = blockGroups;
            else
            {
                blockGroupsTemp = blockGroups.Where(x => x.Advanced == false).ToList();
                blockGroupsTemp.ForEach(s => s.ChildGroups.RemoveAll(y => y.Advanced == true));
            }

            var mappedViewModels = Mapper.Map<ObservableCollection<ItemFilterBlockGroupViewModel>>(blockGroupsTemp);
XtremeBytes
  • 1,469
  • 8
  • 12
  • That only filters the root object's children though - I need the children at every level to be filtered, which is what the second example CreateMap in my question does. – XVar Jul 01 '15 at 18:11
  • My point is to do the filtering on the source object and not at using the mapper. I updated the answer with an option to filter the childgroup. – XtremeBytes Jul 01 '15 at 18:27
  • Your answer still only filters one level of child groups - the hierarchy can be potentially infinite levels deep. What I was hoping to do was avoid manually doing this kind of model manipulation since AutoMapper is clearly capable of doing it as that's what the second CreateMap statement in my question does. – XVar Jul 01 '15 at 18:34
  • Okie I see the way you trying to use the Automapper to do the recursion. Have you tried config mapping like the one here http://stackoverflow.com/questions/14266108/create-two-automapper-maps-between-the-same-two-object-types – XtremeBytes Jul 01 '15 at 18:45