首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> ASP.NET 基礎 >> ASP.NET 基礎

C# 擴展方法優化多條件動態查詢:告別 SQL 字串拼接

     編輯:ASP.NET 基礎

問題場景

管理系統常見「不定條件查詢」:使用者可填寫 10+ 欄位任意組合查詢。傳統 `StringBuilder` 拼接 SQL 程式碼冗長、難維護、易注入、可讀性差。

擴展方法核心設計


public static class SqlBuilderExtensions {
    // 基礎:等值條件 (參數有值才加入 WHERE)
    public static string WhereIf(this string sql, string column, string value, string op = "=") {
        if (string.IsNullOrWhiteSpace(value)) return sql;
        return $"{sql} AND {column} {op} @{column.Replace(".", "_")}";
        // 實務建議:回傳 (sql, parameters) Tuple 或使用 Dapper 動態參數
    }

    // 進階:字串參數化版本 (搭配 Dapper/ADO.NET 參數集合)
    public static (string Sql, DynamicParameters Params) WhereIf(
        this (string Sql, DynamicParameters Params) tuple, 
        string column, string value, string op = "=") 
    {
        var (sql, p) = tuple;
        if (string.IsNullOrWhiteSpace(value)) return tuple;
        
        string paramName = $"p_{column.Replace(".", "_").Replace("[", "").Replace("]", "")}_{Guid.NewGuid():N}";
        sql += $" AND {column} {op} @{paramName}";
        p.Add(paramName, value);
        return (sql, p);
    }

    // 範圍查詢:起迄皆可選
    public static (string Sql, DynamicParameters Params) BetweenIf(
        this (string Sql, DynamicParameters Params) tuple,
        string column, string start, string end) 
    {
        var (sql, p) = tuple;
        bool hasStart = !string.IsNullOrWhiteSpace(start);
        bool hasEnd = !string.IsNullOrWhiteSpace(end);
        if (!hasStart && !hasEnd) return tuple;

        if (hasStart && hasEnd) {
            string pStart = $"p_{column}_s_{Guid.NewGuid():N}";
            string pEnd = $"p_{column}_e_{Guid.NewGuid():N}";
            sql += $" AND {column} BETWEEN @{pStart} AND @{pEnd}";
            p.Add(pStart, start);
            p.Add(pEnd, end);
        } else if (hasStart) {
            string pStart = $"p_{column}_s_{Guid.NewGuid():N}";
            sql += $" AND {column} >= @{pStart}";
            p.Add(pStart, start);
        } else {
            string pEnd = $"p_{column}_e_{Guid.NewGuid():N}";
            sql += $" AND {column} <= @{pEnd}";
            p.Add(pEnd, end);
        }
        return (sql, p);
    }

    // Like 模糊查詢
    public static (string Sql, DynamicParameters Params) LikeIf(
        this (string Sql, DynamicParameters Params) tuple,
        string column, string value) 
    {
        var (sql, p) = tuple;
        if (string.IsNullOrWhiteSpace(value)) return tuple;
        string paramName = $"p_{column}_like_{Guid.NewGuid():N}";
        sql += $" AND {column} LIKE @{paramName}";
        p.Add(paramName, $"%{value}%");
        return (sql, p);
    }

    // In 子句 (陣列/列表)
    public static (string Sql, DynamicParameters Params) InIf(
        this (string Sql, DynamicParameters Params) tuple,
        string column, IEnumerable values) 
    {
        var (sql, p) = tuple;
        var list = values?.Where(v => v != null).ToList();
        if (list == null || list.Count == 0) return tuple;
        
        // Dapper 支援直接傳 IEnumerable 產生 @p0, @p1...
        sql += $" AND {column} IN @{column}_in";
        p.Add($"{column}_in", list);
        return (sql, p);
    }
}

使用範例:從 80 行縮減為 15 行

傳統寫法 (原始碼)


// 80+ 行 if-else 拼接,參數處理分散,SQL 注入風險
StringBuilder sb = new StringBuilder("SELECT * FROM V_QueryGR WHERE 1=1 ");
if (!string.IsNullOrEmpty(varShipto)) sb.Append(" AND Plant=@Plant ");
if (!string.IsNullOrEmpty(varGRNO)) { /* 範圍邏輯 */ }
// ... 重複 13 次

擴展方法鏈式寫法


using Dapper; // 需安裝 Dapper 套件

