.NET Core 中的动态 Linq 表达式

Dynamic Linq Expression in .NET Core

第一

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

设置

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

我们还使用动态 class 和动态属性动态实例化并填充我们的集合,这些动态属性与 System.DataTable 的列名和适当的数据类型相匹配,return 从查询。

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

问题

我需要能够编写一个 linq 查询,根据查询引用的 table 中的唯一约束列动态地从集合中获取 selects 属性。我在一个单独的列表中有它,其中字符串值与动态 class.

上的动态 属性 名称匹配

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

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

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

如何使用 Linq Expression 动态实现此目的?

从头到尾示例

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

IEnumerable 由自动化生成,其中生成动态 class 以表示 table 中的一行,并将动态属性添加到 class 以表示中的每一列table.

通过 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,但你明白了。我无法在编译时知道我需要 select 和分组依据的 BaseItem 上的哪些字段,因此我需要利用 Constraint.Columns 对象并使用它来编写动态 linq 表达式。此外,动态 Class 上的属性仅通过它们从 BaseItem 继承的方法公开... GetValue(string PropertyName)SetValue(string PropertyName, object Value);

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

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<T> 查询 Expression 成员树。

注意:我喜欢不用打电话给 AsConst(),所以我有更多的助手可以让这件事变得更容易。

读完你的 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();

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