8

I have a simple pair of classes which for I've set up a mapping at initialization time.

public class Order {
  public int ID { get; set; }  
  public string Foo { get; set; }
}

public class OrderDTO {
  public int ID { get; set; }
  public string Foo { get; set; }
}

...

Mapper.CreateMap<Order, OrderDTO>();

Now at a certain point I need to map an Order to an OrderDTO. BUT depending on some circumstances, I might need to ignore Foo during mapping. Let's also assume that I cannot "store" the condition in the source or destination object.

I know how I can configure the ignored properties at initialization time, but I have no idea how I could achieve such a dynamic runtime behavior.

Any help would be appreciated.

UPDATE

My use case for this behaviour is something like this. I have an ASP.NET MVC web grid view which displays a list of OrderDTOs. The users can edit the cell values individually. The grid view sends the edited data back to the server like a collection of OrderDTOs, BUT only the edited field values are set, the others are left as default. It also sends data about which fields are edited for each primary key. Now from this special scenario I need to map these "half-empty" objects to Orders, but of course, skip those properties which were not edited for each object.

The other way would be to do the manual mapping, or use Reflection somehow, but I was just thinking about if I could use AutoMapper in some way.

Zoltán Tamási
  • 12,249
  • 8
  • 65
  • 93
  • how about a simple flow-control command after the mapping? Like `if(a == b) dto.Foo = null;` – TGlatzer Jan 18 '16 at 16:19
  • With AutoMapper you're telling it how to translate between two class. The situation you're describing doesn't make sense to me. It sounds like what you want is another DTO that doesn't have (or ignores) the property. The you can do `if (a == b) Mapper.Map(order) else Mapper.Map(order)` – Vlad274 Jan 18 '16 at 16:25
  • Thanks for the comments, I've updated my post with my current use case. – Zoltán Tamási Jan 19 '16 at 07:20
  • You could use custom resolvers to achieve that. Take a look here: https://automapper.codeplex.com/wikipage?title=Custom%20Value%20Resolvers – jpgrassi Jan 19 '16 at 14:35

2 Answers2

13

I've digged into the AutoMapper source code and samples, and found that there is a way to pass runtime parameters at mapping time.

A quick example setup and usage looks like this.

public class Order {
  public int ID { get; set; }  
  public string Foo { get; set; }
}

public class OrderDTO {
  public int ID { get; set; }
  public string Foo { get; set; }
}

...

Mapper.CreateMap<Order, OrderDTO>()
  .ForMember(e => e.Foo, o => o.Condition((ResolutionContext c) => !c.Options.Items.ContainsKey("IWantToSkipFoo")));

...

var target = new Order();
target.ID = 2;
target.Foo = "This should not change";

var source = new OrderDTO();
source.ID = 10;
source.Foo = "This won't be mapped";

Mapper.Map(source, target, opts => { opts.Items["IWantToSkipFoo"] = true; });
Assert.AreEqual(target.ID, 10);
Assert.AreEqual(target.Foo, "This should not change");

In fact this looks quite "technical", but I still think there are quite many use cases when this is really helpful. If this logic is generalized according to application needs, and wrapped into some extension methods for example, then it could be much cleaner.

Zoltán Tamási
  • 12,249
  • 8
  • 65
  • 93
  • 1
    Very nice call using the Items bag. I actually set a singular key to 'Exclusions' which could be an IEnumerable. cfg.ForAllMaps((tm, mc) => mc.ForAllMembers(opt => opt.Condition((o1,o2,o3,o4,rc) => { var name = opt.DestinationMember.Name; object exclusions = null; if (rc.Items.TryGetValue(EXCLUDEKEY,out exclusions)) { if (((IEnumerable)exclusions).Contains(name)) { return false; } return true; }))); – BlackjacketMack Feb 16 '17 at 14:06
  • Thanks for the addition, I've already refactored to something similar too since my answer. – Zoltán Tamási Feb 16 '17 at 15:09
  • Any idea if this solution works with queryable extension? – tyteen4a03 Jun 28 '18 at 13:34
  • @tyteen4a03 I don't think so, as the Queryable extensions must provide a simple, Linq-processable selector. Having runtim conditions within the select is not fitting into being "simple", as many Linq processors (like Linq-to-Entities for example) may not be able to turn that into an SQL statement. – Zoltán Tamási Jul 01 '18 at 15:01
1

Expanding on BlackjacketMack's comment for others:

In your MappingProfile, add a ForAllMaps(...) call to your constructor.

using AutoMapper;
using System.Collections.Generic;
using System.Linq;

public class MappingProfile : Profile
{
    public MappingProfile()
    {
        ForAllMaps((typeMap, mappingExpression) =>
        {
            mappingExpression.ForAllMembers(memberOptions =>
            {
                memberOptions.Condition((o1, o2, o3, o4, resolutionContext) =>
                {
                    var name = memberOptions.DestinationMember.Name;
                    if (resolutionContext.Items.TryGetValue(MemberExclusionKey, out object exclusions))
                    {
                        if (((IEnumerable<string>)exclusions).Contains(name))
                        {
                            return false;
                        }
                    }
                    return true;
                });
            });
        });
    }
    public static string MemberExclusionKey { get; } = "exclude";
}

Then, for ease of use, add the following class to create an extension method for yourself.

public static class IMappingOperationOptionsExtensions
{
    public static void ExcludeMembers(this AutoMapper.IMappingOperationOptions options, params string[] members)
    {
        options.Items[MappingProfile.MemberExclusionKey] = members;
    }
}

Finally, tie it all together: var target = mapper.Map<Order>(source, opts => opts.ExcludeMembers("Foo"));

Aaron Wynn
  • 101
  • 2
  • 9