0
votes

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?

1
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
A repro would help. Make a gist that we can execute and see fail. But without EF, just some classes, to illustrate your point.Lucian Bargaoanu
Here's a gist 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
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
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 too. I think it takes careful debugging of AM's source code to crack this one.Gert Arnold

1 Answers

1
votes

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