views:

640

answers:

1

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.

+7  A: 

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.

Aaronaught
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.
Sam Zhou