Well we have spent the last 7 modules since module 23, MatPaginator & PageEvent: Custom Server-Side Paging, implementing server-side paging and sorting. We have done a lot of work to put all of the paging and sorting logic into the ApiResult class, make all of this logic DRY, loosely coupled, and designed for easy unit testing.
To make all of this happen, we have added paging and sorting HTTP parameters to the Rest API call on the Angular side, and paging and sorting incoming parameters in the Vehicles controller HTTP GET method and ApiResult factory method. I do want to eventually return to the SideNav and create a nice full search which we will call Advanced Search. But, for the time being, let’s see if we can once again piggy back off of our nice clean architecture and quickly implement the Quick Search feature by adding filterColumn and filterValue parameters throughout the layers of the web application.
Modify ApiResult
Open the ApiResult.cs file and make the modifications below in bold blue font.
FredsCarsAPI/Data/ApiResult.cs
using Microsoft.EntityFrameworkCore;
using System.Reflection;
using System.Linq.Dynamic.Core;
namespace FredsCarsAPI.Data
{
public class ApiResult<T>
{
private ApiResult(
List<T> data,
int count,
int pageIndex,
int pageSize
)
{
Data = data;
PageIndex = pageIndex;
PageSize = pageSize;
TotalCount = count;
TotalPages = (int)Math.Ceiling(count / (double)pageSize);
}
// factory method
public static async Task<ApiResult<T>> CreatAsync(
IQueryable<T> source,
int pageIndex,
int pageSize,
string? sortColumn = null,
string? sortOrder = null,
string? filterColumns = null,
string? filterValue = null
)
{
// filtering
string[]? filterColumnArray = null;
if (!string.IsNullOrEmpty(filterColumns))
{
filterColumnArray = filterColumns.Split(',');
}
if (!string.IsNullOrEmpty(filterColumns)
&& !string.IsNullOrEmpty(filterValue)
&& filterColumnArray.ToList().All(fc => IsValidProperty(fc))
)
{
string queryString = string.Empty;
foreach (var fc in filterColumnArray)
{
queryString += string.Format("{0}.StartsWith(@0) || ",
fc);
}
queryString = queryString.Substring(0, queryString.Length - 4);
source = source.Where(queryString, filterValue);
}
// get record count
var count = await source.CountAsync();
/*** existing code ***/
In the code above we added two parameters to the CreateAsync()
factory method of ApiResult; filterColumns and filterValue both of type nullable string.
The filterColumns parameter will receive a comma delineated string of the columns to filter against such as “make,model”. And the filterValue will receive the value to filter by such as “Jeep” for the make or “Compass” for the model.
We add the filtering logic at the top of the method body. The filtering logic comes before the sorting and paging logic because we want to sort and page on the filtered data set.
Once we get into the body of the factory method, the first thing we need to do is convert the incoming filtersColumn string into an array of filter columns. We use the JavaScript string.Split()
method to split the comma delineated string into a string array called filterColumnArray. But first we also check to make sure filterColumns is not null or empty before attempting the conversion.
string[]? filterColumnArray = null;
if (!string.IsNullOrEmpty(filterColumns))
{
filterColumnArray = filterColumns.Split(',');
}
Within the filter logic we first check to make sure that both fitlerColumns (again) and filterValue are not null or empty. We also need to guard here against SQL Injection like we did for sorting. So we add one more condition to the if statement.
&& filterColumnArray.ToList().All(fc => IsValidProperty(fc))
In the above expression we are cleverly able to loop through the filterColunmArray using the LINQ ToList.All()
method and pass to it a lambda expression that passes each filter column (such as “make” and “model”) to the IsValidProperty()
method and make sure both are indeed properties of Type T; in this case T being Vehicle.
If all three conditions in the if statement are true we proceed, otherwise we skip the filtering if
statement block and continue down to sorting.
We then need to modify the IQueryable<T>
source variable using the IQueryable<T>.Where()
method and pass to it a string.Format()
expression as another Dynamic LINQ query just as we did for sorting.
source = source.Where(queryString, filterValue);
The Dynamic LINQ Where() method takes two parameters; queryString and filterValue. queryString is the formatted string for the Dynamic LINQ query and filterValue will fill the parameter placeholder in formatted string. There is actually quite a bit of build up to get the formatted string just right in the queryString variable for the Dynamic LINQ query.
string queryString = string.Empty;
foreach (var fc in filterColumnArray)
{
queryString += string.Format("{0}.StartsWith(@0) || ",
fc);
}
queryString = queryString.Substring(0, queryString.Length - 4);
source = source.Where(queryString, filterValue);
In the code above we create a string variable called querystring to hold the formatted string as we build it up. Next we have a foreach block where we loop through each filter column in the filterColumnArray variable and in each iteration concatenate to the current querystring variable the string:
"{0}.StartsWith(@0) || "
In each iteration the string.Format() expression also has as a parameter fc or the filterColumn name of that iteration and replaces the “{0}” placeholder with it.
Once the application exits the block the querystring will look something like:
"make.StartsWith(@0) || model.StartsWith(@0) || "
In C# this reads as the value of the column “make” starts with the value of the “{@0} parameter OR the value of the column “model” starts with the value of the “{@0}” parameter.
The next line removes the extra logical OR pipes from the end of the queryString.
queryString = queryString.Substring(0, queryString.Length - 4);
Finally, we are able to modify the IQueryable<T>
source variable with the IQueryable<T>.Where()
method passing to it the queryString we built up and the filterValue variable as a parameter to fill every instance of the “{@0}
” parameter placeholder in the foramat string. If we had more then one parameter to pass we would need one more than one parameter placeholder in the format string such as (@0)
, (@1)
, (@2)
.
The end result of the queryString for the Dynamic LINQ query will look similar to the following:
'make.StartsWith("Jeep") || model.StartsWith("Jeep")'
Or
'make.StartsWith("Compass") || model.StartsWith("Compass")'
Modify the Vehicles controller
Open the VehiclesController.cs file and modify it with the contents below.
FredsCarsAPI/Controllers/VehiclesController.cs
/*** existing code ***/
[HttpGet]
public async Task<ApiResult<VehicleDTO>> GetVehicles(
int pageIndex = 0, int pageSize = 10,
string? sortColumn = null,
string? sortOrder = null,
string? filterColumns = null,
string? filterValue = null)
{
// get Vehicles page
var dataQuery = _vehicleRepo.Vehicles.AsNoTracking()
.Include(v => v.VehicleType)
.ConvertVehiclesToDTOs();
return await ApiResult<VehicleDTO>.CreatAsync(
dataQuery,
pageIndex,
pageSize,
sortColumn,
sortOrder,
filterColumns,
filterValue);
}
}
}
In the code above we added filterColumns and filterValue to the incoming parameter list of the HTTP GET method GetVehciles()
. We then pass those values along with the IQueryable datasource, paging variables, and sort variables to the ApiResult factory method and return the result.
Test the API with Postman
Start a debugging instance of the Web API by right clicking on the FredsCarsAPI project and selecting “Debug --> Start New Instance
“.
Postman Test 1
Create a GET request with the following URL and click Send.
https://localhost:40443/api/Vehicles?pageIndex=0&pageSize=5&sortColumn=status&sortOrder=desc&filterColumns=make,model&filterValue=Jeep

In the screenshot above I have sent a request for the first page sorted on status in descending order and filtered on the ‘Make’ and ‘Model’ columns to give me back all Vehicles with a Make or Model value of ‘Compass’.
Here is the complete JSON result.
{
"data": [
{
"id": 9,
"status": "New",
"year": "2022",
"make": "Jeep",
"model": "Compass",
"color": "White",
"price": 34980,
"vin": "3C4NJDFB5NT114024",
"vehicleType": "Jeep"
},
{
"id": 10,
"status": "New",
"year": "2022",
"make": "Jeep",
"model": "Compass",
"color": "Red",
"price": 39275,
"vin": "3C4NJDCB1NT118172",
"vehicleType": "Jeep"
}
],
"pageIndex": 0,
"pageSize": 5,
"totalCount": 2,
"totalPages": 1
}
In the JSON results above you can see that there is a totalCount of 2 and both objects have a model value of Compass.
I’m pretty confident this is accurate because when I look at the data in SSOX (SQL Server Object Explorer) in Visual Studio there are only two records with a model type of Compass.

Postman Test 2
Create another GET request with the following URL and click Send.
https://localhost:40443/api/Vehicles?pageIndex=0&pageSize=5&sortColumn=status&sortOrder=desc&filterColumns=make,model&filterValue=Jeep

In the screenshot above I have sent a request for the first page sorted on status in descending order and filtered on the ‘Make’ and ‘Model’ columns to give me back all Vehicles with a Make or Model value of ‘Jeep’.
Here is the complete JSON result.
{
"data": [
{
"id": 9,
"status": "New",
"year": "2022",
"make": "Jeep",
"model": "Compass",
"color": "White",
"price": 34980,
"vin": "3C4NJDFB5NT114024",
"vehicleType": "Jeep"
},
{
"id": 10,
"status": "New",
"year": "2022",
"make": "Jeep",
"model": "Compass",
"color": "Red",
"price": 39275,
"vin": "3C4NJDCB1NT118172",
"vehicleType": "Jeep"
},
{
"id": 11,
"status": "New",
"year": "2022",
"make": "Jeep",
"model": "Grand Cherokee",
"color": "Pearlcoat",
"price": 53575,
"vin": "1C4RJKBG5M8201121",
"vehicleType": "Jeep"
},
{
"id": 12,
"status": "New",
"year": "2021",
"make": "Jeep",
"model": "Wrangler Sport S",
"color": "Green",
"price": 40940,
"vin": "1C4GJXAN0MW856433",
"vehicleType": "Jeep"
}
],
"pageIndex": 0,
"pageSize": 5,
"totalCount": 4,
"totalPages": 1
}
In the JSON results above there is a totalCount of 4. And all four objects have a make value of Jeep.
Again I can verify this by looking in SSOX (SQL Server Object Explorer) in Visual Studio where I can see four records with a make value of Jeep.

It looks as though the new filtering capability is working fairly well for now. We’ll have a chance to unit test this feature soon enough.
Update the Angular Material Module
Before we update the Angular Vehicles component, we’re going to need to bring in a couple more Angular Material components that we will be needing. Modify the angular-material.module.ts file with the contents below.
FredsCars/src/app/angular-material.module.ts
import { NgModule } from "@angular/core";
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { MatTableModule } from '@angular/material/table';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatSortModule } from '@angular/material/sort';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
const angularMatModules = [
MatToolbarModule,
MatIconModule,
MatButtonModule,
MatTableModule,
MatProgressSpinnerModule,
MatPaginatorModule,
MatSortModule,
MatSidenavModule,
MatCheckboxModule,
MatFormFieldModule,
MatInputModule
]
@NgModule({
imports: [
angularMatModules
],
exports: [
angularMatModules
]
})
export class AngularMaterialModule { }
In the above code we have registered the MatFormField and MatInput Angular Material components so we can use them to dress up the input field where the user enters their filter value and keep the UI consistant.
Modify the Vehicles component TypeScript
Open the vehicles.component.ts file and make the modifications shown below in bold blue font.
FredsCars/src/app/vehicles/vehicles.component.ts
import { Component, OnInit, ViewChild } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Vehicle } from './vehicle';
import { environment } from '../../environments/environment.development';
import { MatTableDataSource } from '@angular/material/table';
import { MatPaginator, PageEvent } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { Category, VehicleType } from './vehicleType';
@Component({
selector: 'app-vehicles',
templateUrl: './vehicles.component.html',
styleUrls: ['./vehicles.component.scss']
})
export class VehiclesComponent implements OnInit {
// debug JSON var for HTML
public json = JSON;
public vehicles!: MatTableDataSource<Vehicle>
columnsToDisplay: string[] = ['id', 'vehicleType', 'status', 'year', 'make', 'model', 'color', 'price'];
defaultPageIndex: number = 0;
defaultPageSize: number = 10;
public defaultSortColumn: string = "id";
public defaultSortOrder: "asc" | "desc" = "asc";
defaultFilterColumns: string = "make,model";
filterValue?: string;
@ViewChild(MatPaginator) paginator!: MatPaginator;
@ViewChild(MatSort) sort!: MatSort;
// Category Search Checkbox client-model
categoryAll: Category = {
id: 0,
name: 'All',
selected: false,
categories: []
};
allCategoriesSelected: boolean = false;
constructor(private http: HttpClient) {
// Instantiate the MatTableDataSource.
this.vehicles = new MatTableDataSource<Vehicle>();
}
ngOnInit() {
// moved get vehicles to load data
// so MatSort can call multiple times as user
// clicks a new sort column.
this.loadData();
// get vehcileTypes and transform into search categories
// for sidenav (leave in ngOnitInit since we only need to
// do this once)
this.http.get<VehicleType[]>(environment.baseUrl + 'api/vehicleTypes').subscribe(result => {
result.forEach(vt => {
this.categoryAll.categories?.push(
{
id: vt.id,
name: vt.name,
selected: false
}
);
})
}, error => console.error(error));
}
loadData(filterValue?: string) {
// get vehicles
var pageEvent = new PageEvent();
pageEvent.pageIndex = 0;
pageEvent.pageSize = 5;
this.filterValue = filterValue;
this.getVehicleData(pageEvent);
}
getVehicleData(event: PageEvent) {
// get rid of third empty click state for MatSort
if (this.sort && this.sort.direction == "") {
this.sort.direction = "asc";
}
var url = environment.baseUrl + 'api/vehicles';
var params = new HttpParams()
.set("pageIndex", event.pageIndex.toString())
.set("pageSize", event.pageSize.toString())
.set("sortColumn", (this.sort)
? this.sort.active
: this.defaultSortColumn)
.set("sortOrder", (this.sort)
? this.sort.direction
: this.defaultSortOrder);
if (this.filterValue) {
params = params
.set("filterColumns", this.defaultFilterColumns)
.set("filterValue", this.filterValue);
}
this.http.get<any>(url, { params })
.subscribe(result => {
this.paginator.length = result.totalCount;
this.paginator.pageIndex = result.pageIndex;
this.paginator.pageSize = result.pageSize;
this.vehicles.data = result.data
}, error => console.error(error));
}
/*** Category Search Checkbox methods ***/
public someSearchCategoriesSelected(): boolean {
return this.categoryAll.categories!.filter(cat =>
cat.selected).length > 0 && !this.allCategoriesSelected;
}
setAllCategoriesSelected(checked: boolean) {
this.allCategoriesSelected = checked;
this.categoryAll.selected = checked;
this.categoryAll.categories!.forEach(cat =>
(cat.selected = checked));
}
updateAllCategoriesSelected() {
var allSelected: boolean =
this.categoryAll.categories!.every(cat =>
cat.selected);
this.allCategoriesSelected = allSelected;
this.categoryAll.selected = allSelected;
}
/*** End Category Search Checkbox methods ***/
}
In the code above, the first thing we did was add two properties to the VehiclesComponent class:
- defaultFilterColumns of type string with the value “make, model”
- filterValue of type nullable string: used to capture the user input for filtering.
Next we modified the signature of the loadData()
method to take in a nullable string parameter called filterValue. Within the loadData()
method body we assign the incoming filterValue parameter’s value to the filterValue property of the class.
The final modification comes in the getVehicleData()
method. While building up the HTTP parameters, we use an if statement to check if the filterValue property of the class has a value or not. If so, we set two new HttpParam values with the HttpParams.set()
method.
- filterColumns: gets set to defaultFilterColumns class property with a value of “make,model”.
- filterValue: gets set to filterValue class property.
Modify the Vehicles HTML Template
Open the vehicles.component.html file and modify the code with the bold blue font shown below.
FredsCars/src/app/vehicles/vehicles.component.html
/*** existing code ***/
<mat-sidenav-content>
<p *ngIf="!(vehicles.data.length > 0)">
<mat-spinner style="margin: 0 auto;"></mat-spinner>
</p>
<div *ngIf="vehicles.data.length > 0">
<button mat-icon-button
color="primary"
(click)="sidenav.toggle()">
<mat-icon>
search
</mat-icon>
</button>
</div>
<mat-form-field [ngClass]="{'hide-results' : vehicles.data.length <= 0}">
<mat-label>Quick Search</mat-label>
<input matInput #filter (keyup)="loadData(filter.value)"
placeholder="Filter by Make or Model" />
</mat-form-field>
<table mat-table [dataSource]="vehicles"
matSort
[matSortActive]="defaultSortColumn"
[matSortDirection]="defaultSortOrder"
(matSortChange)="loadData(filter.value)"
*ngIf="vehicles.data.length > 0">
<ng-container matColumnDef="vehicleType">
<th mat-header-cell *matHeaderCellDef mat-sort-header>Category</th>
<td mat-cell *matCellDef="let item">
<b>{{ item.vehicleType }}</b>
</td>
</ng-container>
/*** existing code ***/
In the HTML Template code above, we introduce two new Angular Material components; MatFormField and MatInput. We added a new Angular Material MatFormField component using the mat-form-field element and use the Angular ngClass
attribute to hide the filter until the MatDataSource has data and the table displays with results.
Nested within the mat-form-field element we have a native input element decorated with the matInput attribute to utilize the AM MatInput component. We have also added an Angular template reference variable called #filter to the input element so we can reference the element in the TypeScript and later in the HTML.
For the final change in the Template HTML we added filter.value
, which references the input element with the #filter
template reference and gets its value, as a parameter to the loadData() method bound to the MatTable table element’s matSortChange event.
Using MatFormField
The definition for mat-form-field
on the Angular Material Form Field overview page is the following:
MatFormField
<mat-form-field>
is a component used to wrap several Angular Material components and apply common Text field styles such as the underline, floating label, and hint messages.
So without the MatInput component/input element being nested in a mat-form-field element, it would look like a basic input element with no styling at all. Which is fine, but since we are using a styling component library we can dress it up a bit and make it look nice.
The overview page goes on to say: The following Angular Material components are designed to work inside a <mat-form-field>
:
<input matNativeControl>
&<textarea matNativeControl>
<select matNativeControl>
<mat-select>
<mat-chip-list>
Using MatInput
The definition for matInput
on the Angular Material Input overview page is the following:
MatInput
matInput
is a directive that allows native<input>
and<textarea>
elements to work with<mat-form-field>
.
You can read more about MatInput in the link above for the overview page.
Modify the MatFormField styling
We are almost there. We just need to make one tweak to the MatFormField component’s styling. Open the vehicles.component.scss file and make the following changes shown in bold blue font.
FredsCars/src/app/vehicles/vehicles.component.scss
/*** existing code ***/
mat-sidenav {
padding-right: 10px;
}
li {
list-style: none;
}
.mat-mdc-form-field {
font-size: 14px;
width: 100%;
}
Here we just bumped up the font size of the input element a little and gave it a width of 100 percent.
Run the project
Run the project in debug mode, navigate to the Vehicles page and your screen should look similar to the following.

Type the character ‘j’ in the Quick Search input field and your browser should show the following results.

In the screenshot above four vehicles matched the filter input whose Make started with ‘j’ and they are all Jeeps.
If you type “com” in the Quick Search input field, you’ll get two Vehicles back in the results with the Model name of “Compass”.

Filter Performance Issues
The filtering we have just created is a very cool feature indeed. However I don’t know if you noticed, but the way this is implemented could affect performance on the server quite extensively if you have a lot of users of the application. You see every time a user types in a character in the filter input field, the keyup()
event makes a call to the server. So if they type “compass”, that search is hitting the server seven times.
But don’t worry. We will have a chance in later modules to refactor this feature and mitigate the issue.
What’s Next
In this module we were able to get a nice filtering Quick Search feature working along side our sorting and paging. In the next module we will unit test the filtering to make sure we didn’t miss anything as far as bugs or code efficiency. And then we will start to implement the Advanced Search in the SideNav.