[英]Select only specific fields with Linq (EF core)
我有一个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) {
// ...
}
有没有办法做到这一点?
由于您将类型T
的成员投影(选择)到相同类型T
,因此可以使用像这样的Expression
类方法相对容易地创建所需的Expression<Func<T, T>>
:
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.MemberInit是等效于new T { Member1 = x.Member1, Member2 = x.Member2, ... }
C# 构造的表达式。
示例用法是:
return context.Set<Person>().SelectMembers(fieldsToSelect);
这可以通过使用Dynamic Linq来实现。
对于 .Net Core - System.Linq.Dynamic.Core
使用 Dynamic Linq,您可以将 SELECT 和 WHERE 作为字符串传递。
使用您的示例,您可以执行以下操作:
IQueryable<Person> query = context.Persons
.Select("new Person { FirstName = p.FirstName }");
根据Ivan 的回答,我制作了粗略版本的缓存功能,以消除使用反射对我们造成的损失。 它允许将重复请求(例如,典型的 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 微秒类似的一致结果。
试试这个代码:
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);
}
我能够很容易地使用包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);
var students = dbContext.Students
.Include(s => s.PresentDetails)
.Where(s => s.StudentStatus == "Admitted")
.Select(p => new Person()
{
Id = p.Id,
Name = p.Name
});
为什么不以常规方式最小化选定的列? 这样更干净。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.