Blazor QuickGrid Tutorial: Sorting, Filtering and Paging (.NET 10)

Say the sales manager at a Dallas furniture store wants this year’s 200 orders on one screen, sorted by total, searchable by customer and ten rows a page. In Web Forms you’d drag in a GridView. Blazor doesn’t have one.

The built-in answer is Blazor QuickGrid, Microsoft’s free grid component. Give it an IQueryable, mark columns Sortable, add a Paginator and a bound search box, and you’re done. Here’s what we’re building on .NET 10:

Blazor QuickGrid result: the 2026 Orders page filtered to CA, sorted by Total descending, two rows ticked and the summary 31 orders, 2 selected ($4,652.41)
The finished grid: CA filter, Total sorted high to low, two orders picked (headless Edge, real run)

Before You Start with Blazor QuickGrid

You need the .NET 10 SDK (I used 10.0.402; check your .NET version first) and any editor. If Blazor itself is new to you, my Blazor tutorial for beginners covers pages and components.

Our sample data is 200 made-up orders from 2026. Each order has these fields:

FieldTypeNotes
IdintOrder number, 1001 to 1200
CustomerstringUS names such as Tyler Bennett
StatestringCA, CO, FL, GA, IL, NY, TX or WA
OrderDateDateOnlyJan 1 to Oct 27, 2026
Totaldecimal$25 to $2,425, shown in USD
StatusstringPending, Shipped, Delivered or Cancelled

Step 1: Create the Blazor App and Add the QuickGrid Package

QuickGrid ships as its own NuGet package. Create a Blazor Web App and add it (10.0.14 was the current version on November 1, 2026):

dotnet new blazor -o QuickGridDemo
cd QuickGridDemo
dotnet add package Microsoft.AspNetCore.Components.QuickGrid --version 10.0.14

I deleted the two appsettings files (this demo doesn’t need them) and trimmed Properties/launchSettings.json to one http profile on port 5737:

{
  "$schema": "https://json.schemastore.org/launchsettings.json",
  "profiles": {
    "http": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": false,
      "applicationUrl": "http://localhost:5737",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

My PC isn’t set to US regional settings, so I pinned the app to en-US at the top of Program.cs. Without it, Format="C" would print the local currency:

using System.Globalization;
using QuickGridDemo.Components;

// Show prices and dates the US way, whatever the server's regional settings are
CultureInfo.DefaultThreadCurrentCulture = new CultureInfo("en-US");
CultureInfo.DefaultThreadCurrentUICulture = new CultureInfo("en-US");

The project targets net10.0, the .NET 10 LTS release. Its full project file is in the EF Core section, after all three packages are added.

Step 2: Add the Order Model and 200 Sample Orders

Create Models/Order.cs. It’s a class, not a record, because Step 6 changes Status in place:

namespace QuickGridDemo.Models;

public class Order
{
    public int Id { get; set; }
    public string Customer { get; set; } = "";
    public string State { get; set; } = "";
    public DateOnly OrderDate { get; set; }
    public decimal Total { get; set; }
    public string Status { get; set; } = "Pending";
}

Then Services/OrderData.cs builds the orders. A fixed Random seed returns the same List of orders on every run, so your numbers will match mine:

using QuickGridDemo.Models;

namespace QuickGridDemo.Services;

public static class OrderData
{
    private static readonly string[] First = ["Jenna", "Tyler", "Maria", "Derek", "Ashley", "Marcus", "Kayla", "Brandon", "Lauren", "Caleb"];
    private static readonly string[] Last = ["Morales", "Brooks", "Bennett", "Hayes", "Foster", "Coleman", "Reed", "Parker", "Sullivan", "Grant"];
    private static readonly string[] States = ["CA", "TX", "NY", "FL", "WA", "IL", "CO", "GA"];
    private static readonly string[] Statuses = ["Pending", "Shipped", "Delivered", "Cancelled"];

    // Same seed = the same orders on every run
    public static List<Order> Create(int count, int seed = 2026)
    {
        var rng = new Random(seed);
        var start = new DateOnly(2026, 1, 1);
        return Enumerable.Range(1, count).Select(i => new Order
        {
            Id = 1000 + i,
            Customer = $"{First[rng.Next(First.Length)]} {Last[rng.Next(Last.Length)]}",
            State = States[rng.Next(States.Length)],
            OrderDate = start.AddDays(rng.Next(0, 300)),
            Total = Math.Round((decimal)(rng.NextDouble() * 2400 + 25), 2),
            Status = Statuses[rng.Next(Statuses.Length)]
        }).ToList();
    }
}

Step 3: Render Your First QuickGrid with PropertyColumn

Create Components/Pages/Orders.razor. Items takes an IQueryable<Order>, and each PropertyColumn shows one property:

@page "/orders"
@rendermode InteractiveServer
@using Microsoft.AspNetCore.Components.QuickGrid
@using QuickGridDemo.Models
@using QuickGridDemo.Services

<PageTitle>Orders</PageTitle>

<h1>2026 Orders</h1>

<QuickGrid Items="orders">
    <PropertyColumn Property="@(o => o.Id)" />
    <PropertyColumn Property="@(o => o.Customer)" />
    <PropertyColumn Property="@(o => o.State)" />
    <PropertyColumn Property="@(o => o.OrderDate)" />
    <PropertyColumn Property="@(o => o.Total)" />
    <PropertyColumn Property="@(o => o.Status)" />
</QuickGrid>

@code {
    private IQueryable<Order> orders = OrderData.Create(200).AsQueryable();
}

Don’t skip @rendermode InteractiveServer. Microsoft Learn says QuickGrid needs an interactive render mode for sorting and paging, which my Blazor render modes guide explains in detail.

First Blazor QuickGrid with PropertyColumn: columns Id, Customer, State, OrderDate, Total and Status, with raw dates like 10/26/2026 and totals like 2223.74
Step 3: six PropertyColumns, no formatting yet

Checkpoint: you should see a long, plain table. curl returned 200 with 201 <tr> rows (header + 200 orders), and the first row reads 1001 | Tyler Bennett | GA | 10/26/2026 | 2223.74 | Pending.

Step 4: Format QuickGrid Currency and Dates and Set a Default Sort

Raw 2223.74 isn’t money yet. Replace the columns with these (the Status column stays for now):

    <PropertyColumn Property="@(o => o.Id)" Title="Order #" Sortable="true" />
    <PropertyColumn Property="@(o => o.Customer)" Sortable="true" />
    <PropertyColumn Property="@(o => o.State)" Sortable="true" />
    <PropertyColumn Property="@(o => o.OrderDate)" Title="Order Date" Format="MMM d, yyyy" Sortable="true"
                    IsDefaultSortColumn="true" InitialSortDirection="SortDirection.Descending" />
    <PropertyColumn Property="@(o => o.Total)" Format="C" Sortable="true" Align="Align.Right" Class="total" />

Format works on any IFormattable type, so "C" gives US dollars, the same format string from formatting numbers with commas in C#. Title renames a header, and Align.Right lines up the cents.

IsDefaultSortColumn plus InitialSortDirection="SortDirection.Descending" puts the newest orders on top. The date pattern works like any DateTime format string in C#.

Blazor QuickGrid with Format C and MMM d, yyyy: the first row is 1016 | Kayla Brooks | NY | Oct 27, 2026 | $189.37 | Pending, sorted by Order Date descending
Step 4: USD totals, readable dates, newest first

Step 5: Sort QuickGrid Columns by Clicking a Header

Every column in Step 4 already has Sortable="true", so each header is now a button. Click Total once for ascending and again for descending.

In headless Edge, the first click put $35.56 (order 1168) on top. The second click gave $2,416.01, order 1051 from CA. The header’s aria-sort switched from ascending to descending.

Blazor QuickGrid sortable column: Total sorted descending after two header clicks, with $2,416.01 for order 1051 on top
Step 5: Total clicked twice, largest order first

Checkpoint: after two clicks on Total, you see $2,416.01, $2,411.95 and $2,395.34 at the top.

Step 6: Add a QuickGrid TemplateColumn with a Status Badge and a Button

A TemplateColumn renders any markup you like, with the row in context. Swap the Status PropertyColumn for these two columns:

    <TemplateColumn Title="Status" SortBy="byStatus">
        <span class="badge @BadgeCss(context.Status)">@context.Status</span>
    </TemplateColumn>
    <TemplateColumn Title="Action">
        @if (context.Status == "Pending")
        {
            <button class="btn btn-sm btn-outline-primary ship" @onclick="@(() => Ship(context))">Ship</button>
        }
    </TemplateColumn>

Template columns can’t guess how to sort, so SortBy takes a GridSort<Order>. Change the grid to Items="orders.AsQueryable()" and add this to @code:

    private readonly List<Order> orders = OrderData.Create(200);
    private readonly GridSort<Order> byStatus = GridSort<Order>.ByAscending(o => o.Status);

    private void Ship(Order order) => order.Status = "Shipped";

    private static string BadgeCss(string status) => status switch
    {
        "Pending" => "text-bg-warning",
        "Shipped" => "text-bg-primary",
        "Delivered" => "text-bg-success",
        _ => "text-bg-secondary"
    };

The badge color comes from a C# switch expression. I clicked the first Ship button, on order 1016, and its row changed to 1016 | Kayla Brooks | NY | Oct 27, 2026 | $189.37 | Shipped.

Blazor QuickGrid TemplateColumn with Bootstrap status badges and Ship buttons; order 1016 now shows Shipped after the click
Step 6: badges and a Ship button in TemplateColumns

Step 7: Add a QuickGrid Filter with a Search Box and a State Dropdown

QuickGrid has no filter row. You filter the IQueryable yourself and hand the result to Items. Add this markup above the grid:

<div class="d-flex gap-2 mb-2" style="max-width: 36rem;">
    <input id="search" class="form-control" placeholder="Search customer"
           @bind="search" @bind:event="oninput" />
    <select id="state" class="form-select" @bind="state">
        <option value="">All states</option>
        @foreach (var s in States)
        {
            <option value="@s">@s</option>
        }
    </select>
</div>

<p id="summary">@Filtered.Count() orders</p>

Then point the grid at Items="Filtered" and add the fields. Filtered is plain LINQ Where over the list:

    private static readonly string[] States = ["CA", "CO", "FL", "GA", "IL", "NY", "TX", "WA"];
    private string search = "";
    private string state = "";

    private IQueryable<Order> Filtered => orders.AsQueryable()
        .Where(o => o.Customer.Contains(search, StringComparison.OrdinalIgnoreCase))
        .Where(o => state == "" || o.State == state);

@bind:event="oninput" filters on every keystroke, and StringComparison.OrdinalIgnoreCase makes “brooks” match “Brooks” (more on Contains checks in C#).

The summary went from 200 orders to 22 orders after typing “brooks”, then 5 orders with TX picked.

Blazor QuickGrid filter before and after: 200 orders unfiltered on the left, 5 orders for brooks in TX on the right
Step 7: search box plus state dropdown

Checkpoint: “brooks” + TX leaves 5 rows, starting with Tyler Brooks.

Step 8: Add QuickGrid Pagination with Paginator

Paging needs a PaginationState with ItemsPerPage, the grid’s Pagination parameter and a <Paginator> under it:

<QuickGrid Items="Filtered" Pagination="pagination">
    ...
</QuickGrid>

<Paginator State="pagination" />
    private readonly PaginationState pagination = new() { ItemsPerPage = 10 };

    // Back to page 1 whenever the filter changes
    private Task ResetPage() => pagination.SetCurrentPageIndexAsync(0);

Why ResetPage? A filter shrinks the list, and a reader on page 3 should land back on page 1 instead of a page that may no longer exist. So both inputs get @bind:after="ResetPage":

@bind="search" @bind:event="oninput" @bind:after="ResetPage"
@bind="state" @bind:after="ResetPage"

I went to page 3 and typed “a”: the pager went from 200 items Page 3 of 20 to 185 items Page 1 of 19. SetCurrentPageIndexAsync returns a Task, the kind of method my async/await guide covers.

Blazor QuickGrid pagination: Paginator shows 200 items Page 1 of 20 on the left and 200 items Page 2 of 20 after clicking next on the right
Step 8: the Paginator, before and after clicking next

Checkpoint: your pager reads 200 items Page 1 of 20, and one click on next shows 200 items Page 2 of 20 starting at order 1157.

Step 9: Style the QuickGrid with Class, CSS Isolation and Alignment

Out of the box, QuickGrid renders a bare table. Wrap it in a div and give it Bootstrap classes through Class:

<div class="grid">
<QuickGrid Items="Filtered" Pagination="pagination" Class="table table-sm align-middle">
    ...
</QuickGrid>
</div>

In the browser, the table’s class became quickgrid table table-sm align-middle, and it kept theme="default", the Theme value QuickGrid’s built-in styles match.

For your own rules, add Components/Pages/Orders.razor.css. The grid is a child component, so isolated CSS needs ::deep to reach its cells:

.grid ::deep table {
    min-width: 52rem;
}

.grid ::deep thead th {
    background: #eef2f7;
}

.grid ::deep td.total {
    font-variant-numeric: tabular-nums;
    font-weight: 600;
}

.grid ::deep tr.picked > td {
    background: #fff4d6;
}

td.total works because the column’s Class="total" lands on its header and every cell. Align.Right added col-justify-right to the same th.

Blazor QuickGrid styling with Bootstrap table classes, a gray header from CSS isolation and bold right-aligned totals
Step 9: Class, ::deep CSS and right-aligned totals

Step 10: Select QuickGrid Rows with a Checkbox TemplateColumn

QuickGrid in .NET 10 has no built-in row selection (a row click event, OnRowClick, only shows up in the .NET 11 docs). A checkbox TemplateColumn and a HashSet do the job.

Add a first column to the grid, plus RowClass="RowCss" on the <QuickGrid> tag. RowClass, new in .NET 10, adds a CSS class to each row you return one for:

    <TemplateColumn Title="Pick">
        <input type="checkbox" checked="@selected.Contains(context.Id)"
               @onchange="@(() => Toggle(context.Id))" />
    </TemplateColumn>

Then change the summary line and add the selection code to @code. The picked class is the pale yellow rule from Step 9’s CSS file:

<p id="summary">@Filtered.Count() orders, @selected.Count selected (@SelectedTotal.ToString("C"))</p>
    private readonly HashSet<int> selected = [];

    private decimal SelectedTotal => orders.Where(o => selected.Contains(o.Id)).Sum(o => o.Total);

    private void Toggle(int id)
    {
        if (!selected.Remove(id)) selected.Add(id);
    }

    private string? RowCss(Order order) => selected.Contains(order.Id) ? "picked" : null;
VS Code with the finished Orders.razor open, showing QuickGrid with RowClass, a checkbox TemplateColumn and PropertyColumns, and a successful dotnet build
The finished Orders.razor in VS Code

Selection is by order Id, so it survives paging. After ticking rows 2 and 4, the summary read 200 orders, 2 selected ($2,259.30), and still 200 orders, 2 selected ($2,259.30) after a trip to page 2.

Blazor QuickGrid select row: two checkboxes ticked, both rows highlighted by RowClass, summary 200 orders, 2 selected ($2,259.30)
Step 10: checkbox selection with RowClass highlighting

Checkpoint: when you tick two rows, they turn pale yellow and the summary shows 200 orders, 2 selected ($2,259.30).

Test the Blazor QuickGrid

Here’s every check I ran on the finished page in headless Edge, with real clicks and typing:

CheckActionResult
Default sortLoad /ordersOrder 1016, Oct 27, 2026, on top
SortClick Total twice$2,416.01 first
Filter“brooks” + TX5 orders
PagingNext200 items Page 2 of 20
Reset on filterPage 3, type “a”185 items Page 1 of 19
Last pageGo to last page185 items Page 19 of 19, 5 orders + 5 empty rows
SelectionTick 2 rows200 orders, 2 selected ($2,259.30)
Intro shotCA, Total high to low, tick 231 orders, 2 selected ($4,652.41)

The empty rows on the last page are on purpose. Learn says the Paginator pads the final page so rows keep the same height on every page.

Virtualize QuickGrid for 10,000 Rows

If you have thousands of rows, paging isn’t your only option. Virtualize="true" renders only the rows near the scroll position. Set ItemSize to your row height in pixels and keep every row the same height:

@page "/orders-virtual"
@rendermode InteractiveServer
@using Microsoft.AspNetCore.Components.QuickGrid
@using QuickGridDemo.Models
@using QuickGridDemo.Services

<PageTitle>10,000 orders</PageTitle>

<h1>10,000 orders, virtualized</h1>

<div class="virtual-grid" style="height: 420px; overflow-y: auto;">
    <QuickGrid Items="orders" Virtualize="true" ItemSize="35" Class="table table-sm">
        <PropertyColumn Property="@(o => o.Id)" Title="Order #" Sortable="true" />
        <PropertyColumn Property="@(o => o.Customer)" Sortable="true" />
        <PropertyColumn Property="@(o => o.State)" Sortable="true" />
        <PropertyColumn Property="@(o => o.Total)" Format="C" Sortable="true" Align="Align.Right" />
    </QuickGrid>
</div>

@code {
    private IQueryable<Order> orders = OrderData.Create(10_000).AsQueryable();
}

With 10,000 orders, the DOM held 21 <tr> elements on load and 25 after I scrolled the box down, showing orders 7209 to 7233.

Blazor QuickGrid Virtualize with 10,000 orders in a 420px scroll box, scrolled down to orders 7209 to 7233
Virtualize: 10,000 orders, about two dozen rows in the DOM

Important: pick paging or virtualization. I set both on a test page, and the grid showed 10 rows a page (10000 items Page 1 of 1000), so virtualization did nothing. Learn advises against the mix too.

Load Server-Side Data with QuickGrid ItemsProvider

When the data lives behind an API, use ItemsProvider instead of Items. QuickGrid calls it with the start index, row count and sort for each page:

@page "/orders-provider"
@rendermode InteractiveServer
@using Microsoft.AspNetCore.Components.QuickGrid
@using QuickGridDemo.Models
@using QuickGridDemo.Services
@inject ILogger<ProviderOrders> Logger

<PageTitle>Orders (ItemsProvider)</PageTitle>

<h1>Orders from an ItemsProvider</h1>

<QuickGrid ItemsProvider="provider" Pagination="pagination">
    <PropertyColumn Property="@(o => o.Id)" Title="Order #" Sortable="true" />
    <PropertyColumn Property="@(o => o.Customer)" Sortable="true" />
    <PropertyColumn Property="@(o => o.Total)" Format="C" Sortable="true" Align="Align.Right" Class="total" />
</QuickGrid>

<Paginator State="pagination" />

@code {
    private static readonly List<Order> all = OrderData.Create(200);
    private readonly PaginationState pagination = new() { ItemsPerPage = 10 };
    private GridItemsProvider<Order> provider = default!;

    protected override void OnInitialized()
    {
        // In a real app, this is where you'd call your web API with skip/take/sort parameters
        provider = request =>
        {
            Logger.LogInformation("StartIndex={Start}, Count={Count}, SortBy={Sort}", request.StartIndex,
                request.Count, string.Join(",", request.GetSortByProperties().Select(p => $"{p.PropertyName} {p.Direction}")));
            var page = request.ApplySorting(all.AsQueryable())
                .Skip(request.StartIndex)
                .Take(request.Count ?? 10)
                .ToList();
            return ValueTask.FromResult(GridItemsProviderResult.From(page, all.Count));
        };
    }
}

My logger printed these three requests: page 1, then next, then a click on Total:

StartIndex=0, Count=10, SortBy=
StartIndex=10, Count=10, SortBy=
StartIndex=10, Count=10, SortBy=Total Ascending

Notice the grid stayed on page 2 after sorting. In a real app, you’d send those values to your API with HttpClient in C# and return the rows plus the total count.

Use QuickGrid with EF Core and the EntityFrameworkAdapter

With EF Core, QuickGrid can page and sort in the database. Add the adapter and a provider (SQLite here, both 10.0.14):

dotnet add package Microsoft.AspNetCore.Components.QuickGrid.EntityFrameworkAdapter --version 10.0.14
dotnet add package Microsoft.EntityFrameworkCore.Sqlite --version 10.0.14

Here’s the project file after all three packages:

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <BlazorDisableThrowNavigationException>true</BlazorDisableThrowNavigationException>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Components.QuickGrid" Version="10.0.14" />
    <PackageReference Include="Microsoft.AspNetCore.Components.QuickGrid.EntityFrameworkAdapter" Version="10.0.14" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.14" />
  </ItemGroup>

</Project>

The context is one DbSet in Data/SalesDbContext.cs:

using Microsoft.EntityFrameworkCore;
using QuickGridDemo.Models;

namespace QuickGridDemo.Data;

public class SalesDbContext(DbContextOptions<SalesDbContext> options) : DbContext(options)
{
    public DbSet<Order> Orders => Set<Order>();
}

AddQuickGridEntityFrameworkAdapter registers the async query executor. Here’s the final Program.cs, which also seeds sales.db with the same 200 orders:

using System.Globalization;
using QuickGridDemo.Components;
using Microsoft.EntityFrameworkCore;
using QuickGridDemo.Data;
using QuickGridDemo.Services;

// Show prices and dates the US way, whatever the server's regional settings are
CultureInfo.DefaultThreadCurrentCulture = new CultureInfo("en-US");
CultureInfo.DefaultThreadCurrentUICulture = new CultureInfo("en-US");

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents();

builder.Services.AddDbContextFactory<SalesDbContext>(options =>
    options.UseSqlite("Data Source=sales.db"));
builder.Services.AddQuickGridEntityFrameworkAdapter();

var app = builder.Build();

// Create sales.db with the same 200 orders the first time the app starts
using (var db = app.Services.GetRequiredService<IDbContextFactory<SalesDbContext>>().CreateDbContext())
{
    if (db.Database.EnsureCreated())
    {
        db.Orders.AddRange(OrderData.Create(200));
        db.SaveChanges();
    }
}

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error", createScopeForErrors: true);
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}
app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true);
app.UseHttpsRedirection();

app.UseAntiforgery();

app.MapStaticAssets();
app.MapRazorComponents<App>()
    .AddInteractiveServerRenderMode();

app.Run();

The page uses a context factory, as Learn recommends, and disposes the context with the component. EF.Functions.Like keeps the search in SQL:

@page "/orders-db"
@rendermode InteractiveServer
@implements IAsyncDisposable
@using Microsoft.AspNetCore.Components.QuickGrid
@using Microsoft.EntityFrameworkCore
@using QuickGridDemo.Data
@using QuickGridDemo.Models
@inject IDbContextFactory<SalesDbContext> DbFactory

<PageTitle>Orders (EF Core)</PageTitle>

<h1>Orders from SQLite</h1>

<input id="search" class="form-control mb-2" style="max-width: 18rem;" placeholder="Search customer"
       @bind="search" @bind:event="oninput" />

<QuickGrid Items="Filtered" Pagination="pagination" Class="table table-sm">
    <PropertyColumn Property="@(o => o.Id)" Title="Order #" Sortable="true" IsDefaultSortColumn="true" />
    <PropertyColumn Property="@(o => o.Customer)" Sortable="true" />
    <PropertyColumn Property="@(o => o.State)" Sortable="true" />
    <PropertyColumn Property="@(o => o.Total)" Format="C" Sortable="true" Align="Align.Right" Class="total" />
</QuickGrid>

<Paginator State="pagination" />

@code {
    private SalesDbContext context = default!;
    private readonly PaginationState pagination = new() { ItemsPerPage = 10 };
    private string search = "";

    private IQueryable<Order> Filtered =>
        context.Orders.Where(o => EF.Functions.Like(o.Customer, $"%{search}%"));

    protected override void OnInitialized() => context = DbFactory.CreateDbContext();

    public async ValueTask DisposeAsync() => await context.DisposeAsync();
}
VS Code with DbOrders.razor open, showing IDbContextFactory, a QuickGrid over context.Orders and EF.Functions.Like, and a successful dotnet build
DbOrders.razor in VS Code

After two clicks on Total and one on next, EF Core logged this query. Only 10 rows leave the database:

SELECT "o"."Id", "o"."Customer", "o"."OrderDate", "o"."State", "o"."Status", "o"."Total"
FROM "Orders" AS "o"
WHERE "o"."Customer" LIKE @Format
ORDER BY "o"."Total" COLLATE EF_DECIMAL DESC
LIMIT @p OFFSET @p
Blazor QuickGrid with the EF Core adapter and SQLite, sorted by Total descending on page 2: 1086 | Ashley Morales | FL | $2,304.04
EF Core + SQLite: 200 items Page 2 of 20, sorted in the database

Two things surprised me. EF Core 10 sorted the decimal column in SQLite with COLLATE EF_DECIMAL, no value converter needed. And my first version, without IsDefaultSortColumn on Order #, made EF Core warn on every page:

The query uses a row limiting operator ('Skip'/'Take') without an 'OrderBy' operator. This may lead to unpredictable results. If the 'Distinct' operator is used after 'OrderBy', then make sure to use the 'OrderBy' operator after 'Distinct' as the ordering would otherwise get erased.

Without an ORDER BY, page 2 can repeat rows from page 1, so always give a database grid a default sort. For the full EF Core setup, see my ASP.NET Core Web API CRUD with EF Core guide.

Note: Learn also documents a QuickGrid scaffolder that builds CRUD pages from a model: dotnet aspnet-codegenerator blazor CRUD with -dbProvider, -dc, -m and -outDir. I didn’t run it here.

QuickGrid vs a Plain HTML Table: Which Should You Use?

You needUse
A short, read-only list (under ~20 rows)A plain <table> with @foreach
Sorting, paging or a filter on hundreds of rowsQuickGrid
Thousands of rows in one scroll boxQuickGrid with Virtualize
Hierarchical rows, column drag-reorder, Excel-style range selectionA third-party grid (Learn says QuickGrid won’t add these)

I start every admin page with QuickGrid now. It’s free, it’s maintained by the ASP.NET Core team, and the whole grid fits in one Razor file.

Troubleshooting Blazor QuickGrid

Headers don’t sort and the Paginator does nothing. The page is static SSR. Add @rendermode InteractiveServer.

“The type or namespace name ‘QuickGrid’ could not be found”. The package is missing, or the page lacks @using Microsoft.AspNetCore.Components.QuickGrid. Put that line in _Imports.razor to cover every page.

Prices show the wrong currency symbol. Format="C" uses the server’s culture. Set en-US as in Step 1.

“A second operation was started on this context instance”. Learn lists this for concurrent EF Core queries. Create the context per component with IDbContextFactory, as DbOrders.razor does.

Note: before you put a grid like this online, lock the page down with [Authorize]. My JWT authentication guide covers the API side.

Where to Take It Next with Blazor QuickGrid

Add an Edit button in the Action column that opens a form, or export the filtered rows to CSV. If you’re moving an older grid app over, start with upgrading .NET 8 to .NET 10 to get RowClass.

A few more places to keep going:

Frequently Asked Questions

Does Blazor QuickGrid support filtering?

Not as a built-in filter row. You filter the IQueryable you pass to Items, for example with a bound search box and a LINQ Where clause, and the grid re-renders with the result. A column’s ColumnOptions can host the filter input too.

How do I change the page size in QuickGrid pagination?

Set ItemsPerPage on the PaginationState, for example new PaginationState { ItemsPerPage = 25 }, and pass it to both the grid’s Pagination parameter and the Paginator’s State.

How do I select a row in Blazor QuickGrid?

QuickGrid in .NET 10 has no selection API. Add a TemplateColumn with a checkbox, store the selected IDs in a HashSet, and use RowClass to highlight the selected rows.

Which package do I need for QuickGrid?

Microsoft.AspNetCore.Components.QuickGrid (10.0.14 for .NET 10 as of November 2026). For EF Core data, also add Microsoft.AspNetCore.Components.QuickGrid.EntityFrameworkAdapter and call AddQuickGridEntityFrameworkAdapter.

Can I use QuickGrid in Blazor WebAssembly?

Yes. It works in any interactive render mode. For data behind a web API, use ItemsProvider so only the current page is requested.

Where is the official QuickGrid documentation and demo?

See ASP.NET Core Blazor QuickGrid component on Microsoft Learn and the QuickGrid for Blazor sample app it links to.