DataTablesDotAspNetCore 26.2.10.1
dotnet add package DataTablesDotAspNetCore --version 26.2.10.1
NuGet\Install-Package DataTablesDotAspNetCore -Version 26.2.10.1
<PackageReference Include="DataTablesDotAspNetCore" Version="26.2.10.1" />
<PackageVersion Include="DataTablesDotAspNetCore" Version="26.2.10.1" />
<PackageReference Include="DataTablesDotAspNetCore" />
paket add DataTablesDotAspNetCore --version 26.2.10.1
#r "nuget: DataTablesDotAspNetCore, 26.2.10.1"
#:package DataTablesDotAspNetCore@26.2.10.1
#addin nuget:?package=DataTablesDotAspNetCore&version=26.2.10.1
#tool nuget:?package=DataTablesDotAspNetCore&version=26.2.10.1
DataTables.AspNetCore
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
- Installation
- Quick Start
- Configuration
- Examples
- API Reference
- Performance Tips
- Troubleshooting
- FAQ
- License
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
Razor Pages (Recommended)
See Quick Start above for a complete Razor Pages example.
Column-Specific Search
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:
- Incorrect AJAX URL: Verify the
@Url.Page()or endpoint is correct - HTTP method mismatch: Ensure you're using
POSTon both client and server - 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:
- Disable draw validation (not recommended for production):
builder.Services.AddDataTables(new DataTableOptions().DisableDrawValidation());
- 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:
- Use camelCase on both sides:
// Server (enable camelCase)
builder.Services.AddDataTables(new DataTableOptions().UseCamelCase());
// Client
columns:
[
{ data: 'firstName' }, // Matches FirstName property { data: 'lastName' }
]
- 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 | Versions 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. |
-
net10.0
- Microsoft.AspNetCore.Mvc (>= 2.3.9)
- Microsoft.Extensions.Http (>= 10.0.3)
-
net9.0
- Microsoft.AspNetCore.Mvc (>= 2.3.9)
- Microsoft.Extensions.Http (>= 9.0.13)
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 |