简体   繁体   English

获取 IQueryable LINQ 中的字典值到实体查询

[英]Get Dictionary value in IQueryable LINQ to Entities query

I have to support multiple languages in production application.我必须在生产应用程序中支持多种语言。

There are lot of Entity Framework queries that gets data from database as deferred IQueryable list like this:有很多实体框架查询从数据库获取数据作为延迟的 IQueryable 列表,如下所示:

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

POCO class looks like this: POCO class 看起来像这样:

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

    public string StatusName { get; set; }

    public string RequestType { get; set; }
}

Data Transfer Object looks like this:数据传输 Object 如下所示:

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

    public string StatusName { get; set; }

    public string RequestType { get; set; }
}

After that I map the EF POCO entity to Data Transfer Object.之后我 map EF POCO 实体到数据传输 Object。 To support multiple languages I want to get resource value by database value in mapping like the following method:为了支持多种语言,我想通过映射中的数据库值获取资源值,如下所示:

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

The problem is that the last command throws the following exception:问题是最后一个命令抛出以下异常:

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.

Unfortunatelly the converting IQueryable to IEnumerable by ToList() is not an option, because I do not want to move list to memory.不幸的是,通过 ToList() 将 IQueryable 转换为 IEnumerable 不是一个选项,因为我不想将列表移动到 memory。

You have to be aware of the difference of IQueryable and IEnumerable.您必须了解 IQueryable 和 IEnumerable 的区别。

IEnumerable IEnumerable

An object that implements IEnumerable represents a sequence.实现IEnumerable的对象表示一个序列。 It holds everything to get the first element of the sequence and once you've got an element you can get the next element as long as there is an element.它包含获取序列中第一个元素的所有内容,一旦你得到一个元素,你就可以获取下一个元素,只要有一个元素。

At lowest level, enumerating over this sequence is done by calling GetEnumerator() and repeatedly calling MoveNext().在最低级别,通过调用 GetEnumerator() 并重复调用 MoveNext() 来完成对这个序列的枚举。 Every time MoveNext returns true, you've got an element.每次 MoveNext 返回 true 时,您就有了一个元素。 This element can be accessed using property Current.可以使用属性 Current 访问此元素。

Enumerating at this lowest level is seldom done.很少在这个最低级别进行枚举。 Usually you enumerate using foreach, or one of the LINQ functions that don't return IEnumerable: ToList(), Count(), Any(), FirstOrDefault(), etc. At the deepest level they all call GetEnumerator and MoveNext / Current.通常您使用 foreach 或不返回 IEnumerable 的 LINQ 函数之一进行枚举:ToList()、Count()、Any()、FirstOrDefault() 等。在最深层次上,它们都调用 GetEnumerator 和 MoveNext/Current。

IQueryable可查询

Although an object that implements IQueryable seems like an IEnumerable, it does not represent the sequence of object itself.虽然实现 IQueryable 的对象看起来像一个 IEnumerable,但它并不代表对象本身的序列。 It represents the potential to create an IEnumerable sequence.它代表了创建 IEnumerable 序列的潜力

For this, the IQueryable holds an Expression and a Provider.为此,IQueryable 包含一个表达式和一个提供程序。 The Expression is a representation of what data must be queried.表达式表示必须查询哪些数据。 The Provider knows who to query for the date (usually a database management system) and what language this DBMS speaks (usually some sort of SQL).提供者知道向谁查询日期(通常是数据库管理系统)以及该 DBMS 使用什么语言(通常是某种 SQL)。

Concatenating IQueryable LINQ statements does not execute the query.连接 IQueryable LINQ 语句不会执行查询。 It only changes the Expression.它只会改变表达式。 To execute the query you need to start enumerating.要执行查询,您需要开始枚举。

Once you start enumerating the IQueryable using GetEnumerator, the Expression is sent to the Provider who will translate the Expression into SQL and execute the query at the DBMS.一旦您开始使用 GetEnumerator 枚举 IQueryable,表达式就会发送到提供程序,提供程序将表达式转换为 SQL 并在 DBMS 上执行查询。 The returned data is represented as an IEnumerable, of which GetEnumerator is called.返回的数据表示为 IEnumerable,其中调用 GetEnumerator。

