Получить значение словаря в запросе IQueryable LINQ to Entities - PullRequest
0 голосов
/ 18 марта 2020

Мне нужно поддерживать несколько языков в производственном приложении.

Существует множество запросов Entity Framework, которые получают данные из базы данных в виде отложенного списка IQueryable, например:

public IQueryable<Request> GetDeferredRequests()
{
    return _dbContext.Set<Request>();
}   

Класс POCO выглядит например:

public partial class Request
{
    public int RequestID { get; set; }

    public string StatusName { get; set; }

    public string RequestType { get; set; }
}

Объект передачи данных выглядит следующим образом:

public class RequestDTO
{
    public int RequestID { get; set; }

    public string StatusName { get; set; }

    public string RequestType { get; set; }
}

После этого я сопоставляю объект EF POCO объекту передачи данных. Для поддержки нескольких языков я хочу получить значение ресурса по значению базы данных при отображении следующим образом:

public IQueryable<RequestDTO> MapRequests(IQueryable<Request> requests)
{
      Dictionary<string, string> resoures = new Dictionary<string, string>();

      System.Resources.ResourceSet resources = DatabaseResources.ResourceManager.GetResourceSet(new System.Globalization.CultureInfo("en"), true, true);

      foreach (DictionaryEntry resource in resources)
      {
          resoures.Add(resource.Key.ToString(), resource.Value.ToString());
      }

      return requests.Select(c => new RequestDTO()
      {
          RequestID = c.RequestID,
          StatusName =  resoures.Single(r => r.Key == c.StatusName).Value,
          RequestType = resoures.Single(r => r.Key == c.RequestType).Value
      });
}

Проблема в том, что последняя команда выдает следующее исключение:

LINQ to Entities does not recognize the method 'System.String get_Item(System.String)' method, and this method cannot be translated into a store expression.

К сожалению, преобразование IQueryable в IEnumerable с помощью ToList () не вариант, потому что я не хочу перемещать список в память.

1 Ответ

0 голосов
/ 19 марта 2020

Вы должны знать о различии IQueryable и IEnumerable.

IEnumerable

Объект, который реализует IEnumerable , представляет последовательность. Он содержит все, чтобы получить первый элемент последовательности, и как только вы получите элемент, вы можете получить следующий элемент, пока есть элемент.

На самом низком уровне перечисление по этой последовательности выполняется с помощью вызов GetEnumerator () и повторный вызов MoveNext (). Каждый раз, когда MoveNext возвращает true, у вас есть элемент. К этому элементу можно получить доступ, используя свойство Current.

Перечисление на этом самом низком уровне выполняется редко. Обычно вы перечисляете, используя foreach или одну из функций LINQ, которые не возвращают IEnumerable: ToList (), Count (), Any (), FirstOrDefault () и т. Д. c. На самом глубоком уровне все они вызывают GetEnumerator и MoveNext / Current.

IQueryable

Хотя объект, реализующий IQueryable, выглядит как IEnumerable, он не представляет последовательность самого объекта. Он представляет потенциал для создания последовательности IEnumerable.

Для этого IQueryable содержит выражение и поставщика. Выражение является представлением того, какие данные должны быть запрошены. Поставщик знает, к кому обращаться за датой (обычно это система управления базами данных) и на каком языке говорит эта СУБД (обычно своего рода SQL).

Конкатенация операторов IQueryable LINQ не выполняет запрос. Это только меняет выражение. Для выполнения запроса вам нужно начать перечисление.

Как только вы начнете перечислять IQueryable с помощью GetEnumerator, выражение отправляется поставщику, который переведет выражение в SQL и выполнит запрос в СУБД. Возвращенные данные представлены в виде IEnumerable, для которого вызывается GetEnumerator.

Какое это имеет отношение к моему вопросу?

Проблема в том, что провайдер не знает вашу функцию MapRequests. Поэтому он не может перевести это на SQL. На самом деле даже несколько стандартных функций LINQ не могут быть переведены в SQL. См. Поддерживаемые и неподдерживаемые методы LINQ .

