【问题标题】:Dynamically build select list from linq to entities query动态构建从 linq 到实体查询的选择列表
【发布时间】:2016-07-21 13:39:58
【问题描述】:

我正在寻找一种从 iQueryable 对象动态创建选择列表的方法。

具体的例子,我想做如下的事情:

public void CreateSelectList(IQueryable(of EntityModel.Core.User entities), string[] columns)
{
    foreach(var columnID in columns)
    {
        switch(columnID)
        {
            case "Type":
                SelectList.add(e => e.UserType);
                break;
            case "Name":
                SelectList.add(e => e.Name);
                break;
            etc....
        }
    }
    var selectResult = (from u in entities select objSelectList);
}

所以所有属性都是已知的,但是我事先不知道要选择哪些属性。这将通过 columns 参数传递。

I know i'm going to run into issues with the type of the selectResult type, because when the select list is dynamic, the compiler doesn't know what the properties of the anonymous type needs to be.

如果以上不可行:我需要它的场景如下:
我正在尝试创建一个可以实现以显示分页/过滤的数据列表的类。该数据可以是任何东西(取决于实现)。使用的 linq 是 linq 到实体。所以它们直接链接到sql数据。现在我只想选择我在列表中实际显示的实体的列。因此我希望选择是动态的。我的实体可能有一百个属性,但如果列表中只显示其中的 3 个,我不想生成一个查询,选择所有 100 列的数据,然后只使用其中的 3 个。如果有我没有想到的不同方法,我愿意接受想法

编辑:

关于约束的一些说明:
- 查询需要使用 linq 到实体(请参阅问题主题)
- 一个实体可能包含 100 列,因此选择所有列然后只读取我需要的列不是一个选项。
- 最终用户决定显示哪些列,因此要选择的列在运行时确定
- 我需要创建一个 SINGLE select,有多个 select 语句意味着对数据库有多个查询,这是我不想要的

【问题讨论】:

  • 我一直在网上搜索,发现一些人有类似的问题。但是,建议的解决方案使用字符串和 propertyinfo 对象来确定创建选择表达式的成员。我的目标是能够在没有字符串属性查找的情况下获得类型安全的解决方案。就像我在原始帖子中所说的那样,所有属性都是已知的。理想情况下,我想使用 lambda 将表达式指向要选择的属性。
  • 如果你不想使用字符串,你可以使用 PropertyInfo 代替,这是类型安全的。此外,即使您确实使用了字符串,Expression.Property 通过检查属性是否真的存在于声明的类型上并在您的属性名称无效时抛出异常来使其“安全”。
  • 首先,最好提供您正在寻找的方法的签名。其次,不幸的是,LINQ to Entities 不允许投影到实体类型,那么您打算如何处理呢?
  • @TimCopenhaver,您认为所有数据都来自同一个表的假设是错误的。大多数情况下,数据来自查询大量表的视图,并不意味着要选择所有列。但是,是的,我们进行了测试并选择了所有列使我们的 sql 服务器非常热
  • @PaulVrugt select new User { UserType = u.UserType, Name = u.Name } 是有效的 LINQ,select u.UserType, u.Name 不是。无论如何,我只想弄清楚是否可以帮助您。创建动态选择没有问题(如果您在表达式区域中查看我的答案,您可以看到),问题是选择目标对象 type 和 EF 限制。如果您可以创建一个具有所有这些属性的类,但 EF 不将其识别为实体(也称为 DTO 对象),那么它是可行的。另一种选择是 DynamicLINQ,它在运行时为投影创建动态类。

标签: c# entity-framework linq linq-to-entities


【解决方案1】:

使用Expression.MemberInit 方法可以轻松构建编译时已知类型的动态选择表达式,并使用Expression.Bind 方法创建MemberBindings。

这是一个自定义扩展方法:

public static class QueryableExtensions
{
    public static IQueryable<TResult> Select<TResult>(this IQueryable source, string[] columns)
    {
        var sourceType = source.ElementType;
        var resultType = typeof(TResult);
        var parameter = Expression.Parameter(sourceType, "e");
        var bindings = columns.Select(column => Expression.Bind(
            resultType.GetProperty(column), Expression.PropertyOrField(parameter, column)));
        var body = Expression.MemberInit(Expression.New(resultType), bindings);
        var selector = Expression.Lambda(body, parameter);
        return source.Provider.CreateQuery<TResult>(
            Expression.Call(typeof(Queryable), "Select", new Type[] { sourceType, resultType },
                source.Expression, Expression.Quote(selector)));
    }
}

唯一的问题是TResult 类型是什么。在 EF Core 中,您可以传递实体类型(例如您的示例中的 EntityModel.Core.User),它将起作用。在 EF 6 及更早版本中,您需要一个单独的非实体类型,否则您将得到 NotSupportedException - 无法在 LINQ to Entities 查询中构造实体或复杂类型.

更新:如果您想摆脱字符串列,我建议您将扩展方法替换为以下类:

public class SelectList<TSource>
{
    private List<MemberInfo> members = new List<MemberInfo>();
    public SelectList<TSource> Add<TValue>(Expression<Func<TSource, TValue>> selector)
    {
        var member = ((MemberExpression)selector.Body).Member;
        members.Add(member);
        return this;
    }
    public IQueryable<TResult> Select<TResult>(IQueryable<TSource> source)
    {
        var sourceType = typeof(TSource);
        var resultType = typeof(TResult);
        var parameter = Expression.Parameter(sourceType, "e");
        var bindings = members.Select(member => Expression.Bind(
            resultType.GetProperty(member.Name), Expression.MakeMemberAccess(parameter, member)));
        var body = Expression.MemberInit(Expression.New(resultType), bindings);
        var selector = Expression.Lambda<Func<TSource, TResult>>(body, parameter);
        return source.Select(selector);
    }
}

使用示例:

var selectList = new SelectList<EntityModel.Core.User>();
selectList.Add(e => e.UserType);
selectList.Add(e => e.Name);

var selectResult = selectList.Select<UserDto>(entities);

【讨论】:

  • 乍一看,这似乎是我正在寻找的东西。一旦我有时间,我会尝试将它集成到我的测试项目中,看看它是否是我正在寻找的。谢谢你的例子
  • 对,我确实遇到了您所指的 NotSupportedException。但是,查询确实做了我想要的,除了我现在仍然需要使用字符串来指定列的事实。但是,我认为我可以使用它来获得我正在寻找的结果。谢谢!
  • 这肯定是一个干净的解决方案,但这不是您在下面我的回答中提到的同一个问题吗?您必须将列作为字符串列表传递,这是您特别试图避免的。
  • 我很高兴我们现在在同一个页面上。用替代解决方案更新了答案。希望有帮助(或者至少给你一个起点:)
  • @TimCopenhaver,确实这个解决方案在字符串列名方面仍然存在同样的问题,但是这个解决方案适用于 linq to entity。
【解决方案2】:

您的目标是可能的,但并不简单。您可以使用 System.Linq.Expressions 命名空间中的方法和类动态构建 EF 查询。

请参阅this question,了解如何动态构建 Select 表达式的一个很好的示例。

【讨论】:

  • 我找到了这篇文章。但是,我想做的是使用该问题的答案中的代码,但是我不想使用 propertyinfo 和字符串来确定属性,而是想以某种方式使用 lambda 表达式来指出我正在使用的属性。这可能吗?我还没有找到这方面的任何例子
  • 我确信这是可能的,但是让您的 ParameterExpressions 保持一致会很复杂,并且可能不会为您节省太多。听起来您正在为一些极端的过早优化而射击 - 选择额外的字段通常不会导致数据库大幅减速。即使是这样,创建视图和新实体类型也更简单。此外,除非您经常重命名属性,否则使用属性表达式并不比使用字符串特别安全。
  • 不能选择额外的数据。在我的情况下,选择的额外数据将导致非常繁重的数据库查询,我试图避免这种情况。创建新视图也不是一种选择。例如:我正在创建的列表可能有 100 个(可能的)列,最终用户将能够确定要显示的列。我无法为每个列组合创建新的实体类型
【解决方案3】:

我相信这就是你所需要的:

var entities = new List<User>();

entities.Add(new User { Name = "First", Type = "TypeA" });
entities.Add(new User { Name = "Second", Type = "TypeB" });

string[] columns = { "Name", "Type" };

var selectResult = new List<string>();

foreach (var columnID in columns)
{
    selectResult.AddRange(entities.Select(e => e.GetType().GetProperty(columnID).GetValue(e, null).ToString()));
}

foreach (var result in selectResult)
{
    Console.WriteLine(result);
}

此代码输出:

  • 第一
  • 第二个
  • A型
  • B型

更新(根据 cmets)

// initialize alist of entities (User)
var entities = new List<User>();
entities.Add(new User { Name = "First", Type = "TypeA", SomeOtherField="abc" });
entities.Add(new User { Name = "Second", Type = "TypeB", SomeOtherField = "xyz" });

// set the wanted fields
string[] columns = { "Name", "Type" };

// create a set of properties of the User class by the set of wanted fields
var properties = typeof(User).GetProperties()
                        .Where(p => columns.Contains(p.Name))
                        .ToList();

// Get it with a single select (by use of the Dynamic object)
var selectResult = entities.Select(e =>
{
    dynamic x = new ExpandoObject();
    var temp = x as IDictionary<string, Object>;
    foreach (var property in properties)
        temp.Add(property.Name, property.GetValue(e));
    return x;
});

// itterate the results
foreach (var result in selectResult)
{
    Console.WriteLine(result.Name);
    Console.WriteLine(result.Type);
}

此代码输出:

  • 第一
  • A型
  • 第二个
  • B型

【讨论】:

  • 我不认为这是我想要的。这会执行多个选择,这会导致对数据库执行多个查询。我正在寻找一种方法来构建一个选择列表,然后执行一个选择
  • @PaulVrugt,我想现在我明白你需要什么了。我更新了我的答案(使用第二组代码),它有一个选择并在每次迭代中“收集”对象的所有想要的属性。
  • 感谢您的更新!我们正朝着正确的方向前进。但我认为这不适用于 linq to entity 是吗?
  • 为什么不呢?你的意思是实体框架的集合?我看不出它不起作用的原因..
  • 尝试处理实体框架集合会导致以下语法错误:“无法将带有语句体的 lambda 表达式转换为表达式树”。这就是为什么我不相信这种方法适用于实体框架集合
猜你喜欢
  • 2014-10-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-01-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多