概述:想象一下,制作一个图书馆应用程序,用户可以毫不费力地按书名、作者或流派查找书籍。传统的搜索方法将您淹没在代码中。但不要害怕!C# 中的动态查询可以节省一天的时间。✅在我们的故事中,为每个书籍属性制定单独的搜索方法成为一个令人头疼的问题。代码变成了嵌套的 if 或 switch case 语句的迷宫,是一场噩梦:public IEnumerableBook GetBooks(string propertyToFilter, string keyword) { switch (propertyToFilter) { case Title:
想象一下,制作一个图书馆应用程序,用户可以毫不费力地按书名、作者或流派查找书籍。传统的搜索方法将您淹没在代码中。但不要害怕!C# 中的动态查询可以节省一天的时间。
✅在我们的故事中,为每个书籍属性制定单独的搜索方法成为一个令人头疼的问题。代码变成了嵌套的 if 或 switch case 语句的迷宫,是一场噩梦:
public IEnumerable<Book> GetBooks(string propertyToFilter, string keyword)
{
switch (propertyToFilter)
{
case "Title":
return await _books.Where(e => e.Title == keyword).ToListAsync();
case "Author":
return await _books.Where(e => e.Author == keyword).ToListAsync();
case "Genre":
return await _books.Where(e => e.Genre == keyword).ToListAsync();
// More cases for other properties
}
}
随着库的扩展,这些代码会变成一团乱麻,在不断变化的需求的重压下崩溃。
✅输入动态查询,与泛型一起发挥其功能:
IQueryable<T> TextFilter<T>(IQueryable<T> source, string keyword)
{
// The instructions and information in the rest of this article
}
可以将此方法应用于任何实体,在所有字符串属性中搜索关键字。此外,您可以灵活地扩展该方法以支持其他数据类型。
摆脱僵化的束缚。无缝适应不断变化的数据结构。轻松浏览复杂的过滤器。
在软件开发的动态环境中,经常会出现查询的性质需要根据运行时条件进行调整的情况。本文探讨了 C# 中的各种技术,这些技术使用 IQueryable 和表达式树根据运行时状态执行不同的查询。我们将深入探讨一个真实场景,并通过实际示例演示如何实现动态查询。
了解 IQueryable 和表达式树
在深入研究真实世界的例子之前,让我们简要了解一下基本原理。C# 中的 IQueryable 由两个主要组件组成:
表达: 当前查询组件的语言和数据源无关的表示形式,描述为表达式树。
供应商: LINQ 提供程序的实例,负责将查询具体化为一个值或一组值。
在动态查询中,提供程序保持不变,而表达式树随每个查询而变化。
下面是用于根据运行时状态执行不同查询的各种技术:
1.在表达式树中使用运行时状态
2. 调用其他 LINQ 方法
3. 改变传递到 LINQ 方法中的表达式树
4. 使用工厂方法构造表达式树
5. 将方法调用节点添加到 IQueryable 的表达式树中
6. 利用动态 LINQ 库
实际场景:管理员工数据
假设您有一个包含员工数据的 HR 应用程序,每个应用程序都具有不同的属性,例如工资、部门和绩效评级。HR 管理员希望能够根据各种标准动态筛选和分析员工数据。挑战在于构建一个灵活的查询系统,可以处理不同的员工属性和动态用户输入。
var employees = new List<Employee>
{
new(Firstname: "Alice", Lastname: "Williams", Salary: 60000, Department: "IT", PerformanceRating: 4),
new(Firstname: "Bob", Lastname: "Brown", Salary: 75000, Department: "HR", PerformanceRating: 3),
new(Firstname: "Charlie", Lastname: "Taylor", Salary: 50000, Department: "Finance", PerformanceRating: 5),
};
var employeeSource = employees.AsQueryable();
record Employee(string Firstname, string Lastname, decimal Salary, string Department, int? PerformanceRating);
📌动态查询技术
现在,让我们探讨各种技术来处理基于用户输入的动态查询。
1.在表达式树中使用运行时状态
考虑管理员希望根据动态薪资范围筛选员工的场景:
decimal minSalary = 55000;
decimal maxSalary = 75000;
var employeeQuery = employeeSource
.Where(x => x.Salary >= minSalary && x.Salary <= maxSalary);
Console.WriteLine(string.Join(",", employeeQuery.Select(x => $"{x.Firstname} {x.Lastname}")));
// Output: Alice Williams,Bob Brown
**好处:**此方法提供了一种基于简单运行时条件调整查询的直接方法。
2. 调用其他 LINQ 方法
管理员可能还希望根据绩效评级对员工进行动态排序:
bool sortByRating = true;
employeeQuery = employeeSource;
if (sortByRating)
employeeQuery = employeeQuery.OrderBy(x => x.PerformanceRating);
Console.WriteLine(string.Join(",", employeeQuery.Select(x => $"{x.Firstname} {x.Lastname}")));
// Output: Bob Brown,Alice Williams,Charlie Taylor
**好处:**此方法允许有条件地应用各种 LINQ 方法,从而根据特定的运行时方案定制查询。
3. 改变传递到 LINQ 方法中的表达式树
使用 .NET 中的 LINQ 方法,可以根据运行时状态使用不同的表达式。
在此方案中,管理员希望根据部门和绩效评级动态筛选员工:
using System.Linq.Expressions;
string targetDepartment = "IT";
int? targetRating = 4;
Expression<Func<Employee, bool>> expr = (targetDepartment, targetRating) switch
{
("" or null, null) => x => true,
(_, null) => x => x.Department.Equals(targetDepartment),
("" or null, _) => x => x.PerformanceRating >= targetRating,
(_, _) => x => x.Department.Equals(targetDepartment) && x.PerformanceRating >= targetRating
};
employeeQuery = employeeSource.Where(expr);
Console.WriteLine(string.Join(",", employeeQuery.Select(x => $"{x.Firstname} {x.Lastname}")));
// Output: Alice Williams
**好处:**此技术提供了一种基于多个运行时条件动态构造表达式的灵活方法。
4. 使用工厂方法构造表达式树
到目前为止,我们一直在处理一些示例,在这些示例中,我们知道在编译时知道元素和查询的类型,特别是使用字符串和 IQueryable<string>。但是,您可能需要修改不同元素类型的查询或根据元素类型添加组件。可以使用 System.Linq.Expressions.Expression 中的方法从头开始生成表达式树,以便在运行时为特定元素类型自定义表达式。
在探讨我们的方案之前,让我们先介绍构造 Expression<TDelegate> 的过程。请按照下列步骤操作:
1) 导入必要的命名空间:
using System.Linq.Expressions;
2) 使用 Parameter factory 方法为 lambda 表达式中的每个参数创建 ParameterExpression 对象:
ParameterExpression parameter = Expression.Parameter(typeof(string), "x");
3) 使用您定义的 ParameterExpression(s) 和 Expression 提供的工厂方法构建 LambdaExpression 的正文。例如,您可以构造一个像 x.StartsWith(“a”) 这样的表达式,如下所示:
Expression body = Expression.Call(
parameter,
typeof(string).GetMethod("StartsWith", new[] { typeof(string) }),
Expression.Constant("a")
);
4) 使用合适的 Lambda 工厂方法重载,将参数和正文包含在具有编译时类型的 Expression<TDelegate> 中:
Expression<Func<string, bool>> lambda = Expression.Lambda<Func<string, bool>>(body, parameter);
5) 编译 lambda 表达式以获取委托:
Func<string, bool> function = lambda.Compile();
6) 这是完整的例子:
using System;
using System.Linq.Expressions;
class Program
{
static void Main()
{
// Step 2: Define ParameterExpression objects for each parameter
ParameterExpression parameter = Expression.Parameter(typeof(string), "x");
// Step 3: Construct the body of your LambdaExpression
Expression body = Expression.Call(
parameter,
typeof(string).GetMethod("StartsWith", new[] { typeof(string) }),
Expression.Constant("a")
);
// Step 4: Wrap parameters and body in an Expression\<TDelegate>
Expression<Func<string, bool>> lambda = Expression.Lambda<Func<string, bool>>(body, parameter);
// Step 5: Compile the lambda expression to get the delegate
Func<string, bool> function = lambda.Compile();
// Test the compiled function
bool result = function("apple");
Console.WriteLine(result); // Output: True
}
}
我们的场景:
请考虑具有两种实体类型:
record Employee(string Firstname, string Lastname, decimal Salary, string Department, int? PerformanceRating);
record Task(string Title, string Description);
您希望筛选和检索在其中一个字符串字段中具有特定文本的实体。
对于“任务”,可以在“标题”和“说明”属性中搜索:
string term1 = "Project abc";
var tasksQry = new List<Task>()
.AsQueryable()
.Where(x => x.Description.Contains(term1) || x.Title.Contains(term1));
对于“员工”,在“名称”和“部门”属性中:
string term2 = "Alice";
var employeesQry = new List<Employee>()
.AsQueryable()
.Where(x => x.Firstname.Contains(term2) || x.Lastname.Contains(term2));
以下函数允许将此筛选添加到任何现有查询中,而不考虑特定元素类型,而不是为 IQueryable<Task> 和 IQueryable<Employee> 创建单独的函数:
using System.Reflection;
string employeeSearchKeyword = "Alice";
string taskSearchKeyword = "Project abc";
IQueryable<T> TextFilter<T>(IQueryable<T> source, string term)
{
if (string.IsNullOrEmpty(term))
return source;
// T stands for the type of element in the query, decided at compile time
Type elementType = typeof(T);
// Retrieve all string properties from this specific type
PropertyInfo[] stringProperties =
elementType.GetProperties()
.Where(x => x.PropertyType == typeof(string))
.ToArray();
if (!stringProperties.Any())
return source;
// Identify the correct String.Contains overload
MethodInfo containsMethod =
typeof(string).GetMethod("Contains", new[] { typeof(string) })!;
// Create a parameter for the expression tree, represented as 'x' in 'x => x.PropertyName.Contains("term")'
// Define a ParameterExpression object
ParameterExpression prm = Expression.Parameter(elementType);
// Map each property to an expression tree node
IEnumerable<Expression> expressions = stringProperties
.Select<PropertyInfo, Expression>(prp =>
// Construct an expression tree node for each property, like x.PropertyName.Contains("term")
Expression.Call( // .Contains(...)
Expression.Property( // .PropertyName
prm, // x
prp
),
containsMethod,
Expression.Constant(term) // "term"
)
);
// Combine all the resulting expression nodes using || (OR operator).
Expression body = expressions
.Aggregate(
(prev, current) => Expression.Or(prev, current)
);
// Encapsulate the expression body in a compile-time-typed lambda expression
Expression<Func<T, bool>> lambda =
Expression.Lambda<Func<T, bool>>(body, prm);
// Because the lambda is compile-time-typed (albeit with a generic parameter), we can use it with the Where method
return source.Where(lambda);
}
employeeQuery = TextFilter(employeeSource, employeeSearchKeyword);
Console.WriteLine(string.Join(",", employeeQuery.Select(x => $"{x.Firstname} {x.Lastname}")));
// Output: Alice Williams
var taskQuery = TextFilter(taskSource, taskSearchKeyword);
Console.WriteLine(string.Join(",",
taskQuery.Select(x => $"Task Detail:\n\tTitle: {x.Title}\n\tDescription: {x.Description}\n")));
// Output: Task Detail:
// Title: Project abc Status Report
// Description: give a quick summary of how the project has gone before the time period
**好处:**此方法支持动态创建复杂查询,以适应各种搜索条件。
5. 将方法调用节点添加到 IQueryable 的表达式树中
如果使用的是 IQueryable 而不是 IQueryable<T>,则无法轻松使用泛型 LINQ 方法。解决此问题的一种方法是构造内部表达式树,如前所述,然后使用反射调用正确的 LINQ 方法,同时为其提供表达式树。
另一种选择是通过将整个树放在 MethodCallExpression 中来复制 LINQ 方法的操作,该 MethodCallExpression 的作用类似于对 LINQ 方法的调用。
在管理员希望根据动态条件筛选员工并处理非类型化查询的情况下:
IQueryable TextFilter_Untyped(IQueryable source, string term)
{
if (string.IsNullOrEmpty(term))
return source;
Type elementType = source.ElementType;
// Retrieve all string properties from this specific type
PropertyInfo[] stringProperties =
elementType.GetProperties()
.Where(x => x.PropertyType == typeof(string))
.ToArray();
if (!stringProperties.Any())
return source;
// Identify the correct String.Contains overload
MethodInfo containsMethod =
typeof(string).GetMethod("Contains", new[] { typeof(string) })!;
// Create a parameter for the expression tree, represented as 'x' in 'x => x.PropertyName.Contains("term")'
// Define a ParameterExpression object
ParameterExpression prm = Expression.Parameter(elementType);
// Map each property to an expression tree node
IEnumerable<Expression> expressions = stringProperties
.Select<PropertyInfo, Expression>(prp =>
// Construct an expression tree node for each property, like x.PropertyName.Contains("term")
Expression.Call( // .Contains(...)
Expression.Property( // .PropertyName
prm, // x
prp
),
containsMethod,
Expression.Constant(term) // "term"
)
);
// Combine all the resulting expression nodes using || (OR operator).
Expression body = expressions
.Aggregate(
(prev, current) => Expression.Or(prev, current)
);
if (body is null)
return source;
Expression filteredTree = Expression.Call(
typeof(Queryable),
"Where",
new[] { elementType },
source.Expression,
Expression.Lambda(body, prm!)
);
return source.Provider.CreateQuery(filteredTree);
}
var eQuery = TextFilter_Untyped(employeeSource, "Charlie");
Console.WriteLine("5. Adding Method Call Nodes to IQueryable's Expression Tree:");
Console.WriteLine(string.Join(",", eQuery.Cast<Employee>().Select(x => $"{x.Firstname} {x.Lastname}")));
// Output: Charlie Taylor
在此方案中,当您没有编译时 T 通用占位符时,请使用不需要编译时类型信息的 Lambda 重载。这将导致创建 LambdaExpression 而不是 Expression<TDelegate>。
**好处:**这种方法有助于将筛选逻辑动态应用于 IQueryable,而无需编译时类型信息。
6. 利用动态 LINQ 库
使用工厂方法制作表达式树是很困难的。将字符串放在一起更简单。动态 LINQ 库具有与常规 LINQ 方法匹配的 IQueryable 的额外方法,但它们使用具有特殊格式的字符串而不是表达式树。该库将字符串转换为正确的表达式树,并返回翻译后的 IQueryable。
从 NuGet 获取动态 LINQ 库:
dotnet add package System.Linq.Dynamic.Core --version 1.3.10
导入必要的命名空间:
using System.Linq.Dynamic.Core;
在管理员希望使用字符串语法编写查询的更简单方法的情况下:
IQueryable TextFilter_Strings(IQueryable source, string term) {
if (string.IsNullOrEmpty(term))
return source;
var elementType = source.ElementType;
// Retrieve all string properties from this specific type
var stringProperties =
elementType.GetProperties()
.Where(x => x.PropertyType == typeof(string))
.ToArray();
if (!stringProperties.Any()) { return source; }
// Build the string expression
string filterExpr = string.Join(" || ",
stringProperties.Select(prp => $"{prp.Name}.Contains(@0)"));
return source.Where(filterExpr, term);
}
var qry = TextFilter_Untyped(employeeSource, "HR");
Console.WriteLine("6. Leveraging the Dynamic LINQ Library:");
Console.WriteLine(string.Join(",", qry.Cast<Employee>().Select(x => $"{x.Firstname} {x.Lastname}")));
// Output: Bob Brown
好处: 动态 LINQ 库通过接受字符串表达式来简化动态查询的构造。
C# 中的动态查询提供了强大的工具,用于使查询适应不同的运行时条件。通过了解 IQueryable 和表达式树,开发人员可以创建灵活高效的系统,动态响应用户输入。员工管理系统的真实场景展示了这些技术在构建强大且适应性强的软件解决方案中的实际应用。根据方案的复杂程度选择适当的方法,并为应用程序提供动态查询功能。
如果你喜欢我的文章,请给我一个赞!谢谢