DataTables With Server Side API

2025-01-15

筆記 DataTables 如何透過 Server Side API 的方式來處理大量資料的情況,並且透過 ASP.NET MVC 5 來實作。

logo

說明

首先準備 Model。需要 DataTablesRequestDataTablesResponse 來處理 DataTables 的 Ajax 請求和回應。

其中 DataTablesRequest 還包含了傳入欄位、搜尋條件與排序條件。

在回應 DataTablesResponse 中,需要包含總筆數、過濾後筆數、當前頁面資料,並且必須要是小寫的名稱,為了保持 C# Class 大寫命名,透過 JsonProperty 來指定小寫名稱。

public class DataTablesRequest
{
    public int Draw { get; set; }

    public int Start { get; set; }

    public int Length { get; set; }

    public List<Column> Columns { get; set; }

    public Search Search { get; set; }

    public List<Order> Order { get; set; }
}

public class Column
{
    public string Data { get; set; }
    public string Name { get; set; }
    public bool Searchable { get; set; }
    public bool Orderable { get; set; }
    public Search Search { get; set; }
}

public class Search
{
    public string Value { get; set; }
    public bool Regex { get; set; }
}

public class Order
{
    public int Column { get; set; }
    public string Dir { get; set; }
}

public class LogViewModel
{
    public int Id { get; set; }
    [JsonConverter(typeof(IsoDateTimeConverter))]
    public DateTime Logged { get; set; }
    public string Level { get; set; }
    public string Message { get; set; }
    public string UserName { get; set; }
}

public class DataTablesResponse<T>
{
    [JsonProperty("draw")]
    public int Draw { get; set; }

    [JsonProperty("recordsTotal")]
    public int RecordsTotal { get; set; }

    [JsonProperty("recordsFiltered")]
    public int RecordsFiltered { get; set; }

    [JsonProperty("data")]
    public List<T> Data { get; set; }
}

HomeController 的 API 處理,在進行資料回應時,不使用 JsonResult,而是使用 ContentResult 搭配 Newtonsoft.Json 來進行序列化,以使用 JsonProperty 所指定小寫名稱。

此外定義了三個輔助的方法,BuildQuery 用來建構查詢,ApplySorting 用來進行排序,GetPagedData 用來取得分頁資料。

其中 ApplySorting 使用泛型的方式,減少重複使用時的程式碼異動。但 BuildQueryGetPagedData 則是針對特定的 Model 進行處理,如果要使用必須要調整對應的欄位。

[HttpPost]
public ActionResult GetLogs(DataTablesRequest request)
{
    try
    {
        var totalCount = db.Logs.Count();
        var query = BuildQuery(request); // 建構查詢
        var filteredCount = query.Count(); 
        query = ApplySorting(query, request); // 進行排序
        var data = GetPagedData(query, request); // 取得分頁資料

        // Prepare response data
        var response = new DataTablesResponse<LogViewModel>
        {
            Draw = request.Draw,
            RecordsTotal = totalCount,
            RecordsFiltered = filteredCount,
            Data = data
        };

        return new ContentResult()
        {
            Content = JsonConvert.SerializeObject(response, Formatting.None),
            ContentEncoding = System.Text.Encoding.UTF8,
            ContentType = "application/json"
        };
    }
    catch (Exception ex)
    {
        Debug.WriteLine($"Error in GetLogs: {ex.Message}");
        return Json(new { error = ex.Message });
    }
}

/// <summary>
/// Apply sorting to the query
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="query"></param>
/// <param name="request"></param>
/// <returns></returns>
private IQueryable<T> ApplySorting<T>(IQueryable<T> query, DataTablesRequest request) where T : class
{
    if (request.Order != null && request.Order.Any())
    {
        var orderColumn = request.Columns[request.Order[0].Column];
        var orderDir = request.Order[0].Dir.ToLower();

        // Validate column data to prevent injection attacks
        if (!string.IsNullOrWhiteSpace(orderColumn.Data))
        {
            var parameter = Expression.Parameter(typeof(T), "x");
            var property = Expression.Property(parameter, orderColumn.Data);
            var lambda = Expression.Lambda(property, parameter);

            var methodName = orderDir == "asc" ? "OrderBy" : "OrderByDescending";
            var method = typeof(Queryable).GetMethods()
                .First(m => m.Name == methodName && m.GetParameters().Length == 2)
                .MakeGenericMethod(typeof(T), property.Type);

            query = (IQueryable<T>)method.Invoke(null, new object[] { query, lambda });
        }
    }
    else
    {
        var parameter = Expression.Parameter(typeof(T), "x");
        var property = Expression.Property(parameter, "Id");
        var lambda = Expression.Lambda(property, parameter);

        var method = typeof(Queryable).GetMethods()
            .First(m => m.Name == "OrderByDescending" && m.GetParameters().Length == 2)
            .MakeGenericMethod(typeof(T), property.Type);

        query = (IQueryable<T>)method.Invoke(null, new object[] { query, lambda });
    }

    return query;
}

private IQueryable<Logs> BuildQuery(DataTablesRequest request)
{
    var query = db.Logs.AsQueryable();

    if (!string.IsNullOrEmpty(request.Search?.Value))
    {
        var searchValue = request.Search.Value.ToLower();
        query = query.Where(x =>
            x.Message.ToLower().Contains(searchValue) ||
            x.UserName.ToLower().Contains(searchValue) ||
            x.Level.ToLower().Contains(searchValue) ||
            x.UserName.ToLower().Contains(searchValue)
        );
    }

    return query;
}

private List<LogViewModel> GetPagedData(IQueryable<Logs> query, DataTablesRequest request)
{
    return query
        .Skip(request.Start)
        .Take(request.Length)
        .Select(x => new LogViewModel
        {
            Id = x.Id,
            Logged = x.Logged,
            Level = x.Level,
            Message = x.Message,
            UserName = x.UserName
        })
        .ToList();
}

在 前端 DataTables 呼叫,需要啟用 serverSide 並且設定 processingtrue

並透過加入 ajax 屬性,並且指定 urltypecontentTypedataTypedata 來進行 Ajax 請求,注意這些關鍵屬性都不可以省略,否則會導致錯誤。

此外還必須加入 columns 來對應回傳資料的欄位。

document.addEventListener('DOMContentLoaded', function () {
    var table = document.getElementById('tableId');
    if (table) {
        new DataTable(table, {
            ajax: {
                url: "Logs/GetLogs",
                type: "POST",
                contentType: "application/json",
                dataType: "json",
                data: function (d) {
                    return JSON.stringify(d);
                }
            },
            processing: true,
            serverSide: true,
            searching: true,
            pageLength: 10,
            lengthChange: [10, 50, 100],
            order: [[0, 'desc']], // Default Order
            columnDefs: [
                { targets: 0, searchable: true, orderable: true },
                { targets: -1, searchable: false, orderable: false }
            ],
            columns: [
                { data: 'Id' },
                { data: 'Logged' },
                { data: 'Level' },
                { data: 'Message' },
                { data: 'UserName' }
            ],
            language: {
                lengthMenu: "每頁顯示 _MENU_ 筆",
                zeroRecords: "沒有任何查詢結果,請調整搜尋條件後再執行。",
                info: "搜尋共有 _TOTAL_ 個項目",
                infoEmpty: "",
                infoFiltered: "(filtered from _MAX_ total records)",
                search: "關鍵字搜尋",
            }
        });
    }
});