What does this have to do with my question?这和我的问题有什么关系?

The problem is, that the Provider does not know your function MapRequests .问题是,提供者不知道您的函数MapRequests Therefore it can't translate it into SQL.因此它无法将其转换为 SQL。 In fact even several standard LINQ functions can't be translated into SQL.事实上,即使是几个标准的 LINQ 函数也无法转换为 SQL。 See Supported and Unsupported LINQ methods .请参阅支持和不支持的 LINQ 方法

AsEnumerable可枚举

One way to solve this, is to move the selected data to your local process.解决此问题的一种方法是将所选数据移动到本地进程。 The local process knows function MapRequests and knows how to Execute it.本地进程知道函数 MapRequests 并知道如何执行它。

Moving data to the local process can be done using ToList().可以使用 ToList() 将数据移动到本地进程。 However, this would be a waste of processing power if after this you will only need a few elements, like Take(3), or FirstOrDefault().但是,如果在此之后您只需要几个元素,例如 Take(3) 或 FirstOrDefault(),那么这将浪费处理能力。

AsEnumerable to the rescue! AsEnumable来救援!

Your Provider knows AsEnumerable.您的提供者知道 AsEnumerable。 It will move the data to your local process.它会将数据移动到您的本地进程。 Some dumb providers will do this by fetching all data.一些愚蠢的提供者会通过获取所有数据来做到这一点。 Smarter Providers will fetch the data "per page".更智能的提供商将“每页”获取数据。 One page consists a subset of the queried data, for instance only 50 rows.一页包含查询数据的子集,例如只有 50 行。 It is still a waste if you only use FirstOrDefault(), but at least you won't have fetched millions of Customers.如果你只使用 FirstOrDefault() 仍然是一种浪费,但至少你不会获取数百万个客户。

It would be nice if you changed MapRequests to an extension method.如果您将 MapRequests 更改为扩展方法,那就太好了。 See Extension Methods Demystified请参阅揭开扩展方法的神秘面纱

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

Usage:用法:

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

Note: the query is not executed until your call GetEnumerator() (or foreach, ToList(), Count(), etc).注意:在您调用 GetEnumerator()(或 foreach、ToList()、Count() 等)之前,不会执行查询。 You can even add other IEnumerable functions:您甚至可以添加其他 IEnumerable 函数:

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

Be aware though, that the statements are not executed by the Database Management System, but by your local process.但请注意,语句不是由数据库管理系统执行,而是由您的本地进程执行。

Can the DBMS map my Requests? DBMS 可以映射我的请求吗?

Yet it probably can.然而它可能可以。 You'll have to transport the resources to the database and use simple database functions to convert Request to RequestDTO.您必须将资源传输到数据库并使用简单的数据库函数将 Request 转换为 RequestDTO。 If there are many resources in comparison to the number of Requests that you'll have to convert, then it is probably not wise to do.如果与您必须转换的请求数量相比有很多资源,那么这样做可能是不明智的。 But if for instance you'll have to convert thousands of Request with 100 resources, and after conversion you'll do a Where , or a GroupJoin with another table, it is probably wise to let the DBMS do the conversion.但是,例如,如果您必须使用 100 个资源转换数千个请求,并且转换后您将执行WhereGroupJoin与另一个表,那么让 DBMS 进行转换可能是明智的。

It seems that every Resource has a Key and a Value.似乎每个资源都有一个键和一个值。

  • StatusName should have the value of the Resource with Key equal to request.StatusName StatusName 应该具有 Key 等于 request.StatusName 的资源的值
  • RequestType should have the value of the Resource with Key equal to request.RequestType. RequestType 应具有 Key 等于 request.RequestType 的资源的值。

So let's rewrite MapRequests into an extension method of IQeryable:所以让我们把 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(),
     }

Usage:用法:

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

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

