简体   繁体   English

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

[英]Dapper query with dynamic list of filters

I have ac# mvc app using Dapper.我有使用 Dapper 的 ac# mvc 应用程序。 There is a list table page which has several optional filters (as well as paging).有一个列表页面,它有几个可选的过滤器(以及分页)。 A user can select (or not) any of several (about 8 right now but could grow) filters, each with a drop down for a from value and to value.用户可以选择(或不选择)几个(现在大约有 8 个,但可能会增加)过滤器中的任何一个,每个过滤器都有一个下拉列表,用于显示起始值和终止值。 So, for example, a user could select category "price" and filter from value "$100" to value "$200".因此,例如,用户可以选择类别“价格”并从值“$100”过滤到值“$200”。 However, I don't know how many categories the user is filtering on before hand and not all of the filter categories are the same type (some int, some decimal/double, some DateTime, though they all come in as string on FilterRange ).但是,我不知道用户事先过滤了多少个类别,并且并非所有的过滤器类别都是相同的类型(有些是整数,有些是十进制/双FilterRange ,有些是 DateTime,尽管它们都以stringFilterRange ) .

I'm trying to build a (relatively) simple yet sustainable Dapper query for this.我正在尝试为此构建一个(相对)简单但可持续的 Dapper 查询。 So far I have this:到目前为止,我有这个:

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();
    }
}    

I Keep getting an error saying "... filterRanges cannot be used as a parameter value"我不断收到错误消息“... filterRanges 不能用作参数值”

Is it possible to even do this in Dapper?甚至可以在 Dapper 中做到这一点吗? All of the IEnumerable examples I see are where in _ which doesn't fit this situation.我看到的所有IEnumerable示例都where in _中不适合这种情况。 Any help is appreciated.任何帮助表示赞赏。

You can use a list of dynamic column values but you cannot do this also for the column name other than using string format which can cause a SQL injection.您可以使用动态列值列表,但除了使用可能导致 SQL 注入的字符串格式之外,您也不能对列名执行此操作。

You have to validate the column names from the list in order to be sure that they really exist before using them in a SQL query.在 SQL 查询中使用它们之前,您必须验证列表中的列名,以确保它们确实存在。

This is how you can use the list of filterRanges dynamically :这是您可以动态使用 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();
}

You can use DynamicParameters class for generic fields.您可以将 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);

You can easily create that dynamic condition using DapperQueryBuilder :您可以使用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();
}

Or without the magic word /**where**/ :或者没有魔法词/**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();
}

The output is fully parametrized SQL, even though it looks like we're doing plain string concatenation.输出是完全参数化的 SQL,即使看起来我们正在执行纯字符串连接。

Disclaimer: I'm one of the authors of this library免责声明:我是这个库的作者之一

I was able to find a solution for this.我能够为此找到解决方案。 The key was to convert the List to a Dictionary .关键是将List转换为Dictionary I created a private method:我创建了一个私有方法:

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;
}

Then to build my query,然后建立我的查询,

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

Special note: Be very careful here as the column name is not a parameter and you could open your self up to injection attacks (as @Popa noted in his answer).特别注意:这里要非常小心,因为列名不是参数,你可以打开自己的注入攻击(正如@Popa 在他的回答中指出的那样)。 In my case those values come from an enum class and not from user in put so I am safe.在我的情况下,这些值来自枚举类而不是用户输入,所以我很安全。

The rest is pretty straight forwared:其余的很直接:

using (var conn = OpenConnection())
{
    string query = @"Select * from  Sales " 
        + ranges
        + skipTake;

    return conn.Query<Sale>(query, filter).AsList();
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM