InvalidCastException 运行 使用 InMemoryDatabase、EF Core 和 AutoMapper 的集成测试

InvalidCastException running integration tests using InMemoryDatabase, EF Core, and AutoMapper

我们有一个具有多个工作端点和集成测试的 API 应用程序,但是我们的一组测试失败并出现以下错误(仅集成测试失败。应用程序本身正常运行):

System.InvalidCastException : Unable to cast object of type 'System.Linq.Expressions.NewExpression' to type 'System.Linq.Expressions.MethodCallExpression'.

当我们将以下项目添加到我们的映射配置文件时会产生此错误(请参阅下面的 code/explanation):

.ForMember(x => x.MaterialKeys, d => d.MapFrom(a => a.ItemMaterial.Select(x => x.MaterialKey)))

我们正在使用:

这是一个失败的测试,它在行 Assert.IsNotNull(response.Result.Data) 处失败(所有代码片段都是简化版本):

public void MyIntegrationTest()
{
      // Arrange 
      var options = GetContextOptions();

      using var context = new MyContext(options);
      Setup2ItemsContext(context, out Item myItem);

      var handler = new GetItemHandler(context, Mapper);
      var request = new GetItemRequest { PageNumber = 1, PageSize = 5 };

      // Act
      var response = handler.HandleAsync(request);

      //Assert 
      Assert.IsNotNull(response.Result.Data);
}

private static void Setup2ItemsContext(MyContext context, out Item myItem)
{
      myItem = new Item
      {
          ItemKey = 1,
          ItemMaterial = new List<ItemMaterial>()
          { 
              new ItemMaterial 
              {
                  MaterialKey = 1,
                  ItemKey = 1
              }
          }
      };
      context.Add(myItem);

      var myItem2 = new Item
      {
          ItemKey = 2,
          ItemMaterial = new List<ItemMaterial>()
          { 
              new ItemMaterial 
              {
                  MaterialKey = 2,
                  ItemKey = 2
              }
          }
      };
      context.Add(myItem2);

      context.SaveChanges();
}

这是映射:

public class MappingProfiles : Profile
{
    public MappingProfiles()
    {
        CreateMap<Item, ItemDto>()
           .ForMember(x => x.ItemKey, d => d.MapFrom(a => a.ItemKey))
           .ForMember(x => x.MaterialKeys, d => d.MapFrom(a => a.ItemMaterial.Select(x => x.MaterialKey)));
    }
}

这里是 ItemDto class:

public class ItemDto
{
    public int ItemKey { get; set; }
    public List<int> MaterialKeys { get; set; }
}

这里是 ItemItemMaterial 上下文 classes:

public partial class Item
{
    public Item()
    {
        ItemMaterial = new HashSet<ItemMaterial >();
    }
    public int? ItemKey { get; set; }
    public virtual ICollection<ItemMaterial> ItemMaterial { get; set; }
}

public partial class ItemMaterial
{
    public ItemMaterial()
    {
        // irrelevant code here
    }
    public int ItemKey { get; set; }
    public int MaterialKey { get; set; }
    public virtual Item ItemKeyNavigation { get; set; }
}

当我们 运行 集成测试时,上面的代码给出了 InvalidCastExceptionError。但是,如果我们从映射中删除此行,则测试 运行 没有错误 .ForMember(x => x.MaterialKeys, d => d.MapFrom(a => a.ItemMaterial.Select(x => x.MaterialKey)))

测试也 运行 没有错误 如果我们将映射更改为包含以下行: .ForMember(x => x.MaterialKeys, d => d.MapFrom(a => a.ItemMaterial.Select(x => x.MaterialKey).Count())),并将 ItemDto 中的 MaterialKeys 改为 int 而不是 List<int>

这是堆栈跟踪:

  Message: 
    System.AggregateException : One or more errors occurred. (Unable to cast object of type 'System.Linq.Expressions.NewExpression' to type 'System.Linq.Expressions.MethodCallExpression'.)
      ----> System.InvalidCastException : Unable to cast object of type 'System.Linq.Expressions.NewExpression' to type 'System.Linq.Expressions.MethodCallExpression'.
  Stack Trace: 
    Task.ThrowIfExceptional(Boolean includeTaskCanceledExceptions)
    Task`1.GetResultCore(Boolean waitCompletionNotification)
    Task`1.get_Result()
    GetItemHandlerTests.MyIntegrationTest() line 177
    --InvalidCastException
    InMemoryQueryExpression.AddSubqueryProjection(ShapedQueryExpression shapedQueryExpression, Expression& innerShaper)
    InMemoryProjectionBindingExpressionVisitor.Visit(Expression expression)
    InMemoryProjectionBindingExpressionVisitor.VisitMemberAssignment(MemberAssignment memberAssignment)
    ExpressionVisitor.VisitMemberBinding(MemberBinding node)
    InMemoryProjectionBindingExpressionVisitor.VisitMemberInit(MemberInitExpression memberInitExpression)
    MemberInitExpression.Accept(ExpressionVisitor visitor)
    ExpressionVisitor.Visit(Expression node)
    InMemoryProjectionBindingExpressionVisitor.Visit(Expression expression)
    InMemoryProjectionBindingExpressionVisitor.Translate(InMemoryQueryExpression queryExpression, Expression expression)
    InMemoryQueryableMethodTranslatingExpressionVisitor.TranslateSelect(ShapedQueryExpression source, LambdaExpression selector)
    QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
    InMemoryQueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
    MethodCallExpression.Accept(ExpressionVisitor visitor)
    ExpressionVisitor.Visit(Expression node)
    QueryCompilationContext.CreateQueryExecutor[TResult](Expression query)
    Database.CompileQuery[TResult](Expression query, Boolean async)
    QueryCompiler.CompileQueryCore[TResult](IDatabase database, Expression query, IModel model, Boolean async)
    <>c__DisplayClass12_0`1.<ExecuteAsync>b__0()
    CompiledQueryCache.GetOrAddQuery[TResult](Object cacheKey, Func`1 compiler)
    QueryCompiler.ExecuteAsync[TResult](Expression query, CancellationToken cancellationToken)
    EntityQueryProvider.ExecuteAsync[TResult](Expression expression, CancellationToken cancellationToken)
    EntityQueryable`1.GetAsyncEnumerator(CancellationToken cancellationToken)
    ConfiguredCancelableAsyncEnumerable`1.GetAsyncEnumerator()
    EntityFrameworkQueryableExtensions.ToListAsync[TSource](IQueryable`1 source, CancellationToken cancellationToken)
    ICollectionExtensions.ToPagedResult[T,TOut](IQueryable`1 source, IMapper mapper, Int32 pageNumber, Int32 pageSize, CancellationToken token) line 37
    GetItemHandler.GetItemQuery(GetItemRequest request) line 155
    GetItemHandler.HandleAsync(GetItemRequest request) line 94