Now the complete statement will be executed by the Database management system.现在完整的语句将由数据库管理系统执行。 Most DBMSs are much more optimized to select specific items from a sequence than your process.大多数 DBMS 都比您的流程更优化,可以从序列中选择特定项目。 Besides this looks much neater.除此之外,这看起来更整洁。

I did some heavy researching and came across a similar but different solution.我做了一些深入的研究,发现了一个类似但不同的解决方案。 The problem I was trying to solve is, how can you look up a value from a dictionary with a known primitive key, so that you can use the value within iqueryable and not have to run.AsEnumerable() or.ToList() and pass it back to the app server for slow processing.我试图解决的问题是,如何从具有已知原始键的字典中查找值,以便可以在 iqueryable 中使用该值而不必运行.AsEnumerable() 或.ToList() 并通过它返回到应用服务器进行缓慢处理。

All credit to the author here: https://www.mlink.in/qa/?qa=1077916/此处归功于作者: https://www.mlink.in/qa/?qa=1077916/

First we make the extension class & methods as defined:首先,我们按照定义进行扩展 class 和方法:

/// <summary>
/// Holds extension methods that simplify querying.
/// </summary>
public static class QueryExtensions
{
    /// <summary>
    ///   Return the element that the specified property's value is contained in the specified values.
    /// </summary>
    /// <typeparam name="TElement"> The type of the element. </typeparam>
    /// <typeparam name="TValue"> The type of the values. </typeparam>
    /// <param name="source"> The source. </param>
    /// <param name="propertySelector"> The property to be tested. </param>
    /// <param name="values"> The accepted values of the property. </param>
    /// <returns> The accepted elements. </returns>
    public static IQueryable<TElement> WhereIn<TElement, TValue>(
        this IQueryable<TElement> source, 
        Expression<Func<TElement, TValue>> propertySelector, 
        params TValue[] values)
    {
        return source.Where(GetWhereInExpression(propertySelector, values));
    }

    /// <summary>
    ///   Return the element that the specified property's value is contained in the specified values.
    /// </summary>
    /// <typeparam name="TElement"> The type of the element. </typeparam>
    /// <typeparam name="TValue"> The type of the values. </typeparam>
    /// <param name="source"> The source. </param>
    /// <param name="propertySelector"> The property to be tested. </param>
    /// <param name="values"> The accepted values of the property. </param>
    /// <returns> The accepted elements. </returns>
    public static IQueryable<TElement> WhereIn<TElement, TValue>(
        this IQueryable<TElement> source, 
        Expression<Func<TElement, TValue>> propertySelector, 
        IEnumerable<TValue> values)
    {
        return source.Where(GetWhereInExpression(propertySelector, values.ToList()));
    }

    /// <summary>
    ///   Gets the expression for a "where in" condition.
    /// </summary>
    /// <typeparam name="TElement"> The type of the element. </typeparam>
    /// <typeparam name="TValue"> The type of the value. </typeparam>
    /// <param name="propertySelector"> The property selector. </param>
    /// <param name="values"> The values. </param>
    /// <returns> The expression. </returns>
    private static Expression<Func<TElement, bool>> GetWhereInExpression<TElement, TValue>(
        Expression<Func<TElement, TValue>> propertySelector, ICollection<TValue> values)
    {
        var p = propertySelector.Parameters.Single();
        if (!values.Any())
            return e => false;

        var equals =
            values.Select(
                value =>
                (Expression)Expression.Equal(propertySelector.Body, Expression.Constant(value, typeof(TValue))));
        var body = equals.Aggregate(Expression.OrElse);

        return Expression.Lambda<Func<TElement, bool>>(body, p);
    }
}

then, we can use it to query where value in dictionary as follows:然后,我们可以使用它来查询字典中的 where 值,如下所示:



            var countryCitiesLookup = Dictionary<string, List<string>>();
            ///
            // fill in values here
            ///
            var country = "USA";
            var x = _context.Countries.Where(x => true)// original query without 
                .WhereIn(x => x.CountryID, CountryCitiesLookup.Keys.Contains(country));`enter code here`

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM