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)))
我们正在使用:
- 网络核心 3.1
- Microsoft.EntityFrameworkCore 3.1.4(我们尝试升级到 5.0.2 时出现同样的错误)
- AutoMapper 9.0.0
- Microsoft.EntityFrameworkCore.InMemory 3.1.4(我们尝试升级到 5.0.2 时出现同样的错误)
- Visual Studio 社区 2019 16.8.3
这是一个失败的测试,它在行 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; }
}
这里是 Item
和 ItemMaterial
上下文 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)
我 运行 进入相同的 NewExpression
到 MethodCallExpression
抛出异常,同时将 .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 没有影响:
希望能帮助遇到此问题的任何人。
我们有一个具有多个工作端点和集成测试的 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)))
我们正在使用:
- 网络核心 3.1
- Microsoft.EntityFrameworkCore 3.1.4(我们尝试升级到 5.0.2 时出现同样的错误)
- AutoMapper 9.0.0
- Microsoft.EntityFrameworkCore.InMemory 3.1.4(我们尝试升级到 5.0.2 时出现同样的错误)
- Visual Studio 社区 2019 16.8.3
这是一个失败的测试,它在行 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; }
}
这里是 Item
和 ItemMaterial
上下文 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)
我 运行 进入相同的 NewExpression
到 MethodCallExpression
抛出异常,同时将 .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 没有影响:
希望能帮助遇到此问题的任何人。