【问题标题】:Select only specific fields with Linq (EF core)仅使用 Linq(EF 核心)选择特定字段
【发布时间】:2019-02-06 08:41:21
【问题描述】:

我有一个DbContext,我想在其中运行查询以仅返回特定列,以避免获取所有数据。
问题是我想用一组字符串指定列名,我想获得一个原始类型的IQueryable,即不构造匿名类型。

这是一个例子:

// Install-Package Microsoft.AspNetCore.All
// Install-Package Microsoft.EntityFrameworkCore

using Microsoft.EntityFrameworkCore;
using System;
using System.Linq;

public class Person {
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

public class TestContext : DbContext {
    public virtual DbSet<Person> Persons { get; set; }
    public TestContext(DbContextOptions<TestContext> options) : base(options) {
    }
}

class Program {
    static void Main(string[] args) {

        var builder = new DbContextOptionsBuilder<TestContext>();
        builder.UseInMemoryDatabase(Guid.NewGuid().ToString());
        var context = new TestContext(builder.Options);

        context.Persons.Add(new Person { FirstName = "John", LastName = "Doe" });
        context.SaveChanges();

        // How can I express this selecting columns with a set of strings? 
        IQueryable<Person> query = from p in context.Persons select new Person { FirstName = p.FirstName };
    }
}

我想要这样的方法:

static IQueryable<Person> GetPersons(TestContext context, params string[] fieldsToSelect) {
    // ...
}

有什么办法可以做到吗?

【问题讨论】:

  • 使用反射。它会变得丑陋和混乱,并且会降低性能,但除了将每个属性映射到字符串之外,这可能是唯一的方法。
  • 即使使用反射,我如何创建选择语句?我要避免的是从数据库中获取所有数据。
  • 使用原始 sql 然后映射到您自己的 DTO。我们过去就是这样做的。 docs.microsoft.com/en-us/ef/core/querying/raw-sql
  • 我很确定你可以做到这一点,谷歌搜索“Dynamic Linq”这可能会让你走上正确的轨道,也看看这个答案stackoverflow.com/questions/16516971/linq-dynamic-select

标签: c# .net-core entity-framework-core


【解决方案1】:

由于您将T 类型的成员投影(选择)到相同类型T,因此可以使用Expression 类方法相对容易地创建所需的Expression&lt;Func&lt;T, T&gt;&gt;,如下所示:

public static partial class QueryableExtensions
{
    public static IQueryable<T> SelectMembers<T>(this IQueryable<T> source, params string[] memberNames)
    {
        var parameter = Expression.Parameter(typeof(T), "e");
        var bindings = memberNames
            .Select(name => Expression.PropertyOrField(parameter, name))
            .Select(member => Expression.Bind(member.Member, member));
        var body = Expression.MemberInit(Expression.New(typeof(T)), bindings);
        var selector = Expression.Lambda<Func<T, T>>(body, parameter);
        return source.Select(selector);
    }
}

Expression.MemberInitnew T { Member1 = x.Member1, Member2 = x.Member2, ... } C# 构造的等效表达式。

示例用法为:

return context.Set<Person>().SelectMembers(fieldsToSelect);

【讨论】:

  • 谢谢,这正是我所需要的。我试图根据这个答案 (stackoverflow.com/questions/12701737/…) 做类似的事情,但我在Lambda 位上迷路了:)
  • 如何在匿名投影中实现这一点?
  • @Wendell 如开头所述,这种方法仅适用于投影到相同类型中。现在,您的评论看起来像 T 不能是匿名类型,如果这就是您的意思。我不得不承认代码不适用于匿名类型。
【解决方案2】:

这可以通过使用Dynamic Linq来实现。

对于 .Net Core - System.Linq.Dynamic.Core

使用 Dynamic Linq,您可以将 SELECT 和 WHERE 作为字符串传递。

使用您的示例,您可以执行以下操作:

IQueryable<Person> query = context.Persons
                        .Select("new Person { FirstName = p.FirstName }");

【讨论】:

  • 请举例说明如何使用 Dynamic Linq 实现这一点
  • OP 已将问题标记为 .NET Core,这个包现在已经有 3 年的历史了,可能只是针对完整的 .NET。
  • .Net Core 也有 Dynamic Linq,我已经更新了我的答案以包含指向 .net Core Nuget 库的链接
  • 谢谢。我试过了,但找不到 select 子句的正确语法。
  • 与使用表达式树的自构建解决方案相比,性能如何会很有趣
【解决方案3】:

基于Ivananswer,我做了一个粗略的缓存功能版本,通过使用反射来消除对我们的影响。它允许在重复请求(例如 DbAccess API 的典型情况)上将此时间从 毫秒秒降低到 秒。

public static class QueryableExtensions
{
    public static IQueryable<T> SelectMembers<T>(this IQueryable<T> source, IEnumerable<string> memberNames)
    {
        var result = QueryableGenericExtensions<T>.SelectMembers(source, memberNames);
        return result;
    }
}


public static class QueryableGenericExtensions<T>
{
    private static readonly ConcurrentDictionary<string, ParameterExpression> _parameters = new();
    private static readonly ConcurrentDictionary<string, MemberAssignment> _bindings = new();
    private static readonly ConcurrentDictionary<string, Expression<Func<T, T>>> _selectors = new();

    public static IQueryable<T> SelectMembers(IQueryable<T> source, IEnumerable<string> memberNames)
    {
        var parameterName = typeof(T).FullName;

        var requestName = $"{parameterName}:{string.Join(",", memberNames.OrderBy(x => x))}";
        if (!_selectors.TryGetValue(requestName, out var selector))
        {
            if (!_parameters.TryGetValue(parameterName, out var parameter))
            {
                parameter = Expression.Parameter(typeof(T), typeof(T).Name.ToLowerInvariant());

                _ = _parameters.TryAdd(parameterName, parameter);
            }

            var bindings = memberNames
                .Select(name =>
                {
                    var memberName = $"{parameterName}:{name}";
                    if (!_bindings.TryGetValue(memberName, out var binding))
                    {
                        var member = Expression.PropertyOrField(parameter, name);
                        binding = Expression.Bind(member.Member, member);

                        _ = _bindings.TryAdd(memberName, binding);
                    }
                    return binding;
                });

            var body = Expression.MemberInit(Expression.New(typeof(T)), bindings);
            selector = Expression.Lambda<Func<T, T>>(body, parameter);

            _selectors.TryAdd(requestName, selector);
        }

        return source.Select(selector);
    }
}

使用相同参数顺序运行后的结果示例(请注意这是 NANOseconds):

SelectMembers time ... 3092214 ns
SelectMembers time ... 145724 ns
SelectMembers time ... 38613 ns
SelectMembers time ... 1969 ns

我不知道为什么时间会逐渐减少,而不是从“没有缓存”到“有缓存”,可能是因为我的环境循环询问具有相同请求的 4 个服务器以及一些带有异步的深层魔法.重复请求会产生与上一个相似的一致结果 +/- 1-2 微秒。

【讨论】:

    【解决方案4】:

    试试这个代码:

    string fieldsToSelect = "new Person { FirstName = p.FirstName }"; //Pass this as parameter.
    
    public static IQueryable<Person> GetPersons(TestContext context, string fieldsToSelect) 
    {
        IQueryable<Person> query = context.Persons.Select(fieldsToSelect);
    }
    

    【讨论】:

      【解决方案5】:

      我可以很容易地使用包https://github.com/StefH/System.Linq.Dynamic.Core 做到这一点。

      这是一个示例代码。

      使用命名空间,using System.Linq.Dynamic.Core;

      //var selectQuery = "new(Name, Id, PresentDetails.RollNo)";
      
      var selectQuery = "new(Name, Id, PresentDetails.GuardianDetails.Name as GuardianName)";
      
      var students = dbContext.Students
          .Include(s => s.PresentDetails)
          .Include(s => s.PresentDetails.GuardianDetails)
          .Where(s => s.StudentStatus == "Admitted")
          .Select(selectQuery);
      

      【讨论】:

        【解决方案6】:
        var students = dbContext.Students
            .Include(s => s.PresentDetails)
            .Where(s => s.StudentStatus == "Admitted")
            .Select(p => new Person() 
                               { 
                                   Id = p.Id, 
                                   Name = p.Name
                               });
        

        为什么不以常规方式最小化选定的列?这样更干净。

        【讨论】:

        • 在问题中,我说我想用一组字符串来指定列名:)
        猜你喜欢
        • 2021-04-28
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-11-29
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多