【问题标题】:Dapper query with dynamic list of filters带有动态过滤器列表的 Dapper 查询
【发布时间】:2018-06-09 02:40:57
【问题描述】:

我有一个使用 Dapper 的 c# mvc 应用程序。有一个列表页,它有几个可选的过滤器(以及分页)。用户可以选择(或不选择)几个(现在大约 8 个,但可能会增长)过滤器中的任何一个,每个过滤器都有一个下拉列表,用于从值和到值。因此,例如,用户可以选择类别“价格”并从值“$100”过滤到值“$200”。但是,我不知道用户事先过滤了多少个类别,并且并非所有过滤器类别都是相同的类型(一些 int、一些小数/双精度、一些 DateTime,尽管它们都以string 的形式出现在FilterRange)。

我正在尝试为此构建一个(相对)简单但可持续的 Dapper 查询。到目前为止,我有这个:

public List<PropertySale> GetSales(List<FilterRange> filterRanges, int skip = 0, int take = 0)
{
    var skipTake = " order by 1 ASC OFFSET @skip ROWS";
    if (take > 0)
        skipTake += " FETCH NEXT @take";

    var ranges = " WHERE 1 = 1 ";

    for(var i = 0; i < filterRanges.Count; i++)
    {
        ranges += " AND @filterRanges[i].columnName BETWEEN @filterRanges[i].fromValue AND @filterRanges[i].toValue ";
    }

    using (var conn = OpenConnection())
    {

        string query = @"Select * from  Sales " 
            + ranges
            + skipTake;

        return conn.Query<Sale>(query, new { filterRanges, skip, take }).AsList();
    }
}    

我不断收到错误消息“... filterRanges 不能用作参数值”

在 Dapper 中甚至可以做到这一点吗?我看到的所有IEnumerable 示例都是where in _,不适合这种情况。任何帮助表示赞赏。

【问题讨论】:

    标签: c# sql dapper


    【解决方案1】:

    您可以将 DynamicParameters 类用于通用字段。

                    Dictionary<string, object> Filters = new Dictionary<string, object>();
                    Filters.Add("UserName", "admin");
                    Filters.Add("Email", "admin@admin.com");
                    var builder = new SqlBuilder();
                    var select = builder.AddTemplate("select * from SomeTable /**where**/");
                    var parameter = new DynamicParameters();
                    foreach (var filter in Filters)
                    {
                        parameter.Add(filter.Key, filter.Value);
                        builder.Where($"{filter.Key} = @{filter.Key}");                        
                    }
    
    
                    var searchResult = appCon.Query<ApplicationUser>(select.RawSql, parameter);
    

    【讨论】:

      【解决方案2】:

      您可以使用动态列值列表,但不能对列名执行此操作,除非使用可能导致 SQL 注入的字符串格式。

      在 SQL 查询中使用它们之前,您必须验证列表中的列名以确保它们确实存在。

      这是动态使用 filterRanges 列表的方法:

      const string sqlTemplate = "SELECT /**select**/ FROM Sale /**where**/ /**orderby**/";
      
      var sqlBuilder = new SqlBuilder();
      var template = sqlBuilder.AddTemplate(sqlTemplate);
      
      sqlBuilder.Select("*");
      
      for (var i = 0; i < filterRanges.Count; i++)
      {
          sqlBuilder.Where($"{filterRanges[i].ColumnName} = @columnValue", new { columnValue = filterRanges[i].FromValue });
      }
      
      using (var conn = OpenConnection())
      {
          return conn.Query<Sale>(template.RawSql, template.Parameters).AsList();
      }
      

      【讨论】:

      • 谢谢@Popa,我会试一试。列名和值都映射到枚举类中,所以我可以安全地防止注入。
      【解决方案3】:

      您可以使用DapperQueryBuilder轻松创建动态条件:

      using (var conn = OpenConnection())
      {
          var query = conn.QueryBuilder($@"
              SELECT * 
              FROM Sales
              /**where**/
              order by 1 ASC 
              OFFSET {skip} ROWS FETCH NEXT {take}
          ");
      
          foreach (var filter in filterRanges)
              query.Where($@"{filter.ColumnName:raw} BETWEEN 
                             {filter.FromValue.Value} AND {filter.ToValue.Value}");
      
          return conn.Query<Sale>(query, new { filterRanges, skip, take }).AsList();
      }
      

      或者没有魔法词/**where**/:

      using (var conn = OpenConnection())
      {
          var query = conn.QueryBuilder($@"
              SELECT * 
              FROM Sales
              WHERE 1=1
          ");
      
          foreach (var filter in filterRanges)
              query.Append($@"{filter.ColumnName:raw} BETWEEN 
                             {filter.FromValue.Value} AND {filter.ToValue.Value}");
      
          query.Append($"order by 1 ASC OFFSET {skip} ROWS FETCH NEXT {take}");
      
          return conn.Query<Sale>(query, new { filterRanges, skip, take }).AsList();
      }
      

      输出是完全参数化的 SQL,尽管看起来我们正在执行纯字符串连接。

      免责声明:我是这个库的作者之一

      【讨论】:

      • 太棒了,下次我做这样的事情时我会检查一下。
      【解决方案4】:

      我能够找到解决方案。关键是将List 转换为Dictionary。我创建了一个私有方法:

      private Dictionary<string, object> CreateParametersDictionary(List<FilterRange> filters, int skip = 0, int take = 0)
      {
          var dict = new Dictionary<string, object>()
          {
              { "@skip", skip },
              { "@take", take },
          };
      
          for (var i = 0; i < filters.Count; i++)
          {
              dict.Add($"column_{i}", filters[i].Filter.Description);
      
              // some logic here which determines how you parse
              // I used a switch, not shown here for brevity
              dict.Add($"@fromVal_{i}", int.Parse(filters[i].FromValue.Value));
              dict.Add($"@toVal_{i}", int.Parse(filters[i].ToValue.Value));                         
          }
          return dict;
      }
      

      然后构建我的查询,

      var ranges = " WHERE 1 = 1 ";
      for(var i = 0; i < filterRanges.Count; i++)
          ranges += $" AND {filter[$"column_{i}"]} BETWEEN @fromVal_{i} AND @toVal_{i} ";
      

      特别注意:这里要非常小心,因为列名不是参数,您可能会向注入攻击敞开心扉(正如@Popa 在他的回答中指出的那样)。在我的情况下,这些值来自枚举类,而不是来自用户,所以我很安全。

      剩下的就很直接了:

      using (var conn = OpenConnection())
      {
          string query = @"Select * from  Sales " 
              + ranges
              + skipTake;
      
          return conn.Query<Sale>(query, filter).AsList();
      }
      

      【讨论】:

        猜你喜欢
        • 2011-12-14
        • 2012-10-28
        • 2021-05-11
        • 2017-09-10
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-06-10
        • 2021-03-08
        相关资源
        最近更新 更多