【问题标题】:Dynamic Linq Expression in .NET Core.NET Core 中的动态 Linq 表达式
【发布时间】:2020-09-15 14:17:33
【问题描述】:

第一

我不能为此使用实体框架,因为这是一个桌面应用程序,其中所有数据库调用首先通过 Web API 端点进行路由。此应用程序的要求是所有流量都是 HTTP 流量。

设置

假设我有一个IEnumerable<BaseItem>,其中BaseItem 是所有模型都继承自的基本模型类。

我们还使用与查询返回的 System.DataTable 的列名和适当数据类型相匹配的动态类和动态属性动态地实例化和填充我们的集合。

这些动态类也继承自 BaseItem,它公开了两个函数:
void SetValue(string PropertyName, object Value)
object GetValue(string PropertyName)

问题

我需要能够编写一个 linq 查询,该查询根据查询引用的表中的唯一约束列从集合中动态选择属性。我在一个单独的列表中拥有它,其中字符串值与动态类上的动态属性名称匹配。

我知道解决方案可能是使用表达式树,但我发现的所有示例都使用实际的属性引用,在这种情况下,只能通过 GetValue 和 SetValue 函数访问和设置属性。我很难弄清楚如何实现这一目标。如果我知道要返回的字段,下面是我想要的 linq 查询示例。

var Result = DefaultCollection.Select(x => new { BUSINESS_UNIT = x.GetValue("BUSINESS_UNIT"), FISCAL_YEAR = x.GetValue("FISCAL_YEAR") }).Distinct();

同样,我不能写这个,因为我不知道我需要在选择列表中包含哪些字段,直到运行时我可以引用 UniqueConstraint 列表。

如何使用 Linq 表达式动态实现这一点?

从头到尾的例子

您从System.Data.DataTable 填充了IEnumerable<BaseItem>,假设您运行的查询是SELECT FIRST_NAME, LAST_NAME, EMAIL ADDRESS FROM TABLEA

IEnumerable 由自动化填充,其中生成一个动态类来表示表中的一行,并将动态属性添加到该类中以表示表中的每一列。

