Introduction
When building web applications, working with large datasets is common. Loading all data at once is inefficient. Users often need to:
See data in pages (Paging)
Sort data by columns (Sorting)
Filter data based on criteria (Filtering)
In this tutorial, we will build a complete beginner-friendly project implementing paging, sorting, and filtering across ASP.NET Core API and Angular UI. You will learn how to pass parameters, query data efficiently, and display it interactively.
Application Architecture Overview
Angular UI → API Calls (Paging/Sorting/Filtering) → ASP.NET Core → SQL Server
Responsibilities:
UI: Capture user input, display paged data, allow sorting and filtering
API: Receive parameters (page, size, sort, filter) and return only required data
Database: Query efficiently with skip/take and where/order clauses
1. Setting Up the Database
Sample Table: Employees
CREATE TABLE Employees (
Id INT IDENTITY(1,1) PRIMARY KEY,
Name NVARCHAR(100),
Email NVARCHAR(100),
Department NVARCHAR(50)
);
Seed with some test data:
INSERT INTO Employees (Name, Email, Department)
VALUES
('Alice', '[email protected]', 'HR'),
('Bob', '[email protected]', 'IT'),
('Charlie', '[email protected]', 'Finance'),
-- repeat for 50+ records for paging
2. Creating the ASP.NET Core API
Create a New Project
dotnet new webapi -n PagingApi
cd PagingApi
Install EF Core packages:
dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
Create Employee Model
public class Employee
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public string Department { get; set; }
}
Create DbContext
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
public DbSet<Employee> Employees { get; set; }
}
Configure connection string and register DbContext in Program.cs.
3. Implementing Paging, Sorting & Filtering in API
Create EmployeesController:
[ApiController]
[Route("api/[controller]")]
public class EmployeesController : ControllerBase
{
private readonly AppDbContext _context;
public EmployeesController(AppDbContext context)
{
_context = context;
}
[HttpGet]
public async Task<IActionResult> Get(
int page = 1,
int pageSize = 10,
string sortColumn = "Name",
string sortDirection = "asc",
string search = "")
{
var query = _context.Employees.AsQueryable();
// Filtering
if(!string.IsNullOrEmpty(search))
{
query = query.Where(e => e.Name.Contains(search) || e.Email.Contains(search) || e.Department.Contains(search));
}
// Sorting
query = sortDirection.ToLower() == "asc"
? query.OrderByDynamic(sortColumn, true)
: query.OrderByDynamic(sortColumn, false);
// Total count for paging
var total = await query.CountAsync();
// Paging
var data = await query
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
return Ok(new { total, data });
}
}
Note: Implement OrderByDynamic helper for dynamic sorting.
public static class IQueryableExtensions
{
public static IQueryable<T> OrderByDynamic<T>(this IQueryable<T> source, string column, bool ascending)
{
if(string.IsNullOrEmpty(column))
return source;
var parameter = Expression.Parameter(typeof(T), "p");
var property = Expression.Property(parameter, column);
var lambda = Expression.Lambda(property, parameter);
string methodName = ascending ? "OrderBy" : "OrderByDescending";
var result = typeof(Queryable).GetMethods()
.Single(
method => method.Name == methodName
&& method.GetParameters().Length == 2
)
.MakeGenericMethod(typeof(T), property.Type)
.Invoke(null, new object[] { source, lambda });
return (IQueryable<T>)result;
}
}
4. Angular Frontend Setup
Create Angular Project
ng new paging-ui
cd paging-ui
ng add @angular/material
Employee Model
employee.model.ts:
export interface Employee {
id: number;
name: string;
email: string;
department: string;
}
Employee Service
employee.service.ts:
@Injectable({
providedIn: 'root'
})
export class EmployeeService {
apiUrl = 'https://localhost:5001/api/employees';
constructor(private http: HttpClient) {}
getEmployees(page: number, pageSize: number, sortColumn: string, sortDir: string, search: string) {
let params = new HttpParams()
.set('page', page)
.set('pageSize', pageSize)
.set('sortColumn', sortColumn)
.set('sortDirection', sortDir)
.set('search', search);
return this.http.get<{ total: number, data: Employee[] }>(this.apiUrl, { params });
}
}
5. Employee List Component
employee-list.component.ts:
export class EmployeeListComponent implements OnInit {
employees: Employee[] = [];
totalRecords = 0;
page = 1;
pageSize = 10;
sortColumn = 'name';
sortDirection = 'asc';
search = '';
constructor(private service: EmployeeService) {}
ngOnInit(): void {
this.loadEmployees();
}
loadEmployees() {
this.service.getEmployees(this.page, this.pageSize, this.sortColumn, this.sortDirection, this.search)
.subscribe(res => {
this.employees = res.data;
this.totalRecords = res.total;
});
}
onPageChange(newPage: number) {
this.page = newPage;
this.loadEmployees();
}
onSort(column: string) {
this.sortColumn = column;
this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
this.loadEmployees();
}
onSearchChange(searchValue: string) {
this.search = searchValue;
this.page = 1;
this.loadEmployees();
}
}
Employee List HTML
<input type="text" placeholder="Search..." (input)="onSearchChange($event.target.value)" />
<table>
<thead>
<tr>
<th (click)="onSort('name')">Name</th>
<th (click)="onSort('email')">Email</th>
<th (click)="onSort('department')">Department</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let emp of employees">
<td>{{ emp.name }}</td>
<td>{{ emp.email }}</td>
<td>{{ emp.department }}</td>
</tr>
</tbody>
</table>
<div>
<button (click)="onPageChange(page - 1)" [disabled]="page === 1">Prev</button>
<span>{{ page }}</span>
<button (click)="onPageChange(page + 1)" [disabled]="page * pageSize >= totalRecords">Next</button>
</div>
6. How It Works
Paging: Skip and Take on API, buttons on UI
Sorting: Dynamic OrderBy on API, click column headers on UI
Filtering: Where clause on API, input box in UI
Everything is connected via Angular HTTP requests and reactive state updates.
7. Best Practices
Limit pageSize to prevent overloading API
Use async pipes in Angular to avoid manual subscriptions
Debounce search input for better performance
Always validate query parameters on API
Use indexes in SQL for faster filtering/sorting
Return totalRecords to let frontend calculate pages
Conclusion
Paging, sorting, and filtering are essential features for any data-driven web app. By integrating ASP.NET Core API with Angular UI, you can build interactive, performant, and scalable applications. This project is perfect for beginners because it demonstrates:
This foundation will prepare you for building complex admin dashboards, reporting apps, and enterprise-level web projects.