Недопустимое имя столбца при использовании фильтра ядра EF включает

#sql-server #asp.net-core-3.1 #entity-framework-core-5

Вопрос:

Я столкнулся с этой ошибкой при изменении первого проекта БД (с использованием fluent migrator) и использовании контекста EF для создания моделей. Я воспроизвел его, сделав упрощение кода в первую очередь. Это означает, что я не могу принять ответы, в которых предлагается изменить аннотации или конфигурацию fluent, потому что они будут удалены и воссозданы при следующей миграции и сборке.

Упрощенная идея состоит в том, что устройство имеет:

  • множество атрибутов
  • множество историй, представляющих изменения устройства с течением времени
    • каждая запись истории имеет необязательное местоположение

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>();
}
 

Запуск следующего запроса для выбора всех устройств работает нормально. Я использую отфильтрованное включение, чтобы выбрать только самую последнюю историю для этого «представления».:

 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();
 

это работает нормально, однако, когда я пытаюсь выбрать только одно устройство по идентификатору, используя то же самое, включает:

 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, Booleanamp; 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, Taskamp; 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, Taskamp; task, Booleanamp; 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.

Я даже не знаю, с чего начать поиски, идеи приветствуются!

Редактировать

Комментарии:

1. Разница в том, что в первом запросе включения игнорируются, потому ToModel() что это проекция непосредственно на IQueryable то, что игнорирует включения. Второй запрос выполняет IQueryable , а затем выполняет проекцию в памяти. Тем не менее, это может быть ошибка. В EF-core 5 я могу выполнить более простой запрос, содержащий Include с OrderByDescending и Take(1) без каких-либо проблем. Что делать, если вы попытаетесь выполнить запросы только с одним включением на запрос? Может быть, это ошибка отображения.

2. Первое, что вам нужно проверить, это действительно ли первый запрос работает без .Select(d => d.ToModel()) ошибок .

3. О, то, что я сказал о проигнорированном Include s, неправда. Это могло бы быть , если ToModel бы был метод расширения, подобный методу AutoMapper ProjectTo , но это метод внутри выражения ( .Select(d => d.ToModel()) ), поэтому EF выполняет весь запрос, а затем выполняет проекцию на стороне клиента, потому что он находится в конечном выборе. На самом деле это отвечает предложению Ивана: это не работает. Было бы интересно посмотреть, какие из них работают, а какие нет.

Ответ №1:

Обновление: Ошибка исправлена в EF Core 6.0, поэтому следующее относится только к EF Core 5.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]
 

и для второго (или просто вставив .Where(d => d.DeviceId == deviceId).Take(1) перед Select этим в первый):

 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]
 

Обратите внимание на использование [t] в первом SELECT [t].[HistoryId]... внутри OUTER APPLY , который в первом запросе является псевдонимом для внутреннего Histories подзапроса в FROM предложении, в то время как во втором это псевдоним для внешнего Devices подзапроса, у которого, конечно, нет столбцов, упомянутых в сообщении об ошибке. Видимо, во втором случае [t0] следовало бы использовать.

Поскольку это ошибка, вам придется подождать, пока она будет исправлена. До тех пор обходной путь, который я мог бы предложить, заключается в явном выполнении оператора ограничения строк ( First ) вне контекста основного запроса EF, например

 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();
 

Комментарии:

1. Спасибо за подробный ответ, но не будет ли этот обходной путь сначала загружать все в память? Я не могу этого сделать, так как таблица слишком велика, поэтому мне, возможно, придется попробовать несколько версий и посмотреть, была ли ошибка во всех версиях v5 (я только что обновил ef-core 3, чтобы использовать отфильтрованные включения!). Сейчас уже поздно, так что завтра я займусь отслеживанием проблем. Спасибо!

2. Да, он загрузится, но не все. Обратите внимание, что Where фильтр по-прежнему применяется на стороне сервера (перед переключением на оценку на стороне клиента AsEnumerable() ), поэтому будут загружены только элементы, соответствующие фильтру. Который для фильтра значений PK, как в вопросе, будет равен 0 или 1 элементу — точно так же, как если First бы выполнялся напрямую. Это было бы проблемой, если бы вы хотели применить подкачку к первому запросу (до ToList() ), так Take как это должно быть после AsEnumerable , чтобы обойти текущую ошибку ядра EF.

3. На момент его появления я не могу точно сказать, но я продублировал его в последнем официальном EF Core 5.0.11, так что есть вероятность, что он появится даже во входящем EFC 6.0

4. К счастью, сегодня я столкнулся с аналогичной проблемой, но я не могу выделить, что именно ее вызывает: она правильно работает в реальном API, она правильно работает в модульном тесте для API, но терпит неудачу в изолированном модульном тесте только для запроса. Странно то, что если я просматриваю представление «Отладка» для запроса, сгенерированный им SQL верен, но в тот момент, когда вы выполняете запрос (например, First ()), он отображается как нечто другое

5. @John First , FirstOrDefault , Single и аналогичные операторы конечного запроса — они изменяют сгенерированный запрос перед его выполнением. В основном добавляется Take(1) в конце. Take является оператором, который в данном случае вызывает недопустимую генерацию SQL.