Inandu.Grid.Extensions.AspNetCore 0.4.0

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

Inandu.Grid.Extensions

One extension method turns an inandu-grid serverSide request into a single paged SQL query. No hand-rolled dynamic sort/filter builder, no OData, no GraphQL layer to stand up — ToInanduGrid() / ToInanduGridAsync() read the grid's own wire contract (multi-column sort, free-text search, per-column filters, grouping, aggregates, keyset paging) and do the query work for you, over plain IEnumerable<T> or EF Core IQueryable<T>.

NuGet downloads CI license .NET grid demo

Try it in 30 seconds

dotnet add package Inandu.Grid.Extensions
using Inandu.Grid.Extensions;

app.MapGet("/api/products", (HttpRequest request) =>
    _products.ToInanduGrid(request.QueryString.Value)); // { data, total, page, pageSize, pageCount }

That single call reads page/sort/q/column filters straight off the query string and returns just the page the grid asked for — see Quick start below for the EF Core (async) and ASP.NET Core binding variants.

Who is this for?

  • .NET / ASP.NET Core APIs backing an <inandu-grid serverSide> (or any client speaking the same query-param contract) that don't want to hand-write dynamic LINQ sort/filter/paging for every entity.
  • EF Core apps that want the database to do the work — sort/filter/paging translate to SQL, so only one page is ever fetched, not the whole table scanned in memory.
  • Teams that want the query surface locked down — per-column allow-lists, a query cost guard, and a ThrowOnUnknownField mode for public endpoints.

When not to use it: if you're not using inandu-grid's serverSide wire contract (or something compatible with it) at all — this isn't a general-purpose OData or GraphQL server, see Not included below.

Not included

Deliberately scoped to what the grid's wire contract needs, not a general-purpose query API:

  • No OData or GraphQL. The query-string contract is inandu-grid's own (documented in docs/request-contract.md), not a spec-compliant OData/GraphQL server.
  • No built-in caching layer. Every request runs a real query; add your own caching (response caching, a distributed cache) in front of it if you need one.
  • .NET only. There's no equivalent for other backend stacks — the contract itself is plain query-string params, so a Node/Python/Go backend can implement it by hand against the same request contract docs, just not with this library.

Packages

Package For
Inandu.Grid.Extensions the core ToInanduGrid() — zero dependencies
Inandu.Grid.Extensions.EntityFrameworkCore ToInanduGridAsync() — CountAsync + ToListAsync
Inandu.Grid.Extensions.AspNetCore [FromInanduGrid] model binder + minimal-API binding

Multi-targets net8.0 and net10.0.


Install

dotnet add package Inandu.Grid.Extensions

Quick start

ASP.NET Core (in-memory or any IEnumerable<T>)

using Inandu.Grid.Extensions;

app.MapGet("/api/products", (HttpRequest request) =>
{
    InanduGridResult<Product> page = _products.ToInanduGrid(request.QueryString.Value, options =>
    {
        options.DefaultPageSize   = 25;
        options.MaxPageSize       = 200;
        options.SearchableFields  = new() { nameof(Product.Name), nameof(Product.Sku) };
    });

    return Results.Ok(page); // { data: [...], total: 1234, page: 1, pageSize: 25, pageCount: 50 }
});

MVC controller

[HttpGet]
public ActionResult<InanduGridResult<Product>> Get()
    => _products.ToInanduGrid(Request.QueryString.Value);

EF Core (async)

dotnet add package Inandu.Grid.Extensions.EntityFrameworkCore
using Inandu.Grid.Extensions.EntityFrameworkCore;

app.MapGet("/api/orders", (HttpRequest req, AppDb db, CancellationToken ct) =>
    db.Orders.AsNoTracking().ToInanduGridAsync(req.QueryString.Value, ct: ct));

ToInanduGridAsync runs CountAsync + ToListAsync under the hood. There are projected (ToInanduGridAsync(selector, …)) and grouped (ToInanduGridGroupedAsync) overloads too. Without the package, ApplyInanduGridQuery gives you the composed FilteredQuery / PagedQuery to await yourself.