public DataTable GetGRCollections(QueryParams q) {
    var baseSql = "SELECT * FROM V_QueryGR WHERE 1=1";
    var p = new DynamicParameters();

    var (sql, params) = (baseSql, p)
        .BetweenIf("GRTime", q.DateFrom, q.DateTo)
        .WhereIf("Plant", q.ShiftTo)
        .WhereIf("GRNO", q.GRNO) // 單值
        // 範圍邏輯需自訂擴展
        .WhereIf("MaterialNO", q.MaterialNO)
        .BetweenIf("PackingNO", q.PL, q.PLTo)
        .WhereIf("CustomID", q.CustomerID)
        .WhereIf("CustomID2", q.CustomerID1)
        .WhereIf("CustomerPN", q.CustomerPN)
        .WhereIf("SuplierPN", q.SupplierPN)
        // Checked 狀態
        .WhereIf("CheckPrice", q.Checked == "Checked" ? "1" : 
                  q.Checked == "UnChecked" ? "0" : null);

    // 執行查詢
    using (var conn = new SqlConnection(connStr)) {
        return conn.Query(sql, params).AsDataTable(); // 擴展方法轉 DataTable
    }
}

// 參數物件 (Model Binding 自動填入)
public class QueryParams {
    public string ShiftTo { get; set; }
    public string GRNO { get; set; }
    public string GRNOTo { get; set; }
    public string MaterialNO { get; set; }
    public string PL { get; set; }
    public string PLTo { get; set; }
    public string CustomerID { get; set; }
    public string CustomerID1 { get; set; }
    public string CustomerPN { get; set; }
    public string DateFrom { get; set; }
    public string DateTo { get; set; }
    public string Checked { get; set; }
    public string SupplierPN { get; set; }
}

進階:動態排序與分頁擴展


public static (string Sql, DynamicParameters Params) OrderByIf(
    this (string Sql, DynamicParameters Params) tuple,
    string column, string direction = "ASC") 
{
    var (sql, p) = tuple;
    if (string.IsNullOrWhiteSpace(column)) return tuple;
    string dir = direction.ToUpper() == "DESC" ? "DESC" : "ASC";
    // 防注入:白名單驗證欄位名
    var allowed = new[] { "GRTime", "GRNO", "Plant", "MaterialNO" };
    if (!allowed.Contains(column)) throw new ArgumentException("非法排序欄位");
    return ($"{sql} ORDER BY {column} {dir}", p);
}

public static (string Sql, DynamicParameters Params) Page(
    this (string Sql, DynamicParameters Params) tuple,
    int pageIndex, int pageSize) 
{
    var (sql, p) = tuple;
    // SQL Server 2012+ OFFSET FETCH
    sql += $" OFFSET @offset ROWS FETCH NEXT @pageSize ROWS ONLY";
    p.Add("offset", (pageIndex - 1) * pageSize);
    p.Add("pageSize", pageSize);
    return (sql, p);
}

完整查詢管線範例


public PagedResult Search(GridQueryRequest req) {
    var (sql, p) = ("SELECT * FROM V_QueryGR WHERE 1=1", new DynamicParameters())
        .WhereIf("Plant", req.Filters.GetValueOrDefault("Plant"))
        .LikeIf("MaterialNO", req.Filters.GetValueOrDefault("MaterialNO"))
        .BetweenIf("GRTime", 
            req.Filters.GetValueOrDefault("DateFrom"),
            req.Filters.GetValueOrDefault("DateTo"))
        .OrderByIf(req.SortColumn, req.SortDirection)
        .Page(req.PageIndex, req.PageSize);

    using var conn = new SqlConnection(_connStr);
    var totalSql = $"SELECT COUNT(1) FROM ({sql.Split(new[]{"ORDER BY"}, StringSplitOptions.None)[0]}) t";
    int total = conn.ExecuteScalar(totalSql, p);
    var items = conn.Query(sql, p).ToList();
    
    return new PagedResult { Items = items, TotalCount = total, PageIndex = req.PageIndex };
}

效能與安全優勢

指標字串拼接擴展方法 + 參數化
程式碼行數80+15-20
SQL 注入風險高 (字串插值)無 (參數化)
計畫快取重用差 (每組合產生新 SQL)佳 (固定參數化模板)
可測試性難 (靜態方法、副作用)易 (純函式、可 Mock)
維護性低 (散落邏輯)高 (集中、可組合)

遷移建議

  1. 引入 Dapper (輕量、高效能、支援 `DynamicParameters`)
  2. 建立 `SqlBuilderExtensions` 靜態類別,集中所有條件擴展方法
  3. 定義查詢參數 DTO,配合 Model Binding 自動對應
  4. 逐個舊查詢方法重構,單元測試驗證 SQL 生成正確性
  5. 加入欄位白名單驗證,防動態排序/欄位注入
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved