实体无法在 LINQ to Entities 查询中构建

424

有一种实体类型叫做Product,由Entity Framework生成。 我编写了这个查询

public IQueryable<Product> GetProducts(int categoryID)
{
    return from p in db.Products
           where p.CategoryID== categoryID
           select new Product { Name = p.Name};
}
下面的代码会抛出以下错误:

"无法在 LINQ to Entities 查询中构造实体或复杂类型 Shop.Product"

var products = productRepository.GetProducts(1).Tolist();

但是当我使用select p而不是select new Product { Name = p.Name};时,它可以正常工作。

如何执行自定义选择操作?


System.NotSupportedException:“无法在LINQ to Entities查询中构造实体或复杂类型'StudentInfoAjax.Models.Student'。” - Md Wahid
你可以像下面被接受的答案一样将它映射到DTO,但也要确保这个模型没有在DB上下文中使用,否则你会得到相同的错误。 - donatasj87
14个回答

431

你不能(也不应该能够)将投影操作映射到实体上。但是,你可以将投影操作映射到一个匿名类型或DTO上:

public class ProductDTO
{
    public string Name { get; set; }
    // Other field you may need from the Product entity
}

你的方法将返回DTO对象的列表。

public List<ProductDTO> GetProducts(int categoryID)
{
    return (from p in db.Products
            where p.CategoryID == categoryID
            select new ProductDTO { Name = p.Name }).ToList();
}

168
我不明白为什么我不能做这件事... 这会非常有用... - Jonx
124
在实体框架中,映射实体基本上代表数据库表。如果你在一个映射实体上进行投影操作,你基本上是部分加载一个实体,这不是一个有效的状态。EF 将无法理解如何处理未来的实体更新(默认行为可能会用 null 或其他对象覆盖未加载的字段)。这将是一项危险的操作,因为你可能会冒着在数据库中丢失一些数据的风险,因此在 EF 中不允许部分加载实体(或在映射实体上进行投影)。 - Yakimych
29
@Yakimych的观点很有道理,但是如果你正在通过查询生成/创建某个聚合实体,并且完全意识到/打算创建一个全新的实体,然后再进行操作并最终添加,那么情况就有所不同。在这种情况下,你必须强制运行查询或将其推入dto,然后再将其转换为实体以进行添加——这非常令人沮丧。 - Cargowire
16
@Cargowire - 我同意,这种情况存在,当你知道自己在做什么但受到限制而无法完成时,这是非常令人沮丧的。然而,如果允许这样做,会有很多沮丧的开发人员抱怨他们的数据在尝试保存部分加载的实体时丢失。在我看来,一个会产生大量噪音的错误(抛出异常等)比可能导致难以跟踪和解释的隐藏错误的行为要好(在注意到丢失数据之前,一切似乎都很正常)。 - Yakimych
14
D.T.O - 数据传输对象,是一种设计模式,用于在应用程序的不同层之间传递数据。它们通常具有与数据库表或其他持久化机制中存储的数据相对应的属性,并被用于向其他层提供数据。 D.T.O 在各种编程语言和框架中得到广泛使用,使得代码更加模块化和可维护。 - tkerwood
显示剩余13条评论

291

您可以将数据投影到匿名类型中,然后再从匿名类型转换为模型类型。

public IEnumerable<Product> GetProducts(int categoryID)
{
    return (from p in Context.Set<Product>()
            where p.CategoryID == categoryID
            select new { Name = p.Name }).ToList()
           .Select(x => new Product { Name = x.Name });
}

编辑:由于这个问题引起了很多关注,我将更加具体。

您不能直接投射到模型类型中(EF限制),因此没有绕过这个问题的方法。唯一的方法是投射到匿名类型(第一次迭代),然后投射到模型类型(第二次迭代)。

请注意,以这种方式部分加载实体时,它们无法更新,因此应保持分离状态。

我从未完全理解为什么这不可能,并且本主题上的答案也没有给出强有力的反对理由(大多数是关于部分加载数据的)。在部分加载状态下无法更新实体是正确的,但是,这个实体会被分离,所以不可能意外尝试保存它们。

考虑我上面使用的方法:我们仍然有一个部分加载的模型实体作为结果。这个实体是分离的。

考虑这个(希望存在的)可能代码:

return (from p in Context.Set<Product>()
        where p.CategoryID == categoryID
        select new Product { Name = p.Name }).AsNoTracking().ToList();

这也可能导致一个分离的实体列表,因此我们不需要进行两次迭代。编译器会智能地看到已经使用了AsNoTracking(),这将导致分离的实体,因此它可以允许我们这样做。但是,如果省略了AsNoTracking(),它可能会抛出与现在相同的异常,警告我们需要足够具体地说明我们想要的结果。


4
如果您不需要/不关心想要投影的所选实体的状态,这是最干净的解决方案。 - Mário Meyrelles
2
当你不关心返回IEnumerable还是IQueryable时,这个解决方案对我很有用,所以我给你点赞。 - Michael Brennt
12
从技术上讲,模型类型的投影发生在查询之外,我认为还需要通过列表进行额外的迭代。我不会在我的代码中使用这个解决方案,但它是问题的解决方案。增加。 - 1c1cle
4
我更喜欢这种解决方案,而不是已经被接受的DTO方案——更加优雅和简洁。 - Adam Hey
7
尊重地说,除此之外,这并不是对问题的回答。这是关于如何进行Linq To Objects投影的回答,而不是Linq to Entities查询投影的回答。因此,在Linq to Entities方面,DTO选项是唯一的选择。 - rism
显示剩余10条评论

87

我发现还有另一种方法,你需要建立一个继承自你的Product类的子类并使用它。例如:

public class PseudoProduct : Product { }

public IQueryable<Product> GetProducts(int categoryID)
{
    return from p in db.Products
           where p.CategoryID== categoryID
           select new PseudoProduct() { Name = p.Name};
}

不确定这是否“被允许”,但它是有效的。


4
聪明!我现在尝试了一下,它有效。虽然我敢肯定它会以某种方式伤害到我。 - Daniel
6
顺便提一句,如果你尝试持久化GetProducts()的结果,这会对你产生影响,因为EF无法找到PseudoProduct的映射,例如:"System.InvalidOperationException: 无法找到'blah.PseudoProduct'实体类型的映射和元数据信息。" - sming
5
最佳答案,也是唯一一个在问题参数范围内作出回答的。所有其他答案都更改了返回类型或过早执行了IQueryable并使用了对象的Linq操作。 - rdans
2
100%震惊它能工作...在EF 6.1中,这是可行的。 - TravisWhidden
2
@mejobloggs 尝试在派生类上使用[NotMapped]属性,或者如果您正在使用流畅的API,则使用.Ignore<T>。 - Dunc
显示剩余5条评论

37

以下是一种不需要声明额外类的方法:

public List<Product> GetProducts(int categoryID)
{
    var query = from p in db.Products
            where p.CategoryID == categoryID
            select new { Name = p.Name };
    var products = query.ToList().Select(r => new Product
    {
        Name = r.Name;
    }).ToList();

    return products;
}

然而,这只有在您想要将多个实体合并为单个实体时才使用。上述功能(简单的产品到产品映射)可以像这样完成:

public List<Product> GetProducts(int categoryID)
{
    var query = from p in db.Products
            where p.CategoryID == categoryID
            select p;
    var products = query.ToList();

    return products;
}

23

另一种简单的方式 :)

public IQueryable<Product> GetProducts(int categoryID)
{
    var productList = db.Products
        .Where(p => p.CategoryID == categoryID)
        .Select(item => 
            new Product
            {
                Name = item.Name
            })
        .ToList()
        .AsQueryable(); // actually it's not useful after "ToList()" :D

    return productList;
}

很好,我从你的回复中学到了一些关于IQueryable的知识。不过如果你能解释一下为什么在使用ToList()之后它就没用了就更好了。原因是你不能在LINQ-to-SQL查询中使用泛型列表。所以,如果你知道你总是会将结果推入调用者的另一个查询中,那么使用IQueryable肯定是有意义的。但如果不是这样...如果你之后要将其用作通用列表,则在方法内部使用ToList(),这样你就不必在每次调用此方法时都对IQueryable进行ToList()操作了。 - PositiveGuy
你完全没问题,我的朋友。我只是模仿问题方法签名,因此我将其转换为可查询的... ;) - Soren
1
这样做有效,使用ToList()后,productList 变成了不可编辑状态。我如何使其可编辑? - doncadavona
如果在查询中放置.ToList,它将被执行并从服务器提取数据,那么再次使用AsQueryable的意义是什么? - Moshi
1
@Moshii 只是为了满足方法返回类型签名(正如我在答案中所说,它已经不再有用)。 - Soren

