带有动态过滤器列表的精简查询

时间:2018-06-09 02:40:57

标签: c# sql dapper

我有一个使用Dapper的c#mvc应用程序。有一个列表表页面,它有几个可选的过滤器(以及分页)。用户可以选择(或不选择)几个(现在大约8个但可能会增长)过滤器中的任何一个,每个过滤器都有一个从值到值的下拉列表。因此,例如,用户可以选择类别"价格"从价值过滤" $ 100"价值200美元"。但是,我不知道用户过滤了多少类别,并且并非所有过滤器类别都是相同的类型(某些int,一些十进制/双精度,一些DateTime,尽管它们都以{{{{ 1}} string}。

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

FilterRange

我继续收到错误说" ... filterRanges不能用作参数值"

甚至可以在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(); } } 示例都是IEnumerable,并不适合这种情况。任何帮助表示赞赏。

4 个答案:

答案 0 :(得分:1)

您可以使用动态列值列表,但除了使用可能导致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();
}

答案 1 :(得分:0)

我能够找到解决方案。关键是将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();
}

答案 2 :(得分:0)

您可以将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);

答案 3 :(得分:0)

您可以使用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。

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