"Where" 子句在 "ProjectTo" 和 Nullable 类型之后?

"Where" clause after "ProjectTo" and Nullable types?

使用 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 字段进行所有过滤,就会发生 None 这些问题。对于那些想知道为什么我要基于 DTO 进行过滤的人:我正在使用 Sieve,一个允许我进行过滤和分页“a la OData”的包,以及客户端在调用我的 API 是 DTO 中的那些,所以我真的需要在 ProjectTo 之后应用所有查询。

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

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

Author = src.Author == null ? null : new AuthorDto
{
    Id = src.Author.Id,
    Name = src.Author.Name,
}

一旦你有,任何尝试在生成的 Author 属性 不可空成员上应用额外的 LINQ 运算符(WhereOrderBy 等)都会命中令人不快的 EF Core bug/limitation(因为对于常规实体引用导航属性 w/o 投影,它的处理方式可能不同并且以某种方式起作用)。

话虽如此,解决方案是什么?如果 属性 是可选的(允许 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 中实体集合永远不会 null)

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

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

实际上还有第三种选择 - 更改默认值,然后使用 opt.AllowNull() 仅针对可选属性覆盖它。

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