4
你可以使用这个,它应该可以工作 --> 在使用 select 前,你必须使用 toList 将其转换为列表:
db.Products
    .where(x=>x.CategoryID == categoryID).ToList()
    .select(x=>new Product { Name = p.Name}).ToList(); 

8
然而,这仍然会执行 'SELECT * FROM [..]',而不是 'SELECT name FROM [..]'。 - Timo Hermans

1
您可以通过使用数据传输对象(DTO)来解决此问题。
这些类似于视图模型,您可以在其中放置所需属性,并且可以通过手动映射控制器或使用第三方解决方案(如AutoMapper)进行映射。
使用DTO,您可以:
  • 使数据可序列化(Json)
  • 消除循环引用
  • 通过留下不需要的属性(视图模型方式)来减少网络流量
  • 使用对象展平
我今年在学校学习了这个,它是一个非常有用的工具。

1

针对被标记为重复的另一个问题(点击此处),我根据Soren的答案想出了一种快速简便的解决方案:

data.Tasks.AddRange(
    data.Task.AsEnumerable().Select(t => new Task{
        creator_id   = t.ID,
        start_date   = t.Incident.DateOpened,
        end_date     = t.Incident.DateCLosed,
        product_code = t.Incident.ProductCode
        // so on...
    })
);
data.SaveChanges();

注意: 此解决方案仅适用于Task类上具有导航属性(外键)的情况(此处称为“Incident”)。 如果没有该属性,您可以使用其他已发布的解决方案之一,并使用“AsQueryable()”。

0
在许多情况下,转换是不必要的。考虑一下你想要强类型列表的原因,并评估一下你是否只想要数据,例如用于 Web 服务或显示它。类型并不重要。您只需要知道如何读取它并检查它是否与您定义的匿名类型中定义的属性相同。这是最理想的情况,因为有时不需要实体的所有字段,这就是匿名类型存在的原因。
一个简单的方法是这样做:
IEnumerable<object> list = dataContext.Table.Select(e => new { MyRequiredField = e.MyRequiredField}).AsEnumerable();

0

如果您正在执行 Linq to Entity,则无法在查询的 select 闭包中使用带有 newClassType,只允许使用匿名类型(不带类型的 new)。

请看一下我的项目代码片段:

//...
var dbQuery = context.Set<Letter>()
                .Include(letter => letter.LetterStatus)
                .Select(l => new {Title =l.Title,ID = l.ID, LastModificationDate = l.LastModificationDate, DateCreated = l.DateCreated,LetterStatus = new {ID = l.LetterStatusID.Value,NameInArabic = l.LetterStatus.NameInArabic,NameInEnglish = l.LetterStatus.NameInEnglish} })
                               ^^ without type__________________________________________________________________________________________________________^^ without type

如果您在Select闭包中添加了new关键字,即使在复杂属性上,您也会遇到此错误

所以,在Linq to Entity查询中,删除new关键字的ClassTypes

因为它将被转换为SQL语句并在SqlServer上执行

那么什么情况下可以在select闭包中使用new和types

如果您正在处理LINQ to Object(内存集合),则可以使用它

//opecations in tempList , LINQ to Entities; so we can not use class types in select only anonymous types are allowed
var tempList = dbQuery.Skip(10).Take(10).ToList();// this is list of <anonymous type> so we have to convert it so list of <letter>

//opecations in list , LINQ to Object; so we can use class types in select
list = tempList.Select(l => new Letter{ Title = l.Title, ID = l.ID, LastModificationDate = l.LastModificationDate, DateCreated = l.DateCreated, LetterStatus = new LetterStatus{ ID = l.LetterStatus.ID, NameInArabic = l.LetterStatus.NameInArabic, NameInEnglish = l.LetterStatus.NameInEnglish } }).ToList();
                                ^^^^^^ with type 

在查询上执行ToList后,它变成了内存集合,因此我们可以在选择中使用new ClassTypes


当然,您可以使用匿名类型,但是您不能在LINQ查询中创建实体,即使是为了设置匿名成员,因为LINQ-to-Entities仍会抛出相同的异常。 - Suncat2000

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接