TableStack.Endpoints
1.0.9
dotnet add package TableStack.Endpoints --version 1.0.9
NuGet\Install-Package TableStack.Endpoints -Version 1.0.9
<PackageReference Include="TableStack.Endpoints" Version="1.0.9" />
<PackageVersion Include="TableStack.Endpoints" Version="1.0.9" />
<PackageReference Include="TableStack.Endpoints" />
paket add TableStack.Endpoints --version 1.0.9
#r "nuget: TableStack.Endpoints, 1.0.9"
#:package TableStack.Endpoints@1.0.9
#addin nuget:?package=TableStack.Endpoints&version=1.0.9
#tool nuget:?package=TableStack.Endpoints&version=1.0.9
TableStack
TableStack is a package that provides core components for paging, sorting, filtering and searching IQueryable data. With the option to use source generation to automatically provide metadata for the endpoints, along with secondary source-generated endpoints for filtered columns, it helps you quickly and reliably produce fully featured data tables. Accompanying JavaScript and CSS (data-table.js and data-table.css) to consume these endpoints and render data tables in the browser are provided in a separate TableStack.Endpoints GitHub repository.
Short guide to paged endpoints with optional metadata and column-filter generation.
Setup
In your app startup (e.g. Program.cs) directly before app.Run():
app.MapGeneratedPagedFeatures();
Request class
Have your request inherit PagedRequest<T>:
using TableStack.Core.Pagination;
public class ExampleRequest : PagedRequest<ExampleRequest>
{
}
Response class
Your response class would then contain your response properties with the optional attrabutes to order and format:
using TableStack.Core.Attributes;
public class ExampleResponse
{
[Column(1, Label = "ID", Format = "#,##0")]
public required int Id { get; init; }
[Column(2, Label = "Name")]
[CellData("data-url", "/example/{Id}")]
public required string Name { get; init; }
[Column(Order = 3, Label = "Description")]
public required string? Description { get; init; }
[Column(1001, Label = "Created At", Format = "DD/MM/YYYY HH:mm")]
public required DateTime CreatedAt { get; init; }
[Column(1002, Label = "Created Date", Format = "DD/MM/YYYY")]
public required DateOnly CreatedAtDate { get; init; }
[Column(1003, Label = "Time of Day", Format = "HH:mm:ss")]
public required TimeSpan CreatedAtTimeOfDay { get; init; }
[Column(1000, Label = "Updated At", Format = "DD/MM/YYYY HH:mm")]
public required DateTime? UpdatedAt { get; init; }
}
Minimal API
Use ToPagedListAsync on an IQueryable, then map the result to your response. Add .WithGeneratedMetadata() to enable source-generated metadata and column-filter endpoints.
app.MapGet("examplegroup/example", async (
ExampleRequest request,
[FromServices] IExampleService service,
CancellationToken cancellationToken) =>
{
var query = service.GetExamplesQuery();
var filterConfig = query.CreateFilter(cfg => cfg
.ForColumn(x => x.Id, FilterType.Range)
.ForColumn(x => x.Name, FilterType.MultiSelect)
.ForColumn(x => x.Description, FilterType.Contains)
.ForColumn(x => x.CreatedAt, FilterType.Range));
var pagedResult = await query.ToPagedListAsync(
request,
filterConfig,
searchExpression: dto => dto.Name.Contains(request.SearchTerm ?? ""),
cancellationToken);
var response = pagedResult.Map(dto => new ExampleResponse
{
Id = dto.Id,
Name = dto.Name,
Description = dto.Description,
CreatedAt = dto.CreatedAt,
UpdatedAt = dto.UpdatedAt
});
return Results.Ok(response);
})
.WithName("GetExamples")
.WithGeneratedMetadata()
.WithOpenApi();
- CreateFilter – Configures which columns are filterable and how (Range, MultiSelect, Contains, Equals, etc.). MultiSelect and Equals get their own generated endpoints when you use
.WithGeneratedMetadata(). - searchExpression – Defines how global search applies to the query (e.g.
dto => dto.Name.Contains(request.SearchTerm ?? "")).
Sorting
Paged requests support server-side ordering via query parameters:
| Parameter | Type | Description |
|---|---|---|
SortBy |
string | Property name to sort by (e.g. Name, CreatedAt). Must match a property on the query type. |
SortDescending |
bool | true for descending, false (default) for ascending. |
ToPagedListAsync applies sorting after filtering and before paging, using System.Linq.Dynamic.Core. If SortBy is omitted or invalid, no sort is applied. Example: ?SortBy=CreatedAt&SortDescending=true.
FastEndpoints
Same pattern: get an IQueryable, call CreateFilter, then ToPagedListAsync and Map to your response. To enable generated metadata endpoints, add the attribute to the endpoint class:
[GenerateMetadataEndpoints(EndpointType.FastEndpoints)]
public class GetProductEndpoint : Endpoint<GetProductRequest, PagedResult<GetProductResponse>>
{
public override void Configure()
{
Get("productgroup/products");
AllowAnonymous();
}
public override async Task HandleAsync(GetProductRequest req, CancellationToken ct)
{
var query = _service.GetExamplesQuery();
var filterConfig = query.CreateFilter(cfg => cfg
.ForColumn(x => x.Id, FilterType.Range)
.ForColumn(x => x.Name, FilterType.MultiSelect)
.ForColumn(x => x.Description, FilterType.Contains)
.ForColumn(x => x.CreatedAt, FilterType.Range));
var pagedResult = await query.ToPagedListAsync(
req,
filterConfig,
searchExpression: dto => dto.Name.Contains(req.SearchTerm ?? ""),
ct);
var response = pagedResult.Map(dto => new GetProductResponse
{
Id = dto.Id,
Name = dto.Name,
Description = dto.Description,
CreatedAt = dto.CreatedAt,
UpdatedAt = dto.UpdatedAt
});
await Send.OkAsync(response);
}
}
Without [GenerateMetadataEndpoints(EndpointType.FastEndpoints)] the paged endpoint still works; the attribute only turns on the generated metadata and column-filter endpoints.
Source-generated endpoints
When you use .WithGeneratedMetadata() or [GenerateMetadataEndpoints], the following endpoints are generated for each paged feature (relative to the route prefix, e.g. examplegroup/example).
Metadata — GET /{routePrefix}/metadata
Returns column definitions and row data attributes derived from your response type and [Column] / [CellData] / [RowData] attributes. The client uses this to build the table header, apply formats, and know which columns are filterable.
Response shape:
{
"columns": [
{
"name": "Id",
"dataType": "Int32",
"order": 0,
"label": "ID",
"format": "#,##0",
"cellDataAttributes": [],
"filterable": true,
"filterType": "Range",
"filterEndpoint": null
},
{
"name": "Name",
"label": "Name",
"filterable": true,
"filterType": "MultiSelect",
"filterEndpoint": "/examplegroup/example/columnfilter/Name"
}
],
"rowDataAttributes": [
{ "name": "data-id", "template": "{Id}" }
]
}
- filterEndpoint is set only for columns with
FilterTypeMultiSelect or Equals; the client calls this to load options for dropdowns.
Data — GET /{routePrefix}?PageNumber=...&PageSize=...&SortBy=...&SearchTerm=...&Filters[...]
Your existing paged endpoint. Query parameters include PageNumber, PageSize, SortBy, SortDescending, SearchTerm, and Filters[n].Column / Filters[n].Value (or MinValue/MaxValue for Range, Values[i] for MultiSelect/Equals). Response is your PagedResult<T> (items, totalCount, pageNumber, pageSize, totalPages, etc.).
Column filter — GET /{routePrefix}/columnfilter/{columnName}?searchTerm=...&pageNumber=1&pageSize=10
Generated only for columns configured with FilterType.MultiSelect or FilterType.Equals. Returns distinct values for that column (optionally filtered by searchTerm, paged). Used by the client to populate filter dropdowns.
Query parameters: searchTerm (optional), pageNumber, pageSize.
Response shape: Same as PagedResult<string> — items (array of strings), totalCount, pageNumber, pageSize, totalPages, hasNextPage, hasPreviousPage.
Response type attributes (metadata)
When you use .WithGeneratedMetadata(), the metadata endpoint is built from your response type. You can annotate it with these attributes to control column order, labels, formats, and HTML data attributes for the table.
[Column]
Put [Column] on each property that should appear as a column. Use the constructor for order and optional named arguments for label and format:
[Column(1, Label = "ID", Format = "#,##0")]
public required int Id { get; init; }
[Column(2, Label = "Name")]
public required string Name { get; init; }
// Order only (named): [Column(Order = 3, Label = "Description")]
// Late columns (Order >= 1000) appear after columns with no Order
[Column(1001, Label = "Created At", Format = "DD/MM/YYYY HH:mm")]
public required DateTime CreatedAt { get; init; }
[Column(1002, Label = "Created Date", Format = "DD/MM/YYYY")]
public required DateOnly CreatedAtDate { get; init; }
[Column(1003, Label = "Time of Day", Format = "HH:mm:ss")]
public required TimeSpan CreatedAtTimeOfDay { get; init; }
[Column(1000, Label = "Updated At", Format = "DD/MM/YYYY HH:mm")]
public required DateTime? UpdatedAt { get; init; }
- Order – Display order. Properties with
Order < 1000come first (by value), properties with noOrderfollow in declaration order, then properties withOrder >= 1000(by value). - Label – Column header text. Defaults to the property name.
- Format – Display pattern only (not for filtering). Examples:
DD/MM/YYYY,DD/MM/YYYY HH:mm,HH:mm:ss,#,##0,#,##0.00,$#,##0.00.
[CellData]
Add HTML data attributes to the cell for that property. Use {PropertyName} in the template; it is replaced with the property value. Multiple attributes per property are allowed.
[Column(2, Label = "Name")]
[CellData("data-url", "/example/{Id}")]
public required string Name { get; init; }
Renders as e.g. <td data-url="/example/42">...</td>. Useful for links, tooltips, or client-side behaviour.
[RowData]
Add HTML data attributes to the row (<tr>). Apply to the response class; use {PropertyName} in the template. Multiple attributes are allowed.
[RowData("data-id", "{Id}")]
[RowData("data-entity", "example")]
public class ExampleResponse
{
// ...
}
Renders as e.g. <tr data-id="42" data-entity="example">...</tr>. Useful for row selection, navigation, or scripting.
Client-side: data-table.js and data-table.css
The JavaScript and CSS that consume the source-generated endpoints and render a full data table (toolbar, search, column filters, sorting, paging) are provided in a separate GitHub repository: TableStack.Endpoints. Clone or install from there, or reference the files via your preferred CDN.
Including the files
Reference the script and stylesheet in your page (use the paths from the GitHub repo or your own copy):
<link rel="stylesheet" href="~/data-table.css" />
<script src="~/data-table.js"></script>
Using the component
Point the table at your paged endpoint’s base URL (the same route prefix as the data endpoint, without trailing slash). The client will call:
GET {baseUrl}/metadata— to load column definitions and filter metadataGET {baseUrl}?PageNumber=...&PageSize=...&...— to load the current page of dataGET {baseUrl}/columnfilter/{columnName}?...— for MultiSelect/Equals columns when opening the filter popup
Single source:
const table = new DataTable({
container: '#my-table',
baseUrl: 'https://localhost:5001/examplegroup/example',
title: 'Examples'
});
await table.init();
Multiple base URLs (e.g. dropdown to switch tables):
const table = new DataTable({
container: '#my-table',
baseUrlOptions: [
{ title: 'Examples', url: 'https://localhost:5001/examplegroup/example' },
{ title: 'Products', url: 'https://localhost:5001/productgroup/products' }
]
});
await table.init();
Optional: pageSize, pageSizeOptions, sortBy, sortDescending. The table uses the metadata response for column order, labels, formats, filter types, and filter endpoints; no extra configuration is needed when the backend uses .WithGeneratedMetadata().
| 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 was computed. 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. |
-
.NETStandard 2.0
- No dependencies.
-
net9.0
- Microsoft.EntityFrameworkCore (>= 9.0.13)
- System.Linq.Dynamic.Core (>= 1.7.1)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.