Blazor Components
Interactive UI components for building dynamic queries in Blazor applications, with optional MudBlazor integration.
Installation#
Install the three packages needed for a Blazor query builder via the .NET CLI.
dotnet add package NetQueryBuilder dotnet add package NetQueryBuilder.EntityFramework dotnet add package NetQueryBuilder.Blazor
NetQueryBuilder provides the core query engine, NetQueryBuilder.EntityFramework
adds EF Core integration for database-backed queries, and NetQueryBuilder.Blazor supplies the
interactive UI components.
Setup#
Three steps to get the query builder running in your Blazor application.
1. Register services in Program.cs
Add your DbContext and the NetQueryBuilder query configurator to the service container.
// Register your DbContext builder.Services.AddDbContext<YourDbContext>(options => options.UseSqlServer(connectionString)); // Register the query configurator builder.Services.AddScoped<IQueryConfigurator, EfQueryConfigurator<YourDbContext>>();
2. Add imports in _Imports.razor
Add the component namespace so Blazor can resolve the query builder tags.
@using NetQueryBuilder.Blazor.Components
3. Include the stylesheet (optional)
The components ship with basic styling. Reference the bundled CSS in your index.html or App.razor.
<link href="_content/NetQueryBuilder.Blazor/css/netquerybuilder.css" rel="stylesheet" />
QueryBuilder Component#
The main component is QueryBuilder<TEntity>. Drop it into any Blazor page to get a
complete query-building interface, including field selection, condition construction, execution, and results.
@page "/products-query" @using NetQueryBuilder.Blazor.Components @using YourNamespace.Models @inject IQueryConfigurator QueryConfigurator <h3>Product Query Builder</h3> <QueryBuilder TEntity="Product" /> @code { // The component injects the configurator automatically // and builds the query on initialization. }
This renders a full query builder interface with four sections:
- SELECT section -- checkboxes for choosing which entity properties to display in the results.
- WHERE section -- a condition builder for creating filters with operators such as Equals, Contains, Greater Than, and more.
- Run button -- executes the compiled query against the data source.
- Results table -- displays the query output with only the selected columns.
Handling Results#
Use the OnQueryExecuted callback to capture query results in your own code. The callback
receives an IEnumerable<object> that you can cast to your entity type.
<QueryBuilder TEntity="Product" OnQueryExecuted="HandleQueryResults" /> @code { private IEnumerable<Product> _queryResults; private void HandleQueryResults(IEnumerable<object> results) { _queryResults = results.Cast<Product>(); // Process results -- export to CSV, update charts, etc. } }
Expression Output#
The Expression parameter exposes the generated LINQ expression as a human-readable string.
Use two-way binding with ExpressionChanged to keep a live copy in your page.
<QueryBuilder TEntity="Product" Expression="@_currentExpression" ExpressionChanged="@HandleExpressionChanged" /> <pre>@_currentExpression</pre> @code { private string _currentExpression; private void HandleExpressionChanged(string expression) { _currentExpression = expression; // Save the expression or use it elsewhere } }
Individual Components#
For more granular control you can break the full QueryBuilder into its constituent parts
and compose them yourself: ConditionComponent, PropertySelector, and
QueryResultTable.
ConditionComponent
Renders the WHERE clause builder. Pass it the IQuery.Condition and wrap everything
in a CascadingValue so child components can access the query context.
@inject IQueryConfigurator QueryConfigurator <CascadingValue Value="@_query"> <ConditionComponent Condition="@_query.Condition" /> </CascadingValue> <button class="btn btn-primary" @onclick="ExecuteQuery">Execute</button> @code { private IQuery _query; protected override void OnInitialized() { _query = QueryConfigurator.BuildFor<Product>(); } private async Task ExecuteQuery() { var results = await _query.Execute(50); // Process results } }
PropertySelector
Renders the SELECT checkboxes. Bind SelectedProperties and
SelectedPropertiesChanged to track which columns the user picks.
<PropertySelector TEntity="Product" SelectedProperties="@_selectedProperties" SelectedPropertiesChanged="@HandleSelectedPropertiesChanged" /> @code { private List<SelectPropertyPath> _selectedProperties = new(); private void HandleSelectedPropertiesChanged(List<SelectPropertyPath> properties) { _selectedProperties = properties; // Update your query or UI } }
QueryResultTable
Renders the results grid. Feed it the data and the list of selected properties so it knows which columns to display.
<QueryResultTable TEntity="Product" Data="@_queryResults" Properties="@_selectedProperties" />
Customization#
The components render standard HTML elements with CSS classes you can override. All class names are
prefixed with nqb- to avoid conflicts with your application styles.
CSS class overrides
/* Main query builder container */
.nqb-query-builder {
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 1.5rem;
}
/* Individual condition rows */
.nqb-condition {
background: #fafafa;
margin-bottom: 0.5rem;
}
/* Results table */
.nqb-results-table {
width: 100%;
border-collapse: collapse;
}
Integration with MudBlazor
MudBlazor is entirely optional. The query builder components work standalone with their own bundled CSS. MudBlazor is used only in the sample application for enhanced styling.
Wrap the query builder in MudBlazor layout components for a consistent look.
<MudCard> <MudCardContent> <QueryBuilder TEntity="Product" /> </MudCardContent> </MudCard>
Integration with Bootstrap
The same approach works with Bootstrap cards or any other CSS framework.
<div class="card"> <div class="card-body"> <QueryBuilder TEntity="Product" /> </div> </div>
Custom Operators#
You can register custom filter operators via ConfigureConditions during service
registration. The Blazor components will automatically pick up any registered operators and display
them in the condition dropdown.
builder.Services.AddScoped<IQueryConfigurator>(provider => { var configurator = new EfQueryConfigurator<YourDbContext>( provider.GetRequiredService<YourDbContext>()); configurator.ConfigureConditions(config => { config.RegisterOperator<YourCustomOperator>(); }); return configurator; });
Once registered, the custom operator appears alongside the built-in operators (Equals, Contains,
Greater Than, etc.) in every ConditionComponent dropdown.
Performance#
When working with large data sets, keep these tips in mind to ensure a responsive UI.
-
Debouncing -- The query builder internally debounces expression compilation so
that rapid user input does not trigger expensive recompilation on every keystroke. The default
debounce interval uses a
System.Threading.Timerwith a 300ms delay. -
Pagination -- Pass a page-size limit to
query.Execute(pageSize)to avoid loading entire tables into memory. The results table supports rendering paginated data. -
Server-side filtering -- Because queries compile to
IQueryableexpressions, filtering happens at the database level when using EF Core. Avoid calling.ToList()before applying conditions.
Component Lifecycle#
The QueryBuilder component subscribes to internal events and creates timers for
debouncing. To prevent memory leaks, it implements IAsyncDisposable. When you build
custom components on top of the query builder, follow the same pattern.
public partial class MyQueryComponent : ComponentBase, IAsyncDisposable { private System.Threading.Timer? _debounceTimer; private IQuery _query = null!; protected override void OnInitialized() { _query = QueryConfigurator.BuildFor<Product>(); _query.Condition.ConditionChanged += OnConditionChanged; } private void OnConditionChanged(object? sender, EventArgs e) { // Debounce the recompilation _debounceTimer?.Dispose(); _debounceTimer = new System.Threading.Timer(_ => { InvokeAsync(() => { StateHasChanged(); }); }, null, 300, Timeout.Infinite); } public async ValueTask DisposeAsync() { _query.Condition.ConditionChanged -= OnConditionChanged; if (_debounceTimer is not null) { await _debounceTimer.DisposeAsync(); } } }
Key points:
- Always unsubscribe from events in
DisposeAsyncto avoid dangling references. - Dispose any
System.Threading.Timerinstances to stop background callbacks. - Use
InvokeAsyncwhen updating UI state from timer or event callbacks, since they run on a background thread.