使用 Linq 表达式进行 C# 动态数据库过滤

Posted

技术标签:

【中文标题】使用 Linq 表达式进行 C# 动态数据库过滤【英文标题】:C# Dynamic database filtering with Linq Expression 【发布时间】:2018-11-21 16:07:56 【问题描述】:

我尝试创建用于过滤数据库中不同实体的通用方法,以避免为每个实体创建巨大的方法,并使用非常相似的 if 语句组合过滤规则。

现在我正在尝试创建表示相等比较的表达式,将其作为描述要比较的属性和一些值的参数表达式。

我的概念如何解决问题以及我根据其他帖子创建的内容显示如下代码 sn-p:

public class FuelCard 

    public int Id  get; set; 
    public string Number  get; set; 
    public virtual User User  get; set; 


public static IQueryable<TEntity> ApplyFilter<TEntity, TProperty>(
    this IQueryable<TEntity> query, 
    Expression<Func<TEntity, TProperty>> expr, TProperty value)

    Expression<Func<TEntity, bool>> predicate = param => true;

    var filterExpression = Expression.Equal(expr, Expression.Constant(value));
    var lambda = Expression.Lambda<Func<TEntity, bool>>(filterExpression);

    predicate = predicate.And(lambda);
    return query.Where(predicate);

最后我想这样使用它:

IQueryable<FuelCard> query = // Get cards from database as IQueryable

query = query.ApplyFilter(x => x.Id, 85);
query = query.ApplyFilter(x => x.User.LastName + " " + x.User.FirstName, "Jon Green");

我想定义匿名表达式,描述如何获取列的值,然后应用不同的过滤器(这里显示了使用 equal 方法的简单示例)。

但是当我调用 Expression.Equal 时,我得到一个错误,即 Func 和 Int32 没有二元运算符。

在所有示例中,都创建了具有属性名称的 Expression.Parameter 对象,但它们仅对 Entity 类中的属性进行操作(不使用导航属性等)。 但是是否可以将过滤器表达式与匿名属性表达式结合起来?

我希望我清楚地描述了我想要实现的目标以及与标准示例有什么区别,这是我的问题的根源

如果有人帮助我如何创建这样的过滤器,以便将参数中给定的表达式的结果与值进行比较,然后将谓词应用于查询以便针对 sql 数据库运行它,我将不胜感激 :)

【问题讨论】:

有时,当您尝试创建许多复杂的方法时,您使用的 API 是错误的。我认为这可能就是这种情况,当您可以执行 query.where(x=> x.id == 86), query.FirstOrDefault(x=> x.LastName == 时,为什么要编写自己的“ApplyFilter”? Green" && x.FirstName == "Jon") 等 @AleksAndreev 很抱歉,我正在更改我的代码以仅显示问题的一小部分。 ApplyFilter 方法是 CombineQuery。我编辑了问题并将名称更改为正确的名称。 @chrispepper1989 但是我有大约 100 个表,每个表都有很多列,我不想手动编写这样的 query.where(x=> x.id == 86) 等。对于每个 get 方法和属性。我想创建一种通用方法,以便使用从客户端应用程序获得的参数过滤所有表 那么你想使用接口并使用接口使用泛型方法,你也可以使用“column”属性来确保你的表列与你的类列命名不同 【参考方案1】:

类似于:

public static IQueryable<TSource> WhereEqual<TSource, TProperty>(this IQueryable<TSource> query, Expression<Func<TSource, TProperty>> propertySelector, TProperty value)

    var body2 = Expression.Equal(propertySelector.Body, Expression.Constant(value));
    var lambda = Expression.Lambda<Func<TSource, bool>>(body2, propertySelector.Parameters);
    return query.Where(lambda);

像这样使用它:

IQueryable<FuelCard> query = // Get cards from database as IQueryable

query = query.WhereEqual(x => x.Id, 85);
query = query.WhereEqual(x => x.User.LastName + " " + x.User.FirstName, "Jon Green");

通常多个.Where() 隐含在&amp;&amp;(在中)它们之间。所以你只需要根据属性选择器表达式加上等于加上传递的值创建一个表达式,然后返回一个使用该表达式的.Where()

【讨论】:

【参考方案2】:

Xanatos 确实已经回答了您提出的问题,但我只是想演示一些替代方法来概括您的数据库表和查询​​语句。第一种是使用函数+接口而不是lambda:

 context.Users.Where(ReallyComplexExpression());
 private static Expression<Func<IHasName, bool>> ReallyComplexExpression()
    
        return x =>
            x.FirstName != null && x.LastName == "Salmon" || x.FirstName == "Fish" && x.LastName == "hermonie" ||
            x.FirstName == "Thanks" && x.LastName == "for the fish";
    

另一种方法是通用单例或通用静态函数,它们确实倾向于与 Linq -> SQL 一起运行,并且通常最终变得更具可读性

这很粗略,没有跑,但它显示了基本概念:

public interface IHasId

    int Id  get; set; 

public interface IHasName

    string FirstName  get;  
    string LastName  get;  


public class Employee : IHasName, IHasId

    [Column("Forename")] //this ensures it is still called "Forename" in the database
    public string FirstName  get; set; 
    [Column("Surname")]
    public string LastName  get; set; 
    [Key]
    [Column("EmployeeId")]//this ensures you are following DB best practice through the same technique as above
    public int Id  get; set; 

public class User : IHasName, IHasId

    public string FirstName  get; set; 
    public string LastName  get; set; 
    [Key]
    [Column("UserId")]
    public int Id  get; set; 

    //equally you could add
    [notmapped]
    public string FullName => $"FirstName LastName"; 
 //or add this as an extension function for all IHasName


public class Job: IHasName, IHasId

    public string Name  get; set; 

    [NotMapped] //not mapped tells entity framework you dont want these fields in the DB, so in the DB their would only be "name" but in your code, firstname & lastname are pseudonyms for Name 
    public string FirstName => Name;

    [NotMapped]
    public string LastName => Name;
    [Key]
    [Column("JobId")]
    public int Id  get; set; 




public class ModelContext : DbContext

    public DbSet<Employee> Employees  get; set; 
    public DbSet<User> Users  get; set; 
    public DbSet<Job> Jobs  get; set; 


public static class GenericExtensions


    public static IQueryable<TModel> GetById<TModel>(this IQueryable<TModel> db, int Id) where TModel : class, IHasId
    
        return db.Where(x => x.Id == Id);
    
    public static IQueryable<TModel> GetByAnyName<TModel>(this IQueryable<TModel> db, string name) where TModel : class, IHasName
    
        return db.Where(x => x.FirstName == name || x.LastName == name);
    
    public static IQueryable<TModel> GetByFirstAndLastName<TModel>(this IQueryable<TModel> db, string firstName, string lastName) where TModel : class, IHasName
    
        return db.Where(x => x.FirstName == firstName && x.LastName == lastName);
    


class Program

    static void Main(string[] args)
    
        ModelContext context = new ModelContext();

        var filteredEmpl =context.Employees.GetByAnyName("John").GetById(1);

        var filteredUsers =context.Users.GetByFirstAndLastName("Terry","Richards").GetById(1);

         var filteretJobs =context.Jobs.GetByAnyName("Supervisor").GetById(1); //this wouldn't work as "Name" is the DB column, so Linq->SQL would fail when confronted with "FirstName" 
    

【讨论】:

您可以为作业添加一个专业化,例如“ public static IQueryable GetByAnyName(this IQueryable db, string name) return db.Where(x => x.Name == 名称); ".

以上是关于使用 Linq 表达式进行 C# 动态数据库过滤的主要内容,如果未能解决你的问题,请参考以下文章

翻译C#表达式中的动态查询

具有嵌套属性的动态 linq 表达式树

通过动态创建 linq 查询,在 c# 中为 Sql Equivalent “column is null”创建 Linq 表达式

C#基础:LINQ 查询函数整理

如何为CriteriaOperator过滤对象转换为lambda表达式,即:linq to xpo的动态where语句

深入LINQ | 动态构建LINQ表达式