You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

带投影的IQueryable Union在SQLite测试报错,SQL Server运行正常

问题

将两个IQueryable查询投影至同一ViewModel类型后执行Union操作并排序,该逻辑在SQL Server生产环境正常运行,但在SQLite集成测试中失败。

示例实体

public class EntityA
{
    public Guid Id { get; set; }
    public string Name { get; set; } = "";
    public string EntityASpecificLabel { get; set; } = "";
}

public class EntityB
{
    public Guid Id { get; set; }
    public string Name { get; set; } = "";
    public string EntityBSpecificLabel { get; set; } = "";
}

ViewModel定义

public class ViewModel
{
    public Guid Id { get; set; }
    public string Name { get; set; } = "";
    public string EntityASpecificLabel  { get; set; } = "";
    public string EntityBSpecificLabel { get; set; } = "";
}

查询代码

var queryA = context.EntityAs
                    .Where(...)
                    .Select(x => new ViewModel
                             {
                                 Id = x.Id,
                                 Name = x.Name,
                                 EntityASpecificLabel = Convert.ToString(x.EntityASpecificLabel),
                                 EntityBSpecificLabel = ""
                             });

var queryB = context.EntityBs
                    .Where(...)
                    .Select(x => new ViewModel
                             {
                                 Id = x.Id,
                                 Name = x.Name,
                                 EntityASpecificLabel = "",
                                 EntityBSpecificLabel = Convert.ToString(x.EntityBSpecificLabel)
                             });

queries = queryA.Union(queryB)
                .OrderBy(x => x.Name)
                .ThenBy(x => x.EntityASpecificLabel)
                .ThenBy(x => x.EntityBSpecificLabel);

var result = await queries.ToListAsync();

报错信息

System.InvalidOperationException : Unable to translate set operation after client projection has been applied. Consider moving the set operation before the last 'Select' call.
   at Microsoft.EntityFrameworkCore.Query.SqlExpressions.SelectExpression.ApplySetOperation(SetOperationType setOperationType, SelectExpression select2, Boolean distinct)
   at Microsoft.EntityFrameworkCore.Query.SqlExpressions.SelectExpression.ApplyUnion(SelectExpression source2, Boolean distinct)
   at Microsoft.EntityFrameworkCore.Query.RelationalQueryableMethodTranslatingExpressionVisitor.TranslateUnion(ShapedQueryExpression source1, ShapedQueryExpression source2)
   at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
   at System.Linq.Expressions.MethodCallExpression.Accept(ExpressionVisitor visitor)
   at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
   at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
   at System.Linq.Expressions.MethodCallExpression.Accept(ExpressionVisitor visitor)
   at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
   at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
   at System.Linq.Expressions.MethodCallExpression.Accept(ExpressionVisitor visitor)
   at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
   at Microsoft.EntityFrameworkCore.Query.QueryableMethodTranslatingExpressionVisitor.VisitMethodCall(MethodCallExpression methodCallExpression)
   at System.Linq.Expressions.MethodCallExpression.Accept(ExpressionVisitor visitor)
   at System.Linq.Expressions.ExpressionVisitor.Visit(Expression node)
   at Microsoft.EntityFrameworkCore.Query.QueryCompilationContext.CreateQueryExecutor[TResult](Expression query)
   at Microsoft.EntityFrameworkCore.Storage.Database.CompileQuery[TResult](Expression query, Boolean async)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.CompileQueryCore[TResult](IDatabase database, Expression query, IModel model, Boolean async)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.<>c__DisplayClass12_0`1.<ExecuteAsync>b__0()
   at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryCache.GetOrAddQuery[TResult](Object cacheKey, Func`1 compiler)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.ExecuteAsync[TResult](Expression query, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.ExecuteAsync[TResult](Expression expression, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1.GetAsyncEnumerator(CancellationToken cancellationToken)
   at LinqKit.ExpandableQuery`1.System.Collections.Generic.IAsyncEnumerable<T>.GetAsyncEnumerator(CancellationToken cancellationToken)
   at System.Runtime.CompilerServices.ConfiguredCancelableAsyncEnumerable`1.GetAsyncEnumerator()
   at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ToListAsync[TSource](IQueryable`1 source, CancellationToken cancellationToken)

排查后确认问题出在ViewModel中EntityASpecificLabel和EntityBSpecificLabel的投影逻辑,注释这些字段及对应排序后查询可正常运行,需解决SQL Server与SQLite的兼容差异。


解决方案

方案1:调整查询顺序,先Union再投影

EF Core对不同数据库的SQL翻译能力存在差异,SQLite无法正确解析「先投影再Union」的逻辑,而SQL Server支持。按照报错提示的建议,先对原始实体的查询执行Union,再统一投影到ViewModel:

// 先从两个实体中取出需要的字段,构造匿名类型执行Union
var unionQuery = context.EntityAs
    .Where(...)
    .Select(x => new 
    {
        x.Id,
        x.Name,
        EntityASpecificLabel = x.EntityASpecificLabel,
        EntityBSpecificLabel = (string)null // 用null代替空字符串,后续统一处理
    })
    .Union(
        context.EntityBs
            .Where(...)
            .Select(x => new 
            {
                x.Id,
                x.Name,
                EntityASpecificLabel = (string)null,
                EntityBSpecificLabel = x.EntityBSpecificLabel
            })
    );

// 再投影到ViewModel,并将null转换为空字符串
var queries = unionQuery
    .Select(x => new ViewModel
    {
        Id = x.Id,
        Name = x.Name,
        EntityASpecificLabel = x.EntityASpecificLabel ?? "",
        EntityBSpecificLabel = x.EntityBSpecificLabel ?? ""
    })
    .OrderBy(x => x.Name)
    .ThenBy(x => x.EntityASpecificLabel)
    .ThenBy(x => x.EntityBSpecificLabel);

var result = await queries.ToListAsync();

方案2:改用Concat替代Union(无需去重场景)

如果业务逻辑不需要Union的去重特性,改用Concat可以避免SQLite的翻译问题——Concat的SQL翻译逻辑更简单,兼容性更好:

var queryA = context.EntityAs
    .Where(...)
    .Select(x => new ViewModel
    {
        Id = x.Id,
        Name = x.Name,
        EntityASpecificLabel = x.EntityASpecificLabel,
        EntityBSpecificLabel = ""
    });

var queryB = context.EntityBs
    .Where(...)
    .Select(x => new ViewModel
    {
        Id = x.Id,
        Name = x.Name,
        EntityASpecificLabel = "",
        EntityBSpecificLabel = x.EntityBSpecificLabel
    });

// 用Concat替换Union
var queries = queryA.Concat(queryB)
    .OrderBy(x => x.Name)
    .ThenBy(x => x.EntityASpecificLabel)
    .ThenBy(x => x.EntityBSpecificLabel);

var result = await queries.ToListAsync();

方案3:客户端执行Union(小数据量场景)

如果数据量不大,可以先将两个查询的结果加载到客户端内存,再执行Union和排序。此方案适合集成测试等小数据量场景,大数据量下会引发性能问题:

var listA = await context.EntityAs
    .Where(...)
    .Select(x => new ViewModel
    {
        Id = x.Id,
        Name = x.Name,
        EntityASpecificLabel = x.EntityASpecificLabel,
        EntityBSpecificLabel = ""
    })
    .ToListAsync();

var listB = await context.EntityBs
    .Where(...)
    .Select(x => new ViewModel
    {
        Id = x.Id,
        Name = x.Name,
        EntityASpecificLabel = "",
        EntityBSpecificLabel = x.EntityBSpecificLabel
    })
    .ToListAsync();

var result = listA.Union(listB)
    .OrderBy(x => x.Name)
    .ThenBy(x => x.EntityASpecificLabel)
    .ThenBy(x => x.EntityBSpecificLabel)
    .ToList();

内容的提问来源于stack exchange,提问作者Steven Manuel

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.17 05:27:03