编辑:这是一些额外的(简化的)代码

这是 GetItemRequest class:

public class GetItemRequest : IRequest<PagedQueryResult<ItemDto>>
{
    public int ItemKey { get; set; }
    public int PageSize { get; set; }
    public int PageNumber { get; set; }
}

这是 GetItemHandler class:

public class GetItemHandler : IRequestHandler<GetItemRequest,PagedQueryResult<ItemDto>>
{
    private readonly MyContext _context;
    private readonly IMapper _mapper;

    public GetItemHandler(MyContext context,
        IMapper mapper)
    {
        _context = context;
        _mapper = mapper;
    }

    public async Task<Response<PagedQueryResult<ItemDto>>> HandleAsync(GetItemRequest request)
    {
        var response = await GetItemQuery(request);
        return response.AsResponse();
    }

    private async Task<PagedQueryResult<ItemDto>> GetItemQuery(GetItemRequest request)
    {
        var items = _context.Set<Item>().AsQueryable();
        if (request.ItemKey != default)
            items = items.Where(x => x.ItemKey == request.ItemKey);

        items = items.OrderBy(x => x.ItemKey).AsQueryable();

        var response = await items.ToPagedResult<Item,ItemDto>(
            _mapper,
            request.PageNumber,
            request.PageSize);

        return response;
    }
}

这里是 ToPagedResult():

public static async Task<PagedQueryResult<TOut>> ToPagedResult<T, TOut>(
    this IQueryable<T> source,
    IMapper mapper,
    int pageNumber,
    int pageSize,
    CancellationToken token = default)
{
    var totalItemCount = await source.CountAsync(token);
    var totalPageCount = (totalItemCount + pageSize - 1) / pageSize;
    var startIndex = (pageNumber - 1) * pageSize;

    var items = await source
        .Skip(startIndex)
        .Take(pageSize)
        .ProjectTo<TOut>(mapper.ConfigurationProvider)
        .ToListAsync(token);

    return new PagedQueryResult<TOut>
    {
        Items = items,
        PageCount = totalPageCount,
        PageNumber = pageNumber,
        PageSize = pageSize,
        TotalItemCount = totalItemCount
    };
}

编辑2:

如果您收到相同的异常 System.InvalidCastException : Unable to cast object of type 'System.Linq.Expressions.NewExpression' to type 'System.Linq.Expressions.MethodCallExpression' 但您在映射中 Select 之后使用 .Count(),那么根据@AntiqTech:

Please check this github discussion : https://github.com/dotnet/efcore/issues/17620 Exception is exactly what you get. If you're using Count() in a subquery , jgbpercy suggest to wrap it with Convert.ToInt32.

这是由于 EF Core 中的一个错误,应该由 6.0.0 版修复(参见此处的线程:https://github.com/dotnet/efcore/issues/24047

我 运行 进入相同的 NewExpressionMethodCallExpression 抛出异常,同时将 .Any 链投影到 bool,像这样:

.Select(q => new AlreadyDefinedClass
{
   HasAnyThings = q.Parents.Any(p => p.Children.Any(c => c.Answers.Any(a => a.IsThing)))
}

这在运行时有效,但在使用 In-Memory 提供程序的单元测试中失败。

我发现 github 问题 24047, which then linked to 24092,然后说:

Problem was that EF wasn't correctly compensating for nullability change of a non-nullable property projected from a left join (after expand). Workaround is to make that property nullable.

AlreadyDefinedClass.HasAnyThings 更改为 bool? 确实使单元测试通过,但导致下游运行时问题。因此,我将嵌套的 .Any 语句向上移动到一个匿名投影中,并将其转换为可为空的布尔值,然后将其合并到具体投影中,如下所示:

.Select(q => new
{
   HasAnyThings = (bool?)q.Parents.Any(p => p.Children.Any(c => c.Answers.Any(a => a.IsThing)))
}
.Select(q => new AlreadyDefinedClass
{
   HasAnyThings = q.HasAnyThings ?? false
}

除了提到的可空性补偿外,这对生成的 SQL 没有影响:

希望能帮助遇到此问题的任何人。