0

Have come across an issue with AutoMapper (v9.0) using the incorrect mapping for an inherited class when mapping to an Entity Framework (v6.4) proxy class. It appears to be related to the order in which the mapping is executed, and seems to be related to some kind of caching of the maps used. Here is the Entity Framwork configuration:

public class MyDbContext : DbContext
{
    public MyDbContext()
    {
        base.Configuration.ProxyCreationEnabled = true;
    }

    public DbSet<Blog> Blogs { get; set; }
    public DbSet<Post> Posts { get; set; }
}

public class Blog
{
    [Key]
    public int Id { get; set; }
    public string Title { get; set; }
}

public class Post
{
    [Key]
    public int Id { get; set; }
    public DateTime PostDate { get; set; }
    public string Content { get; set; }
    public string Keywords { get; set; }
    public virtual Blog Blog { get; set; }
}

And my DTO classes:

public class PostDTO
{
    public DateTime PostDate { get; set; }
    public string Content { get; set; }
}

public class PostWithKeywordsDTO : PostDTO
{
    public string Keywords { get; set; }
}

Mapping profile:

public class MappingProfile : Profile
{
    public MappingProfile()
    {
        CreateMap<PostDTO, Post>()
            .ForMember(dest => dest.Keywords, opt => opt.MapFrom(src => "No Keywords Specified"));
        CreateMap<PostWithKeywordsDTO, Post>()
            .ForMember(dest => dest.Keywords, opt => opt.MapFrom(src => src.Keywords));
    }
}

I'm attempting to map these DTO object onto a proxy of the 'Post' class which is generated either by fetching an existing Post record from the database or by creating a new proxy of the Post class using (note, I need to enable the proxy class creation for performance reasons in my app):

_myDbContext.Posts.Create();

Now, when I attempt to perform a map from the following postDTO and postWithKeywordsDTO objects to the proxy class:

var postDTO = new PostDTO
{
    PostDate = DateTime.Parse("1/1/2000"),
    Content = "Post #1"
};
var postWithKeywordsDTO = new PostWithKeywordsDTO
{
    PostDate = DateTime.Parse("6/30/2005"),
    Content = "Post #2",
    Keywords = "C#, Automapper, Proxy"
};

var postProxy = mapper.Map(postDTO, _myDbContext.Posts.Create());
var postWithKeywordsProxy = mapper.Map(postWithKeywordsDTO, dbContext.Posts.Create());

the resulting proxy objects are (pseudo-json):

postProxy: {
    PostDate: '1/1/2000', 
    Content: 'Post #1', 
    Keywords: 'No Keywords Specified'
}

postWithKeywordsProxy: {
    PostDate: '6/30/2005', 
    Content: 'Post #2', 
    Keywords: 'No Keywords Specified'
}

Furthermore, if I use something like an inline ValueResolver in the mapping and put a breakpoint on the 'return' lines, I can see that the PostDTO -> Post mapping is being used in both cases, and the PostWithKeywords -> Post mapping isn't being hit at all.

public class MappingProfile : Profile
{
    public MappingProfile()
    {
        CreateMap<PostDTO, Post>()
            .ForMember(dest => dest.Keywords, opt => opt.MapFrom(src => "No Keywords Specified"))
            .ForMember(dest => dest.Content, opt => opt.MapFrom((src, dest) =>
            {
                return src.Content; <-- Hit for both PostDTO and PostWithKeywordsDTO maps to Post
            }))
            ;
        CreateMap<PostWithKeywordsDTO, Post>()
            .ForMember(dest => dest.Keywords, opt => opt.MapFrom(src => src.Keywords))
            .ForMember(dest => dest.Content, opt => opt.MapFrom((src, dest) =>
            {
                return src.Content;
            }))
            ;
    }
}

What I take from this is that it appears that there is some kind of issue in identifying which Type Map to use when dealing with a Proxy object. It seems as though in the first scenario, it encounters an attempted map between PostDTO -> Post_B24121DF0B3091C6258AA7C620C6D74F4114A74351B7D90111C87EAAF182C939 (proxy class), and correctly determines that the map to use is the PostDTO -> Post mapping. It then encounters the attempted map between PostWithKeywordsDTO -> Post_B24121DF0B3091C6258AA7C620C6D74F4114A74351B7D90111C87EAAF182C939 and doesn't realize that the PostWithKeywordsDTO is actually a child of PostDTO, and mistakenly re-uses the PostDTO -> Post mapping.

What's odd, however, is what happens if I reverse the order in which the maps are executed:

var postWithKeywordsProxy = mapper.Map(postWithKeywordsDTO, dbContext.Posts.Create());
var postProxy = mapper.Map(postDTO, _myDbContext.Posts.Create());

the resulting proxy objects are correct:

postWithKeywordsProxy: {
    PostDate: '6/30/2005', 
    Content: 'Post #2', 
    Keywords: 'C#, Automapper, Proxy'
}

postProxy: {
    PostDate: '1/1/2000', 
    Content: 'Post #1', 
    Keywords: 'No Keywords Specified'
}  

This makes me think it has to do with some kind of caching mechanism, which possibly looks for the first map it can find which satisfies the requested proxy map, even if it's not an exact match. In this case, the PostWithKeywordsDTO -> Post_B24121DF0B3091C6258AA7C620C6D74F4114A74351B7D90111C87EAAF182C939 mapping happens first, such that when the subsequent PostDTO -> Post_B24121DF0B3091C6258AA7C620C6D74F4114A74351B7D90111C87EAAF182C939 map happens, it's not able to find a cached Type Map which satisfies the parameters and it continues with generating the correct cached map.

I did attempt to use the version of the Map method which takes in the explicit types of the items to be mapped, however this produced the same result:

var postProxy = mapper.Map(postDTO, _myDbContext.Posts.Create(), typeof(PostDTO), typeof(Post));
var postWithKeywordsProxy = mapper.Map(postWithKeywordsDTO, dbContext.Posts.Create(), typeof(PostWithKeywordsDTO), typeof(Post));

Also note that if I don't use the proxy versions of the Post class, everything works as expected, so it doesn't appear to be an issue w/ the mapping configuration.

As for possible workarounds, the closest I've found is in this thread (Automapper : mapping issue with inheritance and abstract base class on collections with Entity Framework 4 Proxy Pocos), which appears to be a similar issue, however the workaround in this case was to use the 'DynamicMap' function, which has since been deprecated in AutoMapper. Has anyone else encountered a similar issue w/ proxy class mapping and know of another solution?

