[NotMapped]标记对象列表属性引发EF Core查询翻译报错问题
问题描述
我有如下模型:
using System.ComponentModel.DataAnnotations.Schema; using Microsoft.EntityFrameworkCore; using MyProject.Class.Dto; namespace SageApi.Model; public partial class FArticle { public string ArRef { get; set; } = null!; [NotMapped] public int? Prices { get; set; } public static void ConfigureModelBuilder(ModelBuilder modelBuilder) { modelBuilder.Entity<FArticle>(entity => { entity.HasIndex(e => e.ArRef, "UKA_F_ARTICLE_AR_Ref").IsUnique(); entity.Property(e => e.ArRef) .HasMaxLength(19) .IsUnicode(false) .HasColumnName("AR_Ref"); }); } }
我使用HotChocolate Query通过GraphQL获取数据:
using SageApi.Model.Sage; namespace SageApi.Services.GraphQl; public class Query { [UseOffsetPaging(MaxPageSize = 100, IncludeTotalCount = true, DefaultPageSize = 20)] [UseProjection] [UseFiltering] [UseSorting] public IEnumerable<FArticle> GetFArticles([Service] SageDbContext context) => context.FArticles; }
执行以下查询时,一切正常,prices返回值为null:
{ fArticles( skip: 0 take: 1 ) { items { arRef prices } } }
现在我创建一个Dto:
namespace SageApi.Class.Dto; public class PriceDto { public short? SomeValue { get; set; } }
并将
[NotMapped] public int? Prices { get; set; }
修改为
[NotMapped] public IList<PriceDto>? Prices { get; set; }
此时执行查询
{ fArticles( skip: 0 take: 1 ) { items { arRef prices{ someValue } } } }
会出现错误:
The LINQ expression 'p1 => new PriceDto{ SomeValue = p1.SomeValue } ' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to 'AsEnumerable', 'AsAsyncEnumerable', 'ToList', or 'ToListAsync'.
为何我明明标记了[NotMapped]属性,.NET仍试图从数据库获取SomeValue?该问题在Prices为String或Int等简单类型时不会出现。我遗漏了什么?
补充说明
给PriceDto类添加NotMapped特性,无法解决问题:
using System.ComponentModel.DataAnnotations.Schema; namespace SageApi.Class.Dto; [NotMapped] public class PriceDto { public short? SomeValue { get; set; } }
在ModelBuilder中添加entity.Ignore(e => e.Prices);,同样无法解决问题:
using System.ComponentModel.DataAnnotations.Schema; using Microsoft.EntityFrameworkCore; using MyProject.Class.Dto; namespace SageApi.Model; public partial class FArticle { public string ArRef { get; set; } = null!; [NotMapped] public IList<PriceDto>? Prices { get; set; } public static void ConfigureModelBuilder(ModelBuilder modelBuilder) { modelBuilder.Entity<FArticle>(entity => { entity.HasIndex(e => e.ArRef, "UKA_F_ARTICLE_AR_Ref").IsUnique(); entity.Property(e => e.ArRef) .HasMaxLength(19) .IsUnicode(false) .HasColumnName("AR_Ref"); entity.Ignore(e => e.Prices); }); } }
补充说明2
已提供最小可复现示例
问题根源
问题出在HotChocolate的[UseProjection]特性和EF Core的查询翻译逻辑的交互上:
- 对于
int?这类简单类型的[NotMapped]属性,EF Core明确知道该字段不存在于数据库,HotChocolate在生成投影时,只会直接返回属性的默认值(比如null),不会触发EF的SQL翻译。 - 当属性变成
IList<PriceDto>这种复杂类型时,HotChocolate会生成一个投影表达式,试图从FArticle实体中构造PriceDto对象。但EF Core无法识别这个表达式是针对[NotMapped]属性的,仍然尝试将整个投影逻辑转换成SQL查询——而PriceDto的SomeValue字段根本不存在于数据库表中,自然会导致翻译失败。
[NotMapped]和entity.Ignore(e => e.Prices)只是告诉EF Core不要将该属性映射到数据库列,但无法阻止HotChocolate生成针对该属性的投影表达式,也无法让EF Core忽略这个投影的翻译尝试。
解决方案
1. 切换到客户端评估(简单直接,但需注意性能)
在查询方法中,先调用AsEnumerable()将EF的IQueryable转换为内存中的Enumerable,让EF先加载数据到内存,再由HotChocolate处理投影:
public IEnumerable<FArticle> GetFArticles([Service] SageDbContext context) => context.FArticles.AsEnumerable();
⚠️ 注意:这样会失去服务器端分页、过滤的优势——所有符合条件的数据都会先加载到内存,数据量大时会影响性能。
2. 使用GraphQL专用DTO层(推荐)
不要直接返回EF实体,而是创建一个专门用于GraphQL的DTO,在服务层完成实体到DTO的映射:
- 定义GraphQL DTO:
public class FArticleGraphDto { public string ArRef { get; set; } = null!; public IList<PriceDto>? Prices { get; set; } }
- 修改查询方法:
[UseOffsetPaging(MaxPageSize = 100, IncludeTotalCount = true, DefaultPageSize = 20)] [UseProjection] [UseFiltering] [UseSorting] public IEnumerable<FArticleGraphDto> GetFArticles([Service] SageDbContext context) => context.FArticles.Select(a => new FArticleGraphDto { ArRef = a.ArRef, Prices = null // 根据需求设置默认值或自定义逻辑 });
这样HotChocolate操作的是DTO,EF只会翻译针对FArticle的必要查询,不会涉及Prices属性的数据库操作。
3. 忽略GraphQL中的该属性投影
如果不需要查询Prices属性,可以在该属性上添加[GraphQLIgnore]特性,阻止HotChocolate为其生成投影:
[NotMapped] [GraphQLIgnore] public IList<PriceDto>? Prices { get; set; }
但这样就无法在GraphQL查询中获取该字段了,适合不需要暴露该属性的场景。
4. 自定义投影处理(进阶)
通过HotChocolate的投影扩展,自定义Prices属性的投影逻辑,告诉它直接返回默认值而不触发EF的查询:
可以使用[Projection]特性指定自定义表达式,确保EF不会尝试翻译该部分逻辑:
[NotMapped] [Projection("null")] public IList<PriceDto>? Prices { get; set; }
内容的提问来源于stack exchange,提问作者Lenny4

