“ProjectTo”和 Nullable 类型之后的“Where”子句?

Posted

技术标签:

【中文标题】“ProjectTo”和 Nullable 类型之后的“Where”子句?【英文标题】:"Where" clause after "ProjectTo" and Nullable types? 【发布时间】:2021-07-30 07:22:26 【问题描述】:

使用 ASP.NET Core 5 和 EF Core 5 后端处理 REST API。

我得到了以下实体、DTO 和映射(仅包括相关代码):

// Entities
//
class Book 
  public int Id  get; set; 
  public string Title  get; set; 
  public int AuthorId  get; set;    // foreing-key
  public Author Author  get; set;   // nav prop


class Author 
  public int Id  get; set; 
  public string Name  get; set; 


(assume proper EF config to indicate HasOne/HasMany WithOne/WithMany accordingly)

// DTOs
//
class AuthorDTO  ... 

class BookDTO 
  ...
  AuthorDTO Author  get; set; 


// Automapper Maps
//
CreateMap<Book, BookDTO>();
CreateMap<Author, AuthorDTO>();

如果我运行下面的代码,生活是美好的。事实上,对 BookDTO 的根级字段进行的任何过滤都运行良好:

var data = await dbContex
  .Books
  .ProjectTo<BookDTO>(mapper.ConfigurationProvider)
  .Where(bDto => bDto.Id == 4)
  .ToListAsync();

只要字段是可为空的或引用类型,对嵌套的 AuthorDTO 字段进行的过滤就可以工作。例如。

var data = await dbContex
  .Books
  .ProjectTo<BookDTO>(mapper.ConfigurationProvider)
  .Where(bDto => bDto.Author.Name == "John")
  .ToListAsync();

但是,对不可为空或值类型的查询会失败:

var data = await dbContex
  .Books
  .ProjectTo<BookDTO>(mapper.ConfigurationProvider)
  .Where(bDto => bDto.Author.Id == 10)
  .ToListAsync();

错误:

"The binary operator Equal is not defined for the types 'System.Nullable`1[System.Int32]' and 'System.Int32'."

如果我将 AuthorDTO Id 声明为 int?,则代码有效。

我已经在 Where 子句中尝试了几种类型转换组合。

欢迎提出任何建议。

PS:如果我将 Where 子句放在 ProjectTo 投影之前并根据实体字段而不是 DTO 字段进行所有过滤,则不会发生这些问题。对于那些想知道我为什么要基于 DTO 进行过滤的人:我正在使用 Sieve,这是一个允许我进行过滤和分页“a la OData”的包,以及客户端在调用我的 API 时收到的字段是 DTO 中的那些,所以我真的需要在 ProjectTo 之后应用所有查询。

【问题讨论】:

Book.Author 是必需的还是可选的,即是否允许为 null 我刚刚更新了原始问题。是的,这是必需的。 Automapper 是额外的故障点,尤其是对于 OData 端点。尝试手动进行投影并检查它是如何在没有 Automapper 的情况下工作的。可能 Automapper 会生成无法转换为 SQL 的投影。 @SvyatoslavDanyliv 实际上这是令人讨厌的 EFC 查询翻译错误/限制。实际上有几个错误,因为通常与实体导航属性一起使用的技巧,比如在这里转换为可空的(int?)xAuthor.Id == 10 会产生表达式访问者异常(不包括来自 OP 的也深入到表达式访问者的异常)。 【参考方案1】:

一般来说,问题出在 EF Core 中,因为 AutoMapper ProjectTo 只会生成一个 Select,您可以手动执行此操作,但仍会遇到同样的问题。

问题在于可空引用。默认情况下 AutoMapper 假定任何引用类型属性 allows null,因此生成的投影是这样的(注意条件运算符和 null 检查):

Author = src.Author == null ? null : new AuthorDto

    Id = src.Author.Id,
    Name = src.Author.Name,

一旦你有了,任何尝试在生成的Author 属性不可为空成员上应用额外的 LINQ 运算符(WhereOrderBy 等)都会遇到令人不快的 EF Core 错误/限制(因为用于常规实体参考没有投影的导航属性,它可能以不同的方式处理并且以某种方式起作用)。

话虽如此,解决方案是什么?如果该属性是可选的(允许null),则无能为力(实际上有一个丑陋的解决方法,需要手动扩展导航属性,这会扼杀导航属性的所有好处,并且通常不适用于 AutoMapper)。

但是如果需要属性(不允许null,即源来自required relationship),那么解决方法就是去掉条件运算符,直接生成

Author = new AuthorDto

    Id = src.Author.Id,
    Name = src.Author.Name,

其中用AutoMapper可以通过两种方式实现。首先是更改所有引用类型属性的默认值

AllowNullDestinationValues = false; // Default is true

(集合有一个类似的属性称为AllowNullCollections,默认情况下也是true,设置为false 是有意义的,因为在LINQ to Entities 中集合永远不会null

另一种方法是保留默认值并为每个属性单独配置:

CreateMap<Book, BookDto>()
    .ForMember(dst => dst.Author, opt => opt.DoNotAllowNull());

实际上还有第三个选项——更改默认值,然后使用opt.AllowNull() 覆盖它,仅用于可选属性。

请记住 - 无论您使用什么选项,如果您有可选的引用属性类型,在 EF Core 提供null 投影问题的解决方案之前,它将不起作用。

【讨论】:

不错的答案,我完全想到了额外的空值检查,可以通过 Automapper 添加。 @SvyatoslavDanyliv 我猜翻译这种表达式树存在技术上的困难。但你知道得更好:-) 是的,它增加了翻译的复杂性,我们检测到这种模式并从 SQL 翻译过程中删除。 @IvanStoev 感谢您的回答! EF Core Github 上是否存在未解决的问题,通过涉及空值的预测跟踪此问题? @gsandorx 我不确定。过去我在github.com/dotnet/efcore/issues/22792(powermetal63 有我)发布了一个与此非常相似(如果不完全相同)的问题,但它已经关闭,我没有坚持更进一步。您可以搜索或发布一个新的 - 如果存在重复,请确保他们将其关闭。

以上是关于“ProjectTo”和 Nullable 类型之后的“Where”子句?的主要内容,如果未能解决你的问题,请参考以下文章

使用 AutoMapper 的 ProjectTo 和扩展选项时,每个请求都会增加内存

aufomaper Queryable Extensions ProjectTo

解决 AutoMapper ProjectTo 不起作用的问题

C# 8 中 Nullable 类型和泛型的问题

YYModel 源码解读之YYClassInfo.h

C#可空类型(Nullable)