Brad Havens
  • 79
  • 10
  • Other items of note, I've tried all of the various combinations of 'IncludeBase', 'IncludeDerived' mapping configurations I can think of, without success as well as removing the 'Keywords' mapping from the PostDTO -> Post mapping alltogether, but no luck. – Brad Havens Apr 23 '20 at 21:21
  • A repro would help. Make a [gist](https://gist.github.com/lbargaoanu/9c7233441c3a3413cc2b9b9ebb5964a9) that we can execute and see fail. But without EF, just some classes, to illustrate your point. – Lucian Bargaoanu Apr 24 '20 at 03:45
  • Here's a [gist](https://gist.github.com/bhavens17/c2021893d0b87cd1cf9ddddc9538ad57) of the issue. Unfortunately, I can't exclude the Entity Framework bit, as the proxy classes which are generated by EF are the ones where the mapping is failing. I used LocalDB on my machine to test it, and was also able to run this gist code in LinqPad 5 after adding the references I listed at the top of the gist. LMK if you're having issues running the gist code. – Brad Havens Apr 24 '20 at 04:25
  • No map will exactly match because of those proxies. That's how it's supposed to work. Replace with `Post` and you'll see. I don't know about the EF part. – Lucian Bargaoanu Apr 24 '20 at 04:49
  • I really don't understand how people can downvote such a carefully prepared question. I wish everybody would ask questions this way. That said, I think it's a good idea to post this as an issue in [AutoMapper's repository](https://github.com/AutoMapper/AutoMapper/issues) too. I think it takes careful debugging of AM's source code to crack this one. – Gert Arnold Apr 24 '20 at 07:22
  • No, there's nothing here as far as AM is concerned. – Lucian Bargaoanu Apr 24 '20 at 07:39
  • Why would you even map into a proxy? After all, the data to update it's in the dto. – Lucian Bargaoanu Apr 24 '20 at 12:14
  • I'm not sure I understand why the map wouldn't work because of the proxy? The proxy (I believe, if I understand correctly) is just a dynamically generated sub-class of the POCO entity class, with some built-in additional features like change tracking and lazy loading. The structure of the class is effectively the same as the POCO class, however. Mapping data to EF POCO classes is a very common use case, IMO. I agree that replacing the proxies with the POCO Post class causes it to work, and that was kind of my point of posting this. It does work in that scenario, but that's not practical. – Brad Havens Apr 24 '20 at 13:18
  • Perhaps my class naming may be what's causing the confusion? Imagine instead if I'd called them PostViewModel and PostWIthKeywordsViewModel, which would be lightweight classes used to get data to/from the UI. In those cases, the data must be mapped from the View Models back onto the POCO entity classes in order for the changes from the UI to then be committed to the database via EF. – Brad Havens Apr 24 '20 at 13:23
  • Thanks @GertArnold, I'll go ahead and submit the question there as well. – Brad Havens Apr 24 '20 at 13:25
  • One final note, @LucianBargaoanu, my second scenario proves that the mapping DOES work with proxies, but in this case it only works if you map the child class first, then the parent class. It's only the scenario where you do it in the reverse order that it doesn't work. – Brad Havens Apr 24 '20 at 13:27
  • That's irrelevant. The problem is your mapping, not AM. As the `Post` example proves. – Lucian Bargaoanu Apr 24 '20 at 13:41
  • What's wrong w/ the mapping? How should it be configured? I agree that AM works correctly in the POCO scenario, but that's not practical if you want to use EF features like lazy loading. Also, as Jimmy states [here](https://github.com/AutoMapper/AutoMapper/issues/2004#issuecomment-286588864), AM should automatically handle proxies of POCOs in the same way as the POCOs themselves – Brad Havens Apr 24 '20 at 14:10
  • That's not what he says there. What's wrong. No mapping will exactly match. AM chooses one and then you're not happy with that choice. Well, AM does its best :) Mapping into EF proxies is not an important use case for AM. The main use case is mapping from entities to dto-s. – Lucian Bargaoanu Apr 24 '20 at 14:17
  • Then what does he mean by 'EF proxies don't count - we handle those automatically'? Doesn't that mean that AM has some awareness of how to handle proxies? If not, I imagine it would be throwing 'Missing Map' exceptions for these proxy maps. Assuming that's the case, why wouldn't it use the map which uses the explicit source type being passed in instead of using the map for the source's parent? Does it basically just see that there's no exact match and instead use the first map which satisfies the source/destination type pair? – Brad Havens Apr 24 '20 at 15:03
  • It's a carefully tuned algorithm :) And I don't mean for EF proxies. – Lucian Bargaoanu Apr 24 '20 at 15:15
  • Finally dug into the code, and it appears that the issue stems from the way the ['GetRelatedTypePairs' function in TypePair.cs](https://github.com/AutoMapper/AutoMapper/blob/master/src/AutoMapper/TypePair.cs) is set up to combine all of the destination inherited types with with the source inherited types. As it is, it loops through the destination types first, then the source types. The result of this is that when it doesn't find a match for the proxy, it first looks for a map for the source parent type -> destination, which in this case is PostDTO -> Post proxy, which it previously created. – Brad Havens Apr 24 '20 at 15:37
  • It would seem that a simple solution would just be to reverse the order in which the combinations are created (source types first, then destination), but I understand that this might be set up this way as to minimize loops through the list and might affect performance or have other consequences – Brad Havens Apr 24 '20 at 15:39
  • A followup question would be, why doesn't it property handle the scenario where I explicitly give it the types to use for the mapping: mapper.Map(postWithKeywordsDTO, dbContext.Posts.Create(), typeof(PostWithKeywordsDTO), typeof(Post)). There, I'm telling it exactly which map to use, right? It seems that in that scenario, it still prefers to infer the type map from the object types passed in. In that case, why even provide the option to pass the types in? – Brad Havens Apr 24 '20 at 15:55
  • Because the source/destination object can be null. – Lucian Bargaoanu Apr 24 '20 at 16:12
  • But in those cases, you'll always also have either a generic type parameter or explicit type parameter telling you the type to use, right? Changing the 'TypePair' Create methods to only use the source/destination object types if the explicit source/destination types are null (which I'm not sure they ever would be) would accomplish this. – Brad Havens Apr 24 '20 at 16:39
  • Funny enough, making the change I suggested solved my initial problem. Here's the [gist](https://gist.github.com/bhavens17/299e7398817207011bbb9fe7962722c8) of the changes to the 'TypePair.cs' class in AM. This appears to force AM to use the explicit mapping between PostWithKeywordsDTO -> Post and doesn't involve proxy inheritance lookups at all. – Brad Havens Apr 24 '20 at 17:38

1 Answers1

1

Here's what I ended up doing to solve the issue. After digging in the code for a bit, I decided that my solution to force the mapping types was going to cause other issues with mapping inheritance.

Instead, I settled on a solution which computes the 'distance' each matching type map is from the requested types based on the number of inheritance levels the type map source/destination types are from the corresponding requested types, and selects the 'closest' one. It does this by treating the 'Source Distance' as the x value and the 'Destination Distance' as the y value in the standard two coordinate distance calculation:

Overall Distance = SQRT([Source Distance]^2 + [Destination Distance]^2)

For example, in my scenario I have the following maps:

PostDTO -> Post
and
PostWithKeywordsDTO -> Post

When attempting to map a PostWithKeywordsDTO -> PostProxy there is no exact mapping match, so we have to determine which map is the best fit. In this case, the list of possible maps which can be used are:

PostDTO -> Post (Since PostWithKeywordsDTO inherits from PostDTO and PostProxy inherits from Post)
or
PostWithKeywordsDTO -> Post (Since PostProxy inherits from Post)

To determine which map to use, it calculates:

PostDTO -> Post: 
Source Distance = 1 (PostDTO is one level above PostWithKeywordsDTO)
Destination Distance = 1 (Post is one level above PostProxy)
Overall Distance = 1.414

PostWithKeywordsDTO -> Post
Source Distance = 0 (since PostWithKeywordsDTO = PostWithKeywordsDTO)
Destination Distance = 1 (Post is one level above PostProxy)
Overall Distance = 1

So in this case, it would use the PostWithKeywordsDTO -> Post mapping, since the distance is the smallest. This appears to work in all cases, and satisfies all of the AM unit tests as well. Here's a gist of the updates needed to the code (although I'm sure there are probably cleaner/more efficient ways to do it).

Brad Havens
  • 79
  • 10
  • I think they call that over engineering :) – Lucian Bargaoanu Apr 25 '20 at 03:54
  • Still complicated, but easier I think, would be to create the map for the proxy type, not for `Post`. – Lucian Bargaoanu Apr 25 '20 at 04:48
  • I call it a carefully tuned algorithm :) – Brad Havens Apr 25 '20 at 14:58
  • Interesting idea w/ the proxy maps, but I'm not sure how you'd do that since EF dynamically generates the proxies at runtime. I suppose you could create a mapping profile which searches through the POCO library and instantiates the proxies and adds maps for any which currently also have corresponding mappings for the POCO, but I have questions there. Does EF always use the same proxy class for its POCOs? Do the proxies used ever change or are they ever replaced w/ new types over the life of the app? Seems like it could work, but I still prefer my solution which is proxy type agnostic. – Brad Havens Apr 25 '20 at 15:06
  • The problem with your solution is the sorting which can happen only after the fact. – Lucian Bargaoanu Apr 25 '20 at 15:23
  • Can you clarify? I'm not following what you mean – Brad Havens Apr 25 '20 at 15:46
  • If I understand your concern correctly, you could do it without the 'possibleMatches' collection. You could just store the current match w/ the lowest distance and only replace that value if you find one with a lower distance. That would get rid of the 'OrderBy' at the end – Brad Havens Apr 25 '20 at 15:49
  • That doesn't solve anything. A first match will always be quicker. – Lucian Bargaoanu Apr 25 '20 at 15:53
  • Aha, I gotcha. So instead of sorting after the fact, we should sort inside the 'GetRelatedTypePairs' function, such that the first match from that list will be the closest. Good call – Brad Havens Apr 25 '20 at 16:01