DataTablesDotAspNetCore 26.2.10.1

dotnet add package DataTablesDotAspNetCore --version 26.2.10.1
                    
NuGet\Install-Package DataTablesDotAspNetCore -Version 26.2.10.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="DataTablesDotAspNetCore" Version="26.2.10.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="DataTablesDotAspNetCore" Version="26.2.10.1" />
                    
Directory.Packages.props
<PackageReference Include="DataTablesDotAspNetCore" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add DataTablesDotAspNetCore --version 26.2.10.1
                    
#r "nuget: DataTablesDotAspNetCore, 26.2.10.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package DataTablesDotAspNetCore@26.2.10.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=DataTablesDotAspNetCore&version=26.2.10.1
                    
Install as a Cake Addin
#tool nuget:?package=DataTablesDotAspNetCore&version=26.2.10.1
                    
Install as a Cake Tool

DataTables.AspNetCore

NuGet License: MIT

Server-side processing helpers for jQuery DataTables in ASP.NET Core.

This library focuses on:

  • ✅ Binding DataTables server-side request payloads into strongly-typed request models (via ASP.NET Core model binding)
  • ✅ Producing DataTables-compliant responses (including optional additional parameters)
  • ✅ Supporting both modern camelCase and legacy Hungarian notation
  • ✅ Seamless integration with Entity Framework Core
  • ✅ Draw validation for enhanced security

Table of Contents


Requirements

  • .NET 9 (recommended) or .NET 10
  • ASP.NET Core (Razor Pages, MVC, or Minimal APIs)
  • jQuery DataTables 1.10+ (client-side)

Installation

NuGet Package Manager Console

Install-Package DataTables.AspNetCore

.NET CLI

dotnet add package DataTables.AspNetCore

PackageReference

<PackageReference Include="DataTables.AspNetCore" Version="*" />

Quick Start

1. Register Services

Program.cs

var builder = WebApplication.CreateBuilder(args);

// Register DataTables services
builder.Services.AddDataTables();

// Register Razor Pages (recommended for this workspace)
builder.Services.AddRazorPages();

var app = builder.Build();
app.UseStaticFiles();
app.MapRazorPages();
app.Run();

2. Create a Page Handler

Pages/Customers.cshtml.cs

using DataTables.AspNetCore.Base;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

public class CustomersModel : PageModel
{
    private static readonly List<CustomerDto> _data = new()
    {
        new CustomerDto(1, "Ada Lovelace", "ada@example.com"),
        new CustomerDto(2, "Alan Turing", "alan@example.com"),
        new CustomerDto(3, "Grace Hopper", "grace@example.com"),
    };

    public void OnGet() { }
    
    public IActionResult OnPostData([FromBody] IDataTableRequest request)
    {
        var query = _data.AsQueryable();

        // Global search
        if (!string.IsNullOrWhiteSpace(request.Search?.Value))
        {
            var searchValue = request.Search.Value;
            query = query.Where(x =>
                x.Name.Contains(searchValue, StringComparison.OrdinalIgnoreCase) ||
                x.Email.Contains(searchValue, StringComparison.OrdinalIgnoreCase));
        }

        var totalRecords = _data.Count;
        var filteredRecords = query.Count();

        // Paging
        var data = query
            .Skip(request.Start)
            .Take(request.Length)
            .ToList();

        return new JsonResult(request.CreateResponse(totalRecords, filteredRecords, data));
    }
}

public sealed record CustomerDto(int Id, string Name, string Email);

3. Add the DataTable to Your Page

Pages/Customers.cshtml

@page
@model CustomersModel
<!DOCTYPE html>
<html>
<head>
    <title>Customers</title>
    <link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/jquery.dataTables.min.css" />
</head>
<body>
    <table id="customers" class="display">
        <thead>
            <tr>
                <th>ID</th>
                <th>Name</th>
                <th>Email</th>
             </tr>
         </thead>
     </table>
    <script src="https://code.jquery.com/jquery-3.7.0.min.js"></script>
    <script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
    <script>
        $(function () {
            $('#customers').DataTable({
                serverSide: true,
                processing: true,
                ajax: {
                    url: '@Url.Page("Customers", "Data")',
                    type: 'POST',
                    contentType: 'application/json',
                    data: function (d) {
                        return JSON.stringify(d);
                    }
                },
                columns: [
                    { data: 'id' },
                    { data: 'name' },
                    { data: 'email' }
                ]
            });
        });
    </script>
</body>
</html>

Configuration

Basic Setup

Program.cs

builder.Services.AddDataTables();

Custom Options

Configure DataTables behavior with custom options:

Program.cs

builder.Services.AddDataTables(new DataTableOptions()
    .SetDefaultPageLength(25)           // Default records per page
    .EnableDrawValidation()             // Prevent CSRF attacks (recommended)
    .UseCamelCase());                   // Use camelCase for DataTables 1.10+

Additional Parameters

Send and receive custom parameters alongside the standard DataTables payload:

Program.cs

using Microsoft.AspNetCore.Mvc.ModelBinding;

builder.Services.AddDataTables(
    parseRequestAdditionalParameters: (ModelBindingContext context) =>
    {
        var additional = new Dictionary<string, object>();

        // Example: department filter
        var departmentId = context.ValueProvider.GetValue("departmentId").FirstValue;
        if (!string.IsNullOrEmpty(departmentId) && int.TryParse(departmentId, out var deptId))
        {
            additional["departmentId"] = deptId;
        }

        // Example: date range filter
        var startDate = context.ValueProvider.GetValue("startDate").FirstValue;
        if (!string.IsNullOrEmpty(startDate) && DateTime.TryParse(startDate, out var start))
        {
            additional["startDate"] = start;
        }

        return additional;
    },
    parseResponseAdditionalParameters: true
);

Client-side usage:

$('#myTable').DataTable({
    serverSide: true,
    ajax: {
        url: '@Url.Page("MyPage", "Data")',
        type: 'POST',
        contentType: 'application/json',
        data: function (d) {
            d.departmentId = $('#departmentFilter').val();
            d.startDate = $('#startDateFilter').val();
            return JSON.stringify(d);
        }
    }
});

Server-side access:

public IActionResult OnPostData([FromBody] IDataTableRequest request)
{
    if (request.AdditionalParameters.TryGetValue("departmentId", out var deptIdObj))
    {
        var departmentId = Convert.ToInt32(deptIdObj); // Apply department filter
    }
    // ... rest of handler
}

Examples

See Quick Start above for a complete Razor Pages example.

Enable individual column search filters:

Pages/Products.cshtml.cs

public IActionResult OnPostData([FromBody] IDataTableRequest request)
{
    var query = _context.Products.AsQueryable();

    // Global search
    if (!string.IsNullOrWhiteSpace(request.Search?.Value))
    {
        var searchValue = request.Search.Value;
        query = query.Where(p =>
            p.Name.Contains(searchValue) ||
            p.Description.Contains(searchValue));
    }

    // Column-specific search
    foreach (var column in request.Columns.Where(c => c.Searchable && !string.IsNullOrWhiteSpace(c.Search?.Value)))
    {
        var searchValue = column.Search.Value;

        switch (column.Data)
        {
            case "name":
                query = query.Where(p => p.Name.Contains(searchValue));
                break;
            case "category":
                query = query.Where(p => p.Category.Contains(searchValue));
                break;
            case "price":
                if (decimal.TryParse(searchValue, out var price))
                {
                    query = query.Where(p => p.Price == price);
                }
                break;
        }
    }

    var totalRecords = _context.Products.Count();
    var filteredRecords = query.Count();
    var data = query.Skip(request.Start).Take(request.Length).ToList();

    return new JsonResult(request.CreateResponse(totalRecords, filteredRecords, data));
}

Custom Sorting

Handle custom sorting logic:

Pages/Orders.cshtml.cs

public IActionResult OnPostData([FromBody] IDataTableRequest request)
{
    var query = _context.Orders.Include(o => o.Customer).AsQueryable();

    // Apply sorting
    if (request.Sorting.Any())
    {
        var firstSort = request.Sorting.First();
        var column = request.Columns[firstSort.ColumnIndex];

        query = column.Data switch
        {
            "orderDate" => firstSort.IsAscending
                ? query.OrderBy(o => o.OrderDate)
                : query.OrderByDescending(o => o.OrderDate),
            "customerName" => firstSort.IsAscending
                ? query.OrderBy(o => o.Customer.Name)
                : query.OrderByDescending(o => o.Customer.Name),
            "total" => firstSort.IsAscending
                ? query.OrderBy(o => o.Total)
                : query.OrderByDescending(o => o.Total),
            _ => query.OrderByDescending(o => o.OrderDate) // Default sort
        };

        // Apply additional sorts
        foreach (var sort in request.Sorting.Skip(1))
        {
            var col = request.Columns[sort.ColumnIndex];
            // Apply ThenBy/ThenByDescending logic
        }
    }

    var totalRecords = _context.Orders.Count();
    var filteredRecords = query.Count();
    var data = query.Skip(request.Start).Take(request.Length).ToList();

    return new JsonResult(request.CreateResponse(totalRecords, filteredRecords, data));
}

Entity Framework Core Integration

Full example with EF Core, async operations, and projections:

Pages/Employees.cshtml.cs

using DataTables.AspNetCore.Base;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;

public class EmployeesModel : PageModel
{
    private readonly ApplicationDbContext _context;

    public EmployeesModel(ApplicationDbContext context)
    {
        _context = context;
    }

    public void OnGet() { }

    public async Task<IActionResult> OnPostDataAsync([FromBody] IDataTableRequest request)
    {
        // Start with base query
        var query = _context.Employees
            .Include(e => e.Department)
            .AsQueryable();

        // Total records before filtering
        var totalRecords = await _context.Employees.CountAsync();

        // Apply global search
        if (!string.IsNullOrWhiteSpace(request.Search?.Value))
        {
            var searchValue = request.Search.Value.ToLower();
            query = query.Where(e =>
                e.FirstName.ToLower().Contains(searchValue) ||
                e.LastName.ToLower().Contains(searchValue) ||
                e.Email.ToLower().Contains(searchValue) ||
                e.Department.Name.ToLower().Contains(searchValue));
        }

        // Records after filtering
        var filteredRecords = await query.CountAsync();

        // Apply sorting
        if (request.Sorting.Any())
        {
            var sort = request.Sorting.First();
            var column = request.Columns[sort.ColumnIndex];

            query = column.Data switch
            {
                "firstName" => sort.IsAscending
                    ? query.OrderBy(e => e.FirstName)
                    : query.OrderByDescending(e => e.FirstName),
                "lastName" => sort.IsAscending
                    ? query.OrderBy(e => e.LastName)
                    : query.OrderByDescending(e => e.LastName),
                "email" => sort.IsAscending
                    ? query.OrderBy(e => e.Email)
                    : query.OrderByDescending(e => e.Email),
                "department" => sort.IsAscending
                    ? query.OrderBy(e => e.Department.Name)
                    : query.OrderByDescending(e => e.Department.Name),
                _ => query.OrderBy(e => e.LastName)
            };
        }

        // Apply pagination and project to DTO
        var data = await query
            .Skip(request.Start)
            .Take(request.Length)
            .Select(e => new EmployeeDto
            {
                Id = e.Id,
                FirstName = e.FirstName,
                LastName = e.LastName,
                Email = e.Email,
                Department = e.Department.Name,
                HireDate = e.HireDate
            })
            .ToListAsync();

        // Create response
        var response = request.CreateResponse(totalRecords, filteredRecords, data);

        return new JsonResult(response);
    }
}

public class EmployeeDto
{
    public int Id { get; set; }
    public string FirstName { get; set; } = string.Empty;
    public string LastName { get; set; } = string.Empty;
    public string Email { get; set; } = string.Empty;
    public string Department { get; set; } = string.Empty;
    public DateTime HireDate { get; set; }
}

Error Handling

Implement proper error handling for production scenarios:

Pages/Users.cshtml.cs

public async Task<IActionResult> OnPostDataAsync([FromBody] IDataTableRequest request)
{
    try
    {
        if (request == null)
        {
            return BadRequest(new { error = "Invalid request" });
        }

        var query = _context.Users.AsQueryable();

        // Apply filters, sorting, pagination...

        var totalRecords = await _context.Users.CountAsync();
        var filteredRecords = await query.CountAsync();
        var data = await query
            .Skip(request.Start)
            .Take(request.Length)
            .ToListAsync();

        return new JsonResult(request.CreateResponse(totalRecords, filteredRecords, data));
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Error processing DataTables request");

        // Return error response that DataTables can handle
        return new JsonResult(new
        {
            draw = request?.Draw ?? 0,
            recordsTotal = 0,
            recordsFiltered = 0,
            data = Array.Empty<object>(),
            error = "An error occurred while processing your request"
        })
        {
            StatusCode = 500
        };
    }
}

Client-side error handling:

$('#myTable').DataTable({
    serverSide: true,
    ajax: {
        url: '@Url.Page("MyPage", "Data")',
        type: 'POST',
        contentType: 'application/json',
        data: function (d) {
            return JSON.stringify(d);
        },
        error: function (xhr, error, code) {
            console.error('DataTables error:', error, code);
            alert('Failed to load data. Please try again.');
        }
    }
});

API Reference

IDataTableRequest Interface

The request object automatically bound from the DataTables Ajax payload:

Property Type Description
Draw int Draw counter for async ordering
Start int Paging: first record index (0-based)
Length int Paging: number of records to return
Search ISearch Global search parameters
Columns IReadOnlyList<IColumn> Column definitions and search
Sorting IReadOnlyList<ISort> Sorting parameters
AdditionalParameters IDictionary<string, object> Custom parameters (if configured)

DataTableOptions Methods

Method Description
.SetDefaultPageLength(int) Set default records per page (default: 10)
.EnableDrawValidation() Enable draw counter validation (recommended)
.DisableDrawValidation() Disable draw validation
.UseCamelCase() Use camelCase for DataTables 1.10+
.UseHungarianNotation() Use Hungarian notation for legacy DataTables

Extension Methods

// Create a DataTables response
IDataTableResponse response = request.CreateResponse(
    totalRecords: 100,
    filteredRecords: 25,
    data: myDataList
);

// With additional parameters
IDataTableResponse response = request.CreateResponse(
    totalRecords: 100,
    filteredRecords: 25,
    data: myDataList,
    additionalParameters: new Dictionary<string, object>
    {
        ["summary"] = new { totalSales = 50000.00m }
    }
);

Performance Tips

1. Use Projections with EF Core

Always project to DTOs to avoid loading unnecessary data:

// ❌ Bad: Loads entire entity
var data = await query.Skip(request.Start).Take(request.Length).ToListAsync();

// ✅ Good: Projects only needed columns
var data = await query
    .Select(e => new { e.Id, e.Name, e.Email })
    .Skip(request.Start)
    .Take(request.Length)
    .ToListAsync();

2. Add Database Indexes

Index columns used for searching and sorting:

modelBuilder.Entity<Employee>()
    .HasIndex(e => e.LastName);

modelBuilder.Entity<Employee>()
    .HasIndex(e => new { e.LastName, e.FirstName });

3. Implement Caching for Static Data

For data that rarely changes:

private readonly IMemoryCache _cache;

public async Task<IActionResult> OnPostDataAsync([FromBody] IDataTableRequest request)
{
    var cacheKey = "departments";

    if (!_cache.TryGetValue(cacheKey, out List<Department> departments))
    {
        departments = await _context.Departments.ToListAsync();
        _cache.Set(cacheKey, departments, TimeSpan.FromMinutes(30));
    }

    // Process cached data...
}

4. Use AsNoTracking() for Read-Only Queries

var query = _context.Employees
    .AsNoTracking() // Faster for read-only scenarios
    .Where(e => e.IsActive);