通过 BaseItem string result = item.GetValue("FIRST_NAME")item.SetValue("EMAIL_ADDRESS", "MyEmail@Email.com'); 中的方法访问属性

您还查询了INFORMATION_SCHEMA 以获取对TABLEA 的唯一约束,并将其存储在public class ConstraintModel 对象中,该对象具有代表构成唯一约束的列的public List<string> Columns。在这种情况下,约束中只有一列“EMAIL_ADDRESS”。

如果用户对IEnumerable<BaseItem> 进行更改并且多个项目具有相同的EMAIL_ADDRESS,我们知道返回SQL 的UPDATE 语句将失败。我们要检查是否违反了唯一性约束。

在用户保存时,我们要对内存中的 IEnumerable 对象运行“验证检查”。理想情况下,我们会编写一个 LINQ 查询,该查询本质上是执行 SELECT SUM(1), EMAIL_ADDRESS GROUP BY EMAIL_ADDRESS,如果任何结果的值超过 1 个,我们就知道存在重复。显然上面的语法是 SQL 而不是 Linq,但你明白了。我无法在编译时知道 BaseItem 上需要选择和分组的字段,因此我需要利用 Constraint.Columns 对象并使用它来编写动态 linq 表达式。此外,Dynamic Class 上的属性仅通过它们从 BaseItem 继承的方法公开...GetValue(string PropertyName)SetValue(string PropertyName, object Value)

【问题讨论】:

  • 您能否举一个dynamically selects properties from the collection based on the unique constraint columns from the table that the query referencedI have that in a separate List, where the string values match the dynamic property names on the dynamic class 的示例(具体来说,向我们展示允许您“基于约束列动态选择属性”和“带有字符串值的列表”的API)
  • 到底是什么
  • 听起来您应该分两步执行此操作。首先,获取您的动态查询结果,然后创建一个新的动态对象,其中仅包含调用者需要的列。或者将结果集转换为 List> 或其他内容,并从每个字典中删除调用者未要求的项目。
  • @zaitsman - 我添加了一个从头到尾的例子,希望能澄清。
  • 通过 http API 调用访问数据是很多人所做的。这不能成为有人认为这种架构是最佳选择的唯一原因。我只是想知道是什么阻止了您的应用程序(不是您现在可以更改它)使用更标准的方法来检索和更改数据(甚至不使用实体框架)。也许有一个特殊的用例迫使你在运行时发现一切,我只是好奇。

标签: c# linq lambda expression


【解决方案1】:

在阅读了您的full example 之后,听起来很像您根本不需要动态 linq 来做您想做的事情,但您确实需要知道主键是什么,为什么不尝试以下操作:

更新我已将简单的 pk 查找替换为 func PK 查找,以便可以使用复合主键。

List<string> primaryKeyColumns = new List<string> {"Id"}; // or whatever else
List<string> uniques = /// ConstraintModel.Columns;
List<BaseItem> baseItems = // your baseItems

// Now, wit
Func<BaseItem, BaseItem, bool> equalPrimaryKey = (left, right) => {
   foreach(var pk in primaryKeyColumns) {
     if(left.GetValue(pk) != right.GetValue(pk)) {
        return false;
     }
   }

   return true;
}; // or whatever else

var violators = baseItems.Select(x => {
   return baseItems.Any(z => {
      foreach(var unique in uniques) {
         if (z.GetValue(unique) == x.GetValue(unique) 
            && !equalPrimaryKey(x, z)) {
            return true;
         }
      }

     return false;
   });
}).ToArray();

这将为您提供一个由任何唯一列重复的基本项目数组

【讨论】:

  • 这个解决方案简单有效。但我认为它失败的地方是:如果主键不止一列。此外,虽然默认情况下主键是唯一约束,并且可能只有一列,但您可以拥有包含多列的其他唯一约束。
  • @Thrifty2100 您可以通过定义方法组轻松修改它以进行复合 PK,将更新我的答案。对于复合唯一约束,您的代码描述这些约束的模式是什么?如果您补充说我也可以为此提供更新
  • 看起来像 var violatersreturns IEnumerable&lt;bool&gt;,所以我将它从 DefaultCollection.Select() 更改为 DefaultCollection.Where()。此外,每个项目都会根据自身进行检查,因此它总是会作为违规返回。所以我在原来的 equalConstraint Func 中添加了if (left.Equals(right)) return false;
【解决方案2】:

使用我的EnumerableExpressionExt 类的缩小版本来帮助为Enumerable 表达式构建Expression 树,以及我的ExpressionExt 助手类:

public static class EnumerableExpressionExt {
    private static Type TEnumerable = typeof(Enumerable);
    private static Type TypeGenArg(this Expression e, int n) => e.Type.GetGenericArguments()[n];

    public static MethodCallExpression Distinct(this Expression src) => Expression.Call(TEnumerable, "Distinct", new[] { src.TypeGenArg(0) }, src);

    public static MethodCallExpression Select(this Expression src, LambdaExpression resultFne) => Expression.Call(TEnumerable, "Select", new[] { src.TypeGenArg(0), resultFne.ReturnType }, src, resultFne);
}

public static class ExpressionExt {
    public static ConstantExpression AsConst<T>(this T obj) => Expression.Constant(obj, typeof(T));

    public static LambdaExpression AsLambda(this Expression body) => Expression.Lambda(body);
    public static LambdaExpression Lambda(this ParameterExpression p1, Expression body) => Expression.Lambda(body, p1);

    public static NewExpression New(this Type t, params Expression[] vals) => Expression.New(t.GetConstructors()[0], vals, t.GetProperties());    

    public static ParameterExpression Param(this Type t, string pName) => Expression.Parameter(t, pName);
}

您可以创建自定义 Expression 扩展来翻译您的特定方法:

public static class MyExpressionExt {
    public static MethodCallExpression GetValue(this Expression obj, string propName) =>
        Expression.Call(obj, "GetValue", null, propName.AsConst());
}

您可以按如下方式构建您的 Expression 树:

// BaseItem x
var xParm = typeof(BaseItem).Param("x");
// typeof(new { object BUSINESS_UNIT, object FISCAL_YEAR })
var anonType = (new { BUSINESS_UNIT = default(object), FISCAL_YEAR = default(object) }).GetType();
// new { BUSINESS_UNIT = x.GetValue("BUSINESS_UNIT"), FISCAL_YEAR = x.GetValue("FISCAL_YEAR") }
var newExpr = anonType.New(xParm.GetValue("BUSINESS_UNIT"), xParm.GetValue("FISCAL_YEAR"));
// DefaultCollection.Select(x => newExpr)
var selExpr = DefaultCollection.AsConst().Select(xParm.Lambda(newExpr));
// DefaultCollection.Select(x => newExpr).Distinct()
var distExpr = selExpr.Distinct();

您可以像这样测试它(使用 LINQPad 的 Dump 方法):

// () => DefaultCollection.Select(x => newExpr).Distinct()
var f = distExpr.AsLambda();
var fc = f.Compile();    
fc.DynamicInvoke().Dump();

LINQPad 也非常有助于输出编译器构建的 Expression 树并查看它们是如何构建的,或者 IQueryable&lt;T&gt; 查询 Expression 成员树。

注意:我喜欢不必打电话给AsConst(),所以我有额外的助手可以让这更容易。

【讨论】:

  • 我将尝试合并它,看看它是否有效。我可能需要使用 linq 来查询我的约束列表以动态填充 newExpr 中的参数#。在您的示例中,它被硬编码为 2。不确定这是否有意义?但挑战在于 1. 我们要选择哪些属性以及 2. 我们要选择多少属性 - 这些都是可变的,并且在编译时是未知的。
  • @Thrifty2100 您只需将值数组而不是离散值传递给params 参数,代码就可以工作。
  • @Thrifty2100 但是,在运行时创建一个具有不同数量的结果字段的匿名对象的价值值得怀疑。也许您可以计划使用DictionaryList&lt;(string,object)&gt; 甚至只是List&lt;object&gt;object[] 来代替?设计为在运行时变化的东西。
猜你喜欢
  • 2017-02-09
  • 2021-08-30
  • 2017-04-13
  • 2011-11-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多