[FromInanduGrid] — ASP.NET Core binding

dotnet add package Inandu.Grid.Extensions.AspNetCore
using Inandu.Grid.Extensions.AspNetCore;

// MVC controller
[HttpGet]
public ActionResult<InanduGridResult<Product>> Get([FromInanduGrid] InanduGridRequest request)
    => _products.ToInanduGrid(InanduGridOptions.For(request));

// …or bind every InanduGridRequest parameter without the attribute
builder.Services.AddControllers().AddInanduGridModelBinding();

// minimal API
app.MapGet("/api/products", async (HttpRequest req) =>
    _products.ToInanduGrid(InanduGridOptions.For(await InanduGridBinding.FromHttpRequestAsync(req))));

InanduGridBinding reads the query string, or a JSON body for a POST.

Bind the request yourself (JSON body, gRPC, tests…)

InanduGridRequest is a plain DTO. @inandu-solutions/grid-pro POSTs a body shaped { sort: [{ field, direction }], page, pageSize, query, columnFilters } — deserialize it straight into InanduGridRequest (a bundled System.Text.Json converter accepts "asc" / "desc" and the "-field" token form):

app.MapPost("/api/products/query", (InanduGridRequest request) =>
    _products.ToInanduGrid(request, o => o.MaxPageSize = 200));

Return a DTO, not the entity

ToInanduGrid<TSource, TResult>(selector, …) sorts and filters on the entity, then projects the page — over EF Core the projection is part of the SQL, so only the DTO's columns are read:

db.Orders.ToInanduGrid(
    o => new OrderDto(o.Id, o.Reference, o.Customer.Name, o.Total),
    Request.QueryString.Value,
    cfg => cfg.FieldMap = new() { ["customer"] = "Customer.Name" }); // filter/sort by Customer.Name

Advanced filter (nested AND / OR)

@inandu-solutions/grid-pro's query builder can serialise its tree with advancedQueryToRestParams into a filter=<json> param. This library parses it (InanduGridRequest.Filter / .AdvancedFilter) and applies it as an extra AND — operators include between, notContains, isTrue/isFalse, isEmpty/isNotEmpty. See docs/advanced-filter.md.

Server-side grouping

ToInanduGridGrouped() — groupBy=region,category returns the level's { key, count } groups; groupKeys=EMEA drills in (the next level, or the group's rows past the last groupBy). Filters, free-text and the advanced filter all still apply. See docs/grouping.md.

var result = db.Sales.ToInanduGridGrouped(InanduGridOptions.FromQueryString(qs));
return result.IsLeaf ? Results.Ok(result.Rows) : Results.Ok(result.Groups);

Aggregate totals

aggregate=sum:amount,avg:rating,count:*,min:createdOn,max:price → computed over the filtered set (not the page) into InanduGridResult<T>.Aggregations ({ "sum:amount": 91234.5, … }) — for the grid's totals row.

Keyset (cursor) pagination

For deep pages, after=<cursor> seeks past the last row instead of Skip-ing over everything:

var page = db.Orders.ToInanduGrid("?pageSize=50&sort=-createdOn,id", o => o.EnableKeyset = true);
// page.NextCursor -> next request adds &after=<NextCursor>;  o.IncludeTotal = false skips the COUNT

The sort columns are the cursor — include a unique tie-breaker (,id). A missing / stale cursor falls back to offset paging. See docs/keyset-pagination.md.

Per-column configuration

o.Column("customer").Path("Customer.Name").Searchable();
o.Column("price").Filterable(FilterOperator.GreaterThanOrEqual, FilterOperator.LessThanOrEqual);
o.Column("internalNote").NotFilterable().Sortable(false);

A disallowed operator / sort / filter is dropped — or, with o.ThrowOnUnknownField = true, throws InanduGridRequestException (its Errors map is ValidationProblemDetails-shaped). See docs/column-config.md.

Query cost guard

o.Limits caps how large a request can be (conditions, sort columns, advanced-filter depth / nodes, in-list length, group levels, aggregations). o.OnLimitExceeded is Trim (default) or Reject (throw). Defaults are generous; tighten for a public endpoint.

Minimal-API one-liners (Inandu.Grid.Extensions.AspNetCore)

app.MapInanduGrid("/api/products", products);
app.MapInanduGridGrouped("/api/products/groups", ctx => ctx.RequestServices.GetRequiredService<AppDb>().Products);
app.MapInanduGridDistinct("/api/products/distinct/{field}", products);

Using Swagger/OpenAPI? Minimal-API endpoints that read the query string by hand (which is what all of the above do) are invisible to Swashbuckle by default — page, sort, q and friends show up as undocumented. See docs/openapi-swagger.md for the ~50-line filter the playground uses to document them properly.

The Angular side

Wire the grid's serverSide outputs to a request and bind the response:

<inandu-grid serverSide
  [data]="rows()" [totalItems]="total()" [loading]="loading()"
  (sortChange)="onSort($event)" (pageChange)="onPage($event)" (filterChange)="onFilter($event)">
  …
</inandu-grid>

Either hand-build the query string, or let @inandu-solutions/grid-pro's createInanduGridDataSource do it — the params it emits are exactly what this package parses. See the playground for a full working example, and the live grid demo for the front end this serves.

Request contract

See docs/request-contract.md for the full parameter list. In short:

Param Meaning
page, pageSize 1-based page.
offset, limit Row window (block model); wins over page/pageSize.
sort Comma-separated; - prefix = descending. Repeatable.
q Free-text search across SearchableFields.
{field}_{op} A filter, op ∈ eq neq contains startsWith endsWith gt gte lt lte in.
filter JSON advanced-filter tree (nested AND / OR).
groupBy, groupKeys Server-side grouping + drill-down (ToInanduGridGrouped).
aggregate sum:f,avg:f,count:*,min:f,max:f totals over the filtered set.
after Keyset cursor — seek past this row instead of Skip.

Configuration

See docs/options.md. Highlights: DefaultPageSize, MaxPageSize, SearchableFields, StringComparison, FieldMap (grid field → CLR path), ThrowOnUnknownField, Culture.

Build, test & benchmark

dotnet build
dotnet test                                                   # 165 tests, net8.0 + net10.0
dotnet run -c Release --project benchmarks/Inandu.Grid.Extensions.Benchmarks

The libraries multi-target net8.0;net10.0 when built with the .NET 10 SDK; with only the .NET 8 SDK they build net8.0 alone (see the condition in each .csproj). Produce release packages with the .NET 10 SDK so the .nupkgs ship both. See docs/publishing.md.

Benchmarks

BenchmarkDotNet micro-benchmarks over 100,000 in-memory rows, one benchmark per query shape (plain page, filter, sort, filter+sort+page, free-text, advanced filter, grouping, projection). Reference numbers (i5-7200U, .NET 8, short job): filter+sort+page ≈ 10 ms, advanced-filter ≈ 4 ms, top-level grouping ≈ 12 ms — treat these as ballpark, not a guarantee, and run it on your own hardware for a number that matters to your deployment. Full methodology and the complete benchmark list: benchmarks/README.md. A workflow_dispatch-triggered Benchmarks workflow runs the same suite on GitHub's runners and uploads the results as a build artifact, for a reproducible (if not hardware-comparable) run anyone can trigger.

Contributing

Issues and PRs welcome on GitHub. Add tests for behaviour changes and keep the core library dependency-free — see CONTRIBUTING.md. Questions and usage help: see .github/SUPPORT.md — prefer Discussions over an issue. For security reports, see .github/SECURITY.md — don't open a public issue.

License

MIT © Inandu SAS — see LICENSE.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  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
0.4.0 87 10/14/2026
0.3.0 124 9/8/2026

See the CHANGELOG at the repository root.