5. Optimize Column Count

Only include columns you need on the client:

// ❌ Bad: Loading many unused columns
columns:
[
    { data: 'id' },
    { data: 'fullEntity' }  // Entire object
]

// ✅ Good: Only necessary columns
columns:
[
    { data: 'id' },
    { data: 'name' },
    { data: 'email' }
]

Troubleshooting

DataTables shows "No data available"

Possible causes:

  1. Incorrect AJAX URL: Verify the @Url.Page() or endpoint is correct
  2. HTTP method mismatch: Ensure you're using POST on both client and server
  3. Model binding failure: Check browser console for 400/500 errors

Solution:

// Add error handler to see what's wrong
ajax: {
    url: '@Url.Page("MyPage", "Data")',
    type: 'POST',
    contentType: 'application/json',
    error: function (xhr, error, code) {
        console.error('Ajax error:', xhr.responseText);
    },
    data: function (d) {
        return JSON.stringify(d);
    }
}

"Uncaught TypeError: Cannot read property 'length'"

Cause: Response format doesn't match DataTables expectations

Solution: Ensure your response includes recordsTotal, recordsFiltered, and data:

// ✅ Correct: Using CreateResponse
return new JsonResult(request.CreateResponse(total, filtered, data));

// ❌ Wrong: Manual object
return new JsonResult(new { myData = data }); // Missing required properties

Draw validation errors

Cause: Draw parameter not being sent or validated incorrectly

Solutions:

  1. Disable draw validation (not recommended for production):
builder.Services.AddDataTables(new DataTableOptions().DisableDrawValidation());
  1. Ensure draw is being sent:
data: function (d) {
    console.log('Draw:', d.draw); // Should increment each request
    return JSON.stringify(d);
}

415 Unsupported Media Type

Cause: Missing contentType: 'application/json' in Ajax settings

Solution:

ajax: {
    url: '@Url.Page("MyPage", "Data")',
    type: 'POST',
    contentType: 'application/json', // ← Required!
    data: function (d) {
        return JSON.stringify(d);
    }
}

Columns not mapping correctly

Cause: Mismatch between property names and data attribute

Solutions:

  1. Use camelCase on both sides:
// Server (enable camelCase)
builder.Services.AddDataTables(new DataTableOptions().UseCamelCase());
// Client
columns:
[
    { data: 'firstName' }, // Matches FirstName property { data: 'lastName' }
]
  1. Or use exact property names:
columns:
[
    { data: 'FirstName' }, // Exact match
    { data: 'LastName' }
]

FAQ

Q: Can I use this with MVC controllers?

A: Yes! The library works with MVC controllers as well:

[HttpPost]
public async Task<IActionResult> GetData([FromBody] IDataTableRequest request)
{
    // Same logic as Razor Pages
    var data = await _service.GetDataAsync(request);
    return Json(request.CreateResponse(totalRecords, filteredRecords, data));
}

Q: Does this support DataTables 2.0?

A: DataTables 2.0 maintains backward compatibility with the 1.10 protocol. Use .UseCamelCase() for best compatibility.

Q: Can I use GET instead of POST?

A: While possible, POST is recommended for server-side processing due to payload size. If you must use GET:

public IActionResult OnGetData([FromQuery] IDataTableRequest request)
{
    // Handler logic
}
ajax: {
    url: '@Url.Page("MyPage", "Data")',
    type: 'GET', // No contentType or JSON.stringify needed
}

Q: How do I implement server-side export (Excel, PDF)?

A: DataTables export runs client-side by default. For server-side export:

public IActionResult OnGetExport()
{
    var allData = _context.Employees.ToList(); // Use a library like ClosedXML, EPPlus, or iTextSharp
    var excelBytes = GenerateExcel(allData);
    return File(excelBytes, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "employees.xlsx");
}

License

This project is licensed under the MIT License - see the LICENSE file for details.


Resources

Product Compatible and additional computed target framework versions.
.NET net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
26.2.10.1 89 2/10/2026