AsEnumerable

Одним из способов решения этой проблемы является перемещение выбранных данных в локальный процесс. Локальный процесс знает функцию MapRequests и знает, как ее выполнить.

Перемещение данных в локальный процесс можно выполнить с помощью ToList (). Однако это будет пустой тратой вычислительной мощности, если после этого вам понадобятся только несколько элементов, таких как Take (3) или FirstOrDefault ().

AsEnumerable на помощь!

Ваш провайдер знает AsEnumerable. Это переместит данные в ваш локальный процесс. Некоторые тупые провайдеры будут делать это, выбирая все данные. Более умные провайдеры будут извлекать данные «на страницу». Одна страница содержит подмножество запрашиваемых данных, например, только 50 строк. Это все еще пустая трата, если вы используете только FirstOrDefault (), но, по крайней мере, вы не получите миллионы клиентов.

Было бы хорошо, если вы изменили MapRequests на метод расширения. См. Демистифицированные методы расширения

public static class MyIEnumerableExtensions
{
    public static IEnumerable<RequestDTO> ToRequestDTO( this IEnumerable<Request> requests)
    {
        // your code
        ...
        return requests.Select(request => new RequestDTO
        {
           RequestId = request.RequestId,
           ...
        });
    }

Использование:

IEnumerable<RequestDto> requestDTOs = GetDeferredRequests()

    // only if you don't want all requests:
    .Where(request => ...)

    // move to local process in a smart way:
    AsEnumerable()

    // Convert to RequestDTO:
    .ToRequestDTO();

Примечание: запрос не выполняется до тех пор, пока вы не вызовете GetEnumerator () (или foreach, ToList () , Count (), et c). Вы даже можете добавить другие функции IEnumerable:

    .Where(requestDTO => requestDTO.StatusName == ...);

Имейте в виду, что операторы выполняются не системой управления базами данных, а вашим локальным процессом.

Может ли СУБД отобразить мою Просит?

И все же возможно. Вам нужно будет перенести ресурсы в базу данных и использовать простые функции базы данных для преобразования Request в RequestDTO. Если есть много ресурсов по сравнению с количеством запросов, которые вам нужно преобразовать, то, вероятно, это не разумно. Но если, например, вам придется конвертировать тысячи Request с 100 ресурсами, а после преобразования вы будете делать Where или GroupJoin с другой таблицей, то, вероятно, целесообразно позволить СУБД выполнить преобразование.

Кажется, что у каждого ресурса есть ключ и значение.

  • StatusName должен иметь значение ресурса с ключом, равным request.StatusName
  • RequestType иметь значение Resource с ключом равным request.RequestType.

Итак, давайте перепишем MapRequests в метод расширения IQeryable:

public IQueryable<RequestDTO> ToRequestDto( this IQueryable<Request> requests,
      IEnumerable<KeyValuePair<string, string>> resources)
{
     // TODO: exception if requests == null, resources == null

     return requests.Select(request => new RequestDTO
     {
         RequestId = request.RequestId,

         // from resources, keep only the resource with key equals to StatusName
         // and select the FirstOrDefault value:
         StatusName = resources
                      .Where(resource => resource.Key == request.StatusName)
                      .Select(resource => resource.Value)
                      .FirstOrDefault(),
         // from resources, keep only the resource with key equals to RequestType
         // and select the FirstOrDefault value:
         RequestType = resources
                      .Where(resource => resource.Key == request.RequestType)
                      .Select(resource => resource.Value)
                      .FirstOrDefault(),
     }

Использование:

IEnumerable<KeyValuePair<string, string> resources = ...
var requestDTOs = GetDeferredRequests()
    .Where(request => ...)
    .ToRequestDTO(resources)

    // do other database processing
    .GroupJoin(myOtherTable, ...)
    .Where(...)
    .Take(3);

Теперь полный оператор будет выполнен системой управления базами данных. Большинство СУБД гораздо более оптимизированы для выбора определенных c элементов из последовательности, чем ваш процесс. Кроме того, это выглядит намного аккуратнее.

...