Does AutoMapper support Linq?
Asked Answered
I

2

25

I am very interested in Linq to SQL with Lazy load feature. And in my project I used AutoMapper to map DB Model to Domain Model (from DB_RoleInfo to DO_RoleInfo). In my repository code as below:

    public DO_RoleInfo SelectByKey(Guid Key)
    {
        return SelectAll().Where(x => x.Id == Key).SingleOrDefault();
    }

    public IQueryable<DO_RoleInfo> SelectAll()
    {
        Mapper.CreateMap<DB_RoleInfo, DO_RoleInfo>();
        return from role in _ctx.DB_RoleInfo
               select Mapper.Map<DB_RoleInfo, DO_RoleInfo>(role);
    }

SelectAll method is run well, but when I call SelectByKey, I get the error:

Method “RealMVC.Data.DO_RoleInfo MapDB_RoleInfo,DO_RoleInfo” could not translate to SQL.

Is it that Automapper doesn't support Linq completely?

Instead of Automapper, I tried the manual mapping code below:

public IQueryable<DO_RoleInfo> SelectAll()
{
    return from role in _ctx.DB_RoleInfo 
    select new DO_RoleInfo 
    {
        Id = role.id,
        name = role.name,
        code = role.code
    };
}

This method works the way I want it to.

Inure answered 6/2, 2010 at 4:56 Comment(2)
Just a note on your update in addition to my answer: The second version works because Linq to SQL already knows that you've done an irreversible projection; the SelectByKey is actually just using Linq to Objects. If you examine the actual query that's being generated, I think you will find that it is still selecting all of the entities from the database, which is equivalent to using ToList() and then filtering the resulting list.Shivers
It's not that AutoMapper supports LINQ. It's that LINQ to SQL does not support AutoMapper. The LINQ to SQL query provider looks at the expression tree to determine how to generate the SQL query. When it gets to the Mapper.Map piece, it has no idea how to generate the SQL.Cad
O
70

While @Aaronaught's answer was correct at the time of writing, as often the world has changed and AutoMapper with it. In the mean time, QueryableExtensions were added to the code base which added support for projections that get translated into expressions and, finally, SQL.

The core extension method is ProjectTo1. This is what your code could look like:

using AutoMapper.QueryableExtensions;

public IQueryable<DO_RoleInfo> SelectAll()
{
    Mapper.CreateMap<DB_RoleInfo, DO_RoleInfo>();
    return _ctx.DB_RoleInfo.ProjectTo<DO_RoleInfo>();
}

and it would behave like the manual mapping. (The CreateMap statement is here for demonstration purposes. Normally, you'd define mappings once at application startup).

Thus, only the columns that are required for the mapping are queried and the result is an IQueryable that still has the original query provider (linq-to-sql, linq-to-entities, whatever). So it is still composable and this will translate into a WHERE clause in SQL:

SelectAll().Where(x => x.Id == Key).SingleOrDefault();

1 Project().To<T>() prior to v. 4.1.0

Oliverolivera answered 11/9, 2012 at 8:43 Comment(5)
This answer should become the new answer.Michale
Just add using AutoMapper.QueryableExtensions;Chard
I'm getting the following error "The method 'Where' cannot follow the method 'Select' or is not supported". Please see question here (https://mcmap.net/q/537717/-the-method-39-where-39-cannot-follow-the-method-39-select-39-or-is-not-supported/1133338).Kenogenesis
Note that you would want to move Mapper.CreateMap out of this method for production code. CreateMap should only be called once at application startup so moving all your CreateMaps to some sort of bootstrapper / calling from Application_Start in global.asax is the way to go.Hypertrophy
last line could be: SelectAll().SingleOrDefault(x => x.Id == Key);Fiddle
S
26

Change your second function to this:

public IEnumerable<DO_RoleInfo> SelectAll()
{
    Mapper.CreateMap<DB_RoleInfo, DO_RoleInfo>();
    return from role in _ctx.DB_RoleInfo.ToList()
           select Mapper.Map<DB_RoleInfo, DO_RoleInfo>(role);
}

AutoMapper works just fine with Linq to SQL, but it can't be executed as part of the deferred query. Adding ToList() at the end of your Linq query causes it to immediately evaluate the results, instead of trying to translate the AutoMapper segment as part of the query.


Clarification

The notion of deferred execution (not "lazy load") does not make any sense once you've changed the resulting type to something that's not a data entity. Consider these two classes:

public class DB_RoleInfo
{
    public int ID { get; set; }
    public string Name { get; set; }
}

public class DO_RoleInfo
{
    public Role Role { get; set; }    // Enumeration type
}

Now consider the following mapping:

Mapper.CreateMap<DB_RoleInfo, DO_RoleInfo>
    .ForMember(dest => dest.Role, opt => opt.MapFrom(src =>
        (Role)Enum.Parse(typeof(Role), src.Name)));

This mapping is completely fine (unless I made a typo), but let's say you write the SelectAll method in your original post instead of my revised one:

public IQueryable<DO_RoleInfo> SelectAll()
{
    Mapper.CreateMap<DB_RoleInfo, DO_RoleInfo>();
    return from role in _ctx.DB_RoleInfo
           select Mapper.Map<DB_RoleInfo, DO_RoleInfo>(role);
}

This actually kind of works, but by calling itself a "queryable", it lies. What happens if I try to write this against it:

public IEnumerable<DO_RoleInfo> SelectSome()
{
    return from ri in SelectAll()
           where (ri.Role == Role.Administrator) ||
                 (ri.Role == Role.Executive)
           select ri;
}

Think really hard about this. How could Linq to SQL possibly be able to successfully turn your where into an actual database query?

Linq knows nothing about the DO_RoleInfo class. It doesn't know how to do the mapping backward - in some cases, that may not even possible. Sure, you may look at this code and go "Oh, that's easy, just search for 'Administrator' or 'Executive' in the Name column", but you're the only one who knows that. As far as Linq to SQL is concerned, the query is pure nonsense.

Imagine that somebody gave you these instructions:

Go to the supermarket and bring back the ingredients for making Morton Thompson Turkey.

Unless you've made it before, and most people haven't, your response to that instruction is most likely going to be:

  • What the hell is that?

You can go to the market, and you can get specific ingredients by name, but you can't evaluate the condition I've given you while you're over there. I have to "un-map" the criteria first. I have to tell you, here are the ingredients we need for this recipe - now go and get them.


To summarize, this is not some simple incompatibility between Linq to SQL and AutoMapper. It is not unique to either of those two libraries. It doesn't matter how you actually do the mapping to a non-entity type - you could just as easily do the mapping manually, and you'd still get the same error, because you are now giving Linq to SQL a set of instructions that are no longer comprehensible, dealing with mysterious classes that don't have an intrinsic mapping to any particular entity type.

This issue is fundamental to the concept of O/R Mapping and deferred query execution. A projection is a one-way operation. Once you project, you can no longer go back to the query engine and say oh by the way, here are some more conditions for you. It's too late. The best you can do is take what it already gave you and evaluate the extra conditions yourself.


Last but not least, I'll leave you with a workaround. If the only thing you want to be able to do from your mapping is filter the rows, you can write this:

public IEnumerable<DO_RoleInfo> SelectRoles(Func<DB_RoleInfo, bool> selector)
{
    Mapper.CreateMap<DB_RoleInfo, DO_RoleInfo>();
    return _ctx.DB_RoleInfo
        .Where(selector)
        .Select(dbr => Mapper.Map<DB_RoleInfo, DO_RoleInfo>(dbr));
}

This is a utility method that handles the mapping for you and accepts a filter on the original entity, and not the mapped entity. It might be useful if you have many different kinds of filters but always need to do the same mapping.

Personally, I think you will be better off just writing out the queries properly, by first determining what you need to retrieve from the database, then doing any projections/mappings, and then, finally, if you need to do further filtering (which you shouldn't), then materialize the results with ToList() or ToArray() and write more conditions against the local list.

Don't try to use AutoMapper or any other tool to hide the real entities exposed by Linq to SQL. The domain model is your public interface. The queries you write are an aspect of your private implementation. It's important to understand the difference and maintain a good separation of concerns.

Shivers answered 6/2, 2010 at 5:18 Comment(2)
Thank for your answer! But in the way you gave, the SelectAll will retrieve all records from DB. I'd like to enjoy the benefits of lazy load, It means that application will select one record from DB in SelectByKey but not all records in SelectAll.Inure
Gert Arnold's answer below is THE solution. For reference, here's the relevant API github.com/AutoMapper/AutoMapper/blob/master/src/AutoMapper/…Michale

© 2022 - 2024 — McMap. All rights reserved.