使用EF核心过滤包括时无效的列名

在修改DB First项目(使用Fluent Migrator)并搭建EF上下文以生成模型时,我遇到了这个错误。我通过进行代码优先简化来重现它。这意味着我不能接受建议修改批注或流畅配置的答案,因为这将在下一次迁移和搭建时删除并重新创建。

简化的想法是设备具有:

  • 多个属性
  • 表示设备随时间的变化的多个历史记录
    • 每个历史记录条目都有一个可选位置

IOW您可以将设备移动到不同的位置(或不在任何位置),并随着时间的推移进行跟踪。

我提出的代码优先模型来模拟这一点,如下所示:

public class ApiContext : DbContext
{
    public ApiContext(DbContextOptions<ApiContext> options) : base(options) { }

    public DbSet<Device> Devices { get; set; }
    public DbSet<History> Histories { get; set; }
    public DbSet<Location> Locations { get; set; }
}

public class Device
{
    public int DeviceId { get; set; }
    public string DeviceName { get; set; }

    public List<History> Histories { get; } = new List<History>();
    public List<Attribute> Attributes { get; } = new List<Attribute>();
}

public class History
{
    public int HistoryId { get; set; }
    public DateTime DateFrom { get; set; }
    public string State { get; set; }

    public int DeviceId { get; set; }
    public Device Device { get; set; }

    public int? LocationId { get; set; }
    public Location Location { get; set; }
}

public class Attribute
{
    public int AttributeId { get; set; }
    public string Name { get; set; }

    public int DeviceId { get; set; }
    public Device Device { get; set; }
}

public class Location
{
    public int LocationId { get; set; }
    public string LocationName { get; set; }

    public List<History> Histories { get; } = new List<History>();
}
运行以下查询以选择所有设备运行正常。我使用filtered include仅选择此视图的最新历史记录:

var devices = _apiContext.Devices.AsNoTracking()
    .Include(d => d.Histories.OrderByDescending(h => h.DateFrom).Take(1))
    .ThenInclude(h => h.Location)
    .Include(d => d.Attributes)
    .Select(d => d.ToModel()).ToList();

这很好用,但是当我尝试使用相同的ID只选择一个设备时,包括:

var device = _apiContext.Devices.AsNoTracking()
    .Include(d => d.Histories.OrderByDescending(h => h.DateFrom).Take(1))
    .ThenInclude(h => h.Location)
    .Include(d => d.Attributes)
    .First(d => d.DeviceId == deviceId)
    .ToModel();

我收到以下错误:

Unhandled exception. Microsoft.Data.SqlClient.SqlException (0x80131904): Invalid column name 'LocationId'.
Invalid column name 'HistoryId'.
Invalid column name 'DateFrom'.
Invalid column name 'LocationId'.
Invalid column name 'State'.
   at Microsoft.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
   at Microsoft.Data.SqlClient.SqlInternalConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
   at Microsoft.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj, Boolean callerHasConnectionLock, Boolean asyncClose)
   at Microsoft.Data.SqlClient.TdsParser.TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj, Boolean& dataReady)
   at Microsoft.Data.SqlClient.SqlDataReader.TryConsumeMetaData()
   at Microsoft.Data.SqlClient.SqlDataReader.get_MetaData()
   at Microsoft.Data.SqlClient.SqlCommand.FinishExecuteReader(SqlDataReader ds, RunBehavior runBehavior, String resetOptionsString, Boolean isInternal, Boolean forDescribeParameterEncryption, Boolean shouldCacheForAlwaysEncrypted)
   at Microsoft.Data.SqlClient.SqlCommand.RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, Boolean isAsync, Int32 timeout, Task& task, Boolean asyncWrite, Boolean inRetry, SqlDataReader ds, Boolean describeParameterEncryptionRequest)
   at Microsoft.Data.SqlClient.SqlCommand.RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, TaskCompletionSource`1 completion, Int32 timeout, Task& task, Boolean& usedCache, Boolean asyncWrite, Boolean inRetry, String method)
   at Microsoft.Data.SqlClient.SqlCommand.RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, String method)
   at Microsoft.Data.SqlClient.SqlCommand.ExecuteReader(CommandBehavior behavior)
   at Microsoft.Data.SqlClient.SqlCommand.ExecuteDbDataReader(CommandBehavior behavior)
   at System.Data.Common.DbCommand.ExecuteReader()
   at Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReader(RelationalCommandParameterObject parameterObject)
   at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.Enumerator.InitializeReader(DbContext _, Boolean result)
   at Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal.SqlServerExecutionStrategy.Execute[TState,TResult](TState state, Func`3 operation, Func`3 verifySucceeded)
   at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.Enumerator.MoveNext()
   at System.Linq.Enumerable.Single[TSource](IEnumerable`1 source)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.Execute[TResult](Expression query)
   at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.Execute[TResult](Expression expression)
   at System.Linq.Queryable.First[TSource](IQueryable`1 source, Expression`1 predicate)
   at efcore_test.App.PrintSingleDevice(Int32 deviceId) in C:UsersIainprojectsefcore-5-bugefcore-testApp.cs:line 44
   at efcore_test.Program.<>c__DisplayClass1_0.<Main>b__4(App app) in C:UsersIainprojectsefcore-5-bugefcore-testProgram.cs:line 28
   at efcore_test.Program.RunInScope(IServiceProvider serviceProvider, Action`1 method) in C:UsersIainprojectsefcore-5-bugefcore-testProgram.cs:line 35
   at efcore_test.Program.Main(String[] args) in C:UsersIainprojectsefcore-5-bugefcore-testProgram.cs:line 28
ClientConnectionId:1418edb2-0889-4f4d-9554-85344c9a35a9
Error Number:207,State:1,Class:16

我搞不懂为什么这对多行有效,但对单行无效。

为了完整起见,ToModel()只是返回POCO的扩展方法。

我甚至不知道从哪里开始寻找,欢迎您的想法!

编辑

  • 错误报告:https://github.com/dotnet/efcore/issues/26585
  • 转载:https://github.com/thinkOfaNumber/efcore-5-test

解决方案

更新:该错误已在EF Core6.0中修复,因此下一个错误仅适用于EF Core5.0。

看起来您遇到了EF Core 5.0查询翻译错误,所以我建议您寻找/报告给EF Core GitHub问题跟踪器。

据我所知,这是由于Take运算符(这基本上就是First方法在第二种情况下使用的运算符)将根查询作为子查询下推造成的。这以某种方式扰乱了生成的子查询别名,并导致无效的SQL。

通过比较第一个查询生成的SQL可以看出

SELECT [d].[DeviceId], [d].[DeviceName], [t0].[HistoryId], [t0].[DateFrom], [t0].[DeviceId], [t0].[LocationId], [t0].[State], [t0].[LocationId0], [t0].[LocationName], [a].[AttributeId], [a].[DeviceId], [a].[Name]
FROM [Devices] AS [d]
OUTER APPLY (
    SELECT [t].[HistoryId], [t].[DateFrom], [t].[DeviceId], [t].[LocationId], [t].[State], [l].[LocationId] AS [LocationId0], [l].[LocationName]
    FROM (
        SELECT TOP(1) [h].[HistoryId], [h].[DateFrom], [h].[DeviceId], [h].[LocationId], [h].[State]
        FROM [Histories] AS [h]
        WHERE [d].[DeviceId] = [h].[DeviceId]
        ORDER BY [h].[DateFrom] DESC
    ) AS [t]
    LEFT JOIN [Locations] AS [l] ON [t].[LocationId] = [l].[LocationId]
) AS [t0]
LEFT JOIN [Attribute] AS [a] ON [d].[DeviceId] = [a].[DeviceId]
ORDER BY [d].[DeviceId], [t0].[DateFrom] DESC, [t0].[HistoryId], [t0].[LocationId0], [a].[AttributeId]

和第二个(或仅在第一个Select之前插入.Where(d => d.DeviceId == deviceId).Take(1)):

SELECT [t].[DeviceId], [t].[DeviceName], [t1].[HistoryId], [t1].[DateFrom], [t1].[DeviceId], [t1].[LocationId], [t1].[State], [t1].[LocationId0], [t1].[LocationName], [a].[AttributeId], [a].[DeviceId], [a].[Name]
FROM (
    SELECT TOP(1) [d].[DeviceId], [d].[DeviceName]
    FROM [Devices] AS [d]
    WHERE [d].[DeviceId] = @__deviceId_0
) AS [t]
OUTER APPLY (
    SELECT [t].[HistoryId], [t].[DateFrom], [t].[DeviceId], [t].[LocationId], [t].[State], [l].[LocationId] AS [LocationId0], [l].[LocationName]
    FROM (
        SELECT TOP(1) [h].[HistoryId], [h].[DateFrom], [h].[DeviceId], [h].[LocationId], [h].[State]
        FROM [Histories] AS [h]
        WHERE [t].[DeviceId] = [h].[DeviceId]
        ORDER BY [h].[DateFrom] DESC
    ) AS [t0]
    LEFT JOIN [Locations] AS [l] ON [t].[LocationId] = [l].[LocationId]
) AS [t1]
LEFT JOIN [Attribute] AS [a] ON [t].[DeviceId] = [a].[DeviceId]
ORDER BY [t].[DeviceId], [t1].[DateFrom] DESC, [t1].[HistoryId], [t1].[LocationId0], [a].[AttributeId]
注意OUTER APPLY中第一个SELECT [t].[HistoryId]...[t]的用法,在第一个查询中,它是FROM子句中内部Histories子查询的别名,而在第二个查询中,它是外部Devices子查询的别名,这两个子查询中没有错误消息中提到的列。显然,在第二种情况下,应该使用[t0]

由于它是一个错误,您必须等待修复它。在此之前,我建议的解决方法是在EF核心查询上下文之外显式执行行限制运算符(First),例如

var device = _apiContext.Devices.AsNoTracking()
    .Include(d => d.Histories.OrderByDescending(h => h.DateFrom).Take(1))
    .ThenInclude(h => h.Location)
    .Include(d => d.Attributes)
    .Where(d => d.DeviceId == deviceId) // instead of .First(d => d.DeviceId == deviceId)
    .AsEnumerable() // switch to client evaluation (LINQ to Objects context)
    .First() // and execute `First` here
    .ToModel();

相关文章