inandu-grid's serverSide mode and the
Inandu.Grid.Extensions
NuGet package are designed as a matched pair: the grid emits plain events on every sort, page
or filter change; the API turns the resulting request into one paged EF Core query. No
client-side data-fetching library, no server-side query builder to write by hand.
dotnet add package Inandu.Grid.Extensions.EntityFrameworkCore
using Inandu.Grid.Extensions.EntityFrameworkCore;
app.MapGet("/api/products", (HttpRequest request, AppDb db, CancellationToken ct) =>
db.Products.AsNoTracking().ToInanduGridAsync(request.QueryString.Value, options =>
{
options.DefaultPageSize = 25;
options.MaxPageSize = 200;
options.SearchableFields = new() { nameof(Product.Name), nameof(Product.Sku) };
}, ct: ct));
ToInanduGridAsync reads page/pageSize, sort,
q and any {field}_{operator} column filters straight off the query
string, runs CountAsync + ToListAsync, and returns
{ data, total, page, pageSize, pageCount } — exactly the shape the grid needs.
Only one page of rows is ever read from the database; sorting and filtering translate to SQL,
not an in-memory scan.
<inandu-grid
[data]="pageRows()"
serverSide="true"
[totalItems]="total()"
[loading]="loading()"
[error]="error()"
[paging]="{ pageSize: 25 }"
(sortChange)="onSort($event)"
(pageChange)="onPage($event)"
(filterChange)="onFilter($event)"
filter="true">
<inandu-column field="name" title="Name" sortable="true" filter="yes" />
<inandu-column field="sku" title="SKU" filter="yes" />
<inandu-column field="price" title="Price" type="number" sortable="true" filter="yes" />
</inandu-grid>
Once serverSide is on, the grid trusts [data] to already be the
current page and stops sorting/filtering/paging locally. It fires (sortChange) /
(pageChange) / (filterChange) on every relevant change —
including once on init — so a single handler chain drives the first fetch too.
The free grid's events are plain objects — InanduGridSortCriterion[],
{ page, pageSize }, { query, columnFilters } — not HTTP params. This
service keeps the current request state in signals and rebuilds the query string
Inandu.Grid.Extensions expects
(page, pageSize, sort=-field, q=,
field_gte= / field_lte= / field_contains=) on every
change:
@Injectable({ providedIn: 'root' })
export class ProductGridService {
private readonly http = inject(HttpClient);
readonly pageRows = signal<Product[]>([]);
readonly total = signal(0);
readonly loading = signal(false);
readonly error = signal('');
private page = 1;
private pageSize = 25;
private sort: InanduGridSortCriterion[] = [];
private query = '';
private columnFilters: Record<string, InanduGridColumnFilterValue> = {};
onSort(criteria: InanduGridSortCriterion[]) { this.sort = criteria; this.page = 1; this.fetch(); }
onPage(p: { page: number; pageSize: number }) { this.page = p.page; this.pageSize = p.pageSize; this.fetch(); }
onFilter(f: { query: string; columnFilters: Record<string, InanduGridColumnFilterValue> }) {
this.query = f.query; this.columnFilters = f.columnFilters; this.page = 1; this.fetch();
}
private fetch() {
this.loading.set(true);
this.error.set('');
const params = new URLSearchParams({ page: String(this.page), pageSize: String(this.pageSize) });
if (this.query) params.set('q', this.query);
this.sort.forEach(s => params.append('sort', s.direction === 'desc' ? `-${s.field}` : s.field));
for (const [field, value] of Object.entries(this.columnFilters)) {
if (value.text) params.set(`${field}_contains`, value.text);
if (value.min != null) params.set(`${field}_gte`, String(value.min));
if (value.max != null) params.set(`${field}_lte`, String(value.max));
if (value.from) params.set(`${field}_gte`, value.from);
if (value.to) params.set(`${field}_lte`, value.to);
if (value.bool) params.set(`${field}_eq`, value.bool);
}
this.http.get<InanduGridResult<Product>>(`/api/products?${params}`).subscribe({
next: res => { this.pageRows.set(res.data); this.total.set(res.total); this.loading.set(false); },
error: () => { this.error.set('Could not load products.'); this.loading.set(false); },
});
}
}
@inandu-solutions/grid-pro's
createInanduGridDataSource/createInanduGridServerRowModel do for you —
if you'd rather not maintain this service by hand across every grid in your app, that's what
they're for. The free grid ships the events; the wiring above is the whole cost of staying on
the MIT tier.
SearchableFields (or every string property if left unset) — set it explicitly for predictable results and to avoid scanning unindexed columns.ToInanduGridAsync(selector, …) overload — the projection becomes part of the SQL, so only the columns you expose are read.ToInanduGridGroupedAsync and the aggregate=sum:price param — see the extensions repo's grouping docs if you need grouped server-side rows rather than the grid's client-side groupable.rowSave/rowCreate/rowDelete handlers — inandu-grid never talks to your backend directly for writes, only for the read side shown above.Full server-side reference: manual §11. Full request contract: request-contract.md. Questions: open an issue.