TableStack.Endpoints 1.0.9

dotnet add package TableStack.Endpoints --version 1.0.9
                    
NuGet\Install-Package TableStack.Endpoints -Version 1.0.9
                    
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="TableStack.Endpoints" Version="1.0.9" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="TableStack.Endpoints" Version="1.0.9" />
                    
Directory.Packages.props
<PackageReference Include="TableStack.Endpoints" />
                    
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 TableStack.Endpoints --version 1.0.9
                    
#r "nuget: TableStack.Endpoints, 1.0.9"
                    
#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 TableStack.Endpoints@1.0.9
                    
#: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=TableStack.Endpoints&version=1.0.9
                    
Install as a Cake Addin
#tool nuget:?package=TableStack.Endpoints&version=1.0.9
                    
Install as a Cake Tool

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 FilterType MultiSelect 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 < 1000 come first (by value), properties with no Order follow in declaration order, then properties with Order >= 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 metadata
  • GET {baseUrl}?PageNumber=...&PageSize=...&... — to load the current page of data
  • GET {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 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. 
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
1.0.9 116 2/12/2026
1.0.8 110 2/11/2026