简体   繁体   中英

How to map multiple entities from LINQ query into a new entity using a common map method

The following code is very similar to what I have in my solution, the difference is only that values for "entitiesA" and "entitiesB" collection are stored in a SQL Server database using LINQ data context. The code works perfects in the console application, but when values are retrieved from database, the system says "Could not translate expression '...' into SQL and could not treat it as a local expression.". What I'm doing wrong?

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

namespace LinqSelectDemo
{
    class EntityA
    {
        public int ID { get; set; }
        public string StringValue { get; set; }
    }

    class EntityB
    {
        public int ID { get; set; }
        public int? EntityAID { get; set; }
        public int IntValue { get; set; }
    }

    class EntityC
    {
        public int ID { get; set; }
        public string StringValue { get; set; }
        public int IntValue { get; set; }
    }

    class Program
    {
        static List<EntityA> entitiesA;

        static List<EntityB> entitiesB;

        static EntityC MapEntityC(EntityB entityB, EntityA entityA = null)
        {
            if (entityA == null)
            {
                entityA = entitiesA.FirstOrDefault(entity => (entity.ID == entityB.EntityAID));
            }
            return new EntityC()
            {
                ID = ((entityA != null) ? entityA.ID : 0) + ((entityB != null) ? entityB.ID : 0),
                StringValue = (entityA != null) ? entityA.StringValue : null,
                IntValue = (entityB != null) ? entityB.IntValue : int.MinValue
            };
        }

        static void Main(string[] args)
        {
            entitiesA = new List<EntityA>()
            {
                new EntityA() { ID = 11, StringValue = "string1" },
                new EntityA() { ID = 12, StringValue = "string2" },
                new EntityA() { ID = 13, StringValue = "string3" },
            };
            entitiesB = new List<EntityB>()
            {
                new EntityB() { ID = 21, IntValue = 1, EntityAID = 11 },
                new EntityB() { ID = 22, IntValue = 2 },
                new EntityB() { ID = 23, IntValue = 3, EntityAID = 13 },
            };

            List<EntityC> entitiesC1 = entitiesB.GroupJoin(entitiesA, entityB => entityB.EntityAID, entityA => entityA.ID, (entityB, entityA) => new { entityB, entityA = entityA.SingleOrDefault() })
                                                .Select(entity => MapEntityC(entity.entityB, entity.entityA))
                                                .ToList();

            List<EntityC> entitiesC2 =
            (
                from entityB in entitiesB
                join entityA in entitiesA on entityB.EntityAID equals entityA.ID into tempEntitiesA
                from entityA in tempEntitiesA.DefaultIfEmpty()
                select MapEntityC(entityB, entityA)
            ).ToList();

            foreach (EntityC entityC in entitiesC1)
            {
                Console.WriteLine("ID: {0}, StringValue: '{1}', IntValue: {2}", entityC.ID, entityC.StringValue, entityC.IntValue);
            };

            Console.WriteLine();

            foreach (EntityC entityC in entitiesC2)
            {
                Console.WriteLine("ID: {0}, StringValue: '{1}', IntValue: {2}", entityC.ID, entityC.StringValue, entityC.IntValue);
            };
        }
    }
}

I don't think you should be creating a linq-to-objects and linq-to-entities compatible query. Even though they appear similar, functionally they are completely different. So with that said, my proposed solution is going to ignore the linq-to-objects side of things.


First step is to create a class that holds both of our Entities. In this case EntityA and EntityB

public class EntityBandA
{
   public EntityB EntityB { get;set; }
   public EntityA EntityA { get;set; }
}

Now we need to create an IQueryable transformation method. The purpose of this method is to translate our new IQueryable<EntityBandA> into an IQueryable<EntityC> . Note - this will only run against the database once we iterate against it (ie ToList , FirstOrDefault , etc...).

static IQueryable<EntityC> MapEntityC(IQueryable<EntityBandA> query)
{
  var query = from e in query
              select new EntityC()
              {
                ID = e.EntityA.ID + e.EntityB,
                StringValue = e.EntityA.StringValue,
                IntValue = e.EntityB != null ? entityB.IntValue : int.MinValue
            };
  return query;
}

Notice that we removed some of the null checks. They are not necessary when using linq-to-entities , because our expression is being translated into sql. If no value is found, it will choose the default. ( Int:0, String:null )


Now we need to adjust your current application to use our new method

IQueryable<EntityBandA> queryBandA = entitiesB.GroupJoin(entitiesA, entityB => entityB.EntityAID, entityA => entityA.ID, 
                                                 (entityB, entityA) => new EntityBandA 
                                                 { 
                                                    EntityB = entityB, 
                                                    EntityA = entityA.SingleOrDefault()
                                                 });

List<EntityC> entitiesC1 = MapEntityC(queryBandA).ToList();

It means Linq To SQL cannot map your call to MapEntity() to an equal SQL expression.

You need to fetch data from Linq To SQL (maybe by querying a VIEW that contains your JOIN , that is easy to do) and, after you have received it and it is in memory (in a List ), then map it to the EntityC data type.

ALTERNATIVELY You can just download the lists entitiesA and entitiesB by reading the whole tables into memory, something like this:

entitiesA = DbContext.EntitiesA.ToList();
entitiesB = DbContext.EntitiesB.ToList();

And then you can continue with the expressions that you have used, since you are now using in-memory lists and thus regular LINQ to Objects instead of LINQ to SQL.

Thank you to all of you for take a moment to write a possible solution and/or a comment. All ideas were fantastic. Actually the original code works as suppose to do it. The problem was that I was calling the mapping function not with trivial parameters but using a terciary operator expression. Something like:

Select(entity => MapEntityC(entity.entityB,
                           (entity.entityA != null) ? entity.entityA : new EntityA()));

That LINQ to SQL can't translate to SQL statement. As soon as I removed that expression and used a simple one, the code works. Once again thank you.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM