Creating Effective Pagination and Filtering Middleware for ASP.NET Core Web API

Introduction

Implementing pagination and filtering using custom middleware can significantly optimize API responses and enable efficient handling of large datasets. Here's a step-by-step example of how to achieve this:

Step 1. Create a new ASP.NET Core Web API project using Visual Studio or the .NET CLI

Create an Asp.net core application using Visual Studio or the CLI commands

Step 2. Implement a custom middleware for pagination and filtering

using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
using Newtonsoft.Json;
using System;
using System.Linq;
using System.Threading.Tasks;

Author: Sardar mudassar ali khan
public class PaginationFilterMiddleware
{
    private readonly RequestDelegate _next;

    public PaginationFilterMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        if (context.Request.Query.ContainsKey("page") && 
        context.Request.Query.ContainsKey("pageSize"))
        {
            if (int.TryParse(context.Request.Query["page"], out int page) &&
                int.TryParse(context.Request.Query["pageSize"], out int pageSize))
            {
                var originalBodyStream = context.Response.Body;
                using (var responseBody = new System.IO.MemoryStream())
                {
                    context.Response.Body = responseBody;

                    await _next(context);

                    context.Response.Body.Seek(0, System.IO.SeekOrigin.Begin);

                    if (context.Response.StatusCode == 200) // Only apply pagination to successful responses
                    {
                        var response = await new System.IO.StreamReader(context.Response.Body).ReadToEndAsync();
                        var responseData = JsonConvert.DeserializeObject(response);

                        if (responseData is IEnumerable<object> data)
                        {
                            var totalItems = data.Count();
                            var totalPages = (int)Math.Ceiling((double)totalItems / pageSize);

                            var paginatedData = data.Skip((page - 1) * pageSize).Take(pageSize);

                            var paginationHeader = new
                            {
                                TotalItems = totalItems,
                                TotalPages = totalPages,
                                CurrentPage = page,
                                PageSize = pageSize
                            };

                            context.Response.Headers.Add("Pagination", JsonConvert.SerializeObject(paginationHeader));

                            var serializedData = JsonConvert.SerializeObject(paginatedData);
                            await context.Response.WriteAsync(serializedData);
                        }
                    }

                    context.Response.Body.Seek(0, System.IO.SeekOrigin.Begin);
                    await context.Response.Body.CopyToAsync(originalBodyStream);
                }
            }
        }
        else
        {
            await _next(context);
        }
    }
}

Step 3. Configure the custom middleware and implement pagination and filtering in the controller

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

Author: Sardar Mudassar Ali Khan
public class Startup
{
    public IConfiguration Configuration { get; }

    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();

        // Add pagination and filtering middleware
        services.AddTransient<PaginationFilterMiddleware>();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        // Use the custom pagination and filtering middleware
        app.UseMiddleware<PaginationFilterMiddleware>();

        app.UseRouting();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }
}

Step 4. Implement pagination and filtering in the controller

using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;

Author: Sardar Mudassar Ali Khan
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private static List<Product> _products = GenerateSampleProducts();

    private static List<Product> GenerateSampleProducts()
    {
        // Generate and return sample products
        return new List<Product>
        {
            new Product { Id = 1, Name = "Product A", Category = "Electronics" },
            new Product { Id = 2, Name = "Product B", Category = "Clothing" },
            new Product { Id = 3, Name = "Product C", Category = "Electronics" },
            // ... Add more products
        };
    }

    [HttpGet]
    public IActionResult Get([FromQuery] PaginationFilterDto filters)
    {
        var validFilters = new PaginationFilterDto(filters.Page, filters.PageSize);
        var pagedData = _products
            .Where(product =>
                (string.IsNullOrEmpty(validFilters.Category) || product.Category.ToLower() == validFilters.Category.ToLower())
            )
            .Skip((validFilters.Page - 1) * validFilters.PageSize)
            .Take(validFilters.PageSize)
            .ToList();

        return Ok(new PagedResponse<List<Product>>(pagedData, validFilters.Page, validFilters.PageSize));
    }
}

public class PaginationFilterDto
{
    public int Page { get; set; } = 1;
    public int PageSize { get; set; } = 10;
    public string Category { get; set; }
}

public class PagedResponse<T>
{
    public PagedResponse(T data, int currentPage, int pageSize)
    {
        Data = data;
        CurrentPage = currentPage;
        PageSize = pageSize;
    }

    public T Data { get; set; }
    public int CurrentPage { get; set; }
    public int PageSize { get; set; }
}

With this setup, the custom middleware PaginationFilterMiddleware intercepts responses and applies pagination based on query parameters page and pageSize if they are present. It calculates the pagination details and modifies the response body accordingly. This middleware helps optimize API responses by sending only the required data and providing pagination metadata in the response headers.

By integrating custom middleware for pagination and filtering, you ensure that API responses are optimized for large datasets while maintaining control over the pagination process.

Conclusion

Implementing pagination and filtering in your ASP.NET Core Web API using custom middleware offers several benefits for optimizing API responses and managing large datasets effectively. Let's summarize the key takeaways from this example:

Custom Middleware for Pagination and Filtering

  • Custom middleware, such as the PaginationFilterMiddleware, intercepts responses and applies pagination and filtering logic based on query parameters.
  • The middleware calculates pagination details, extracts relevant data, and modifies the response body and headers accordingly.

Optimizing API Responses

  1. Pagination and filtering help optimize API responses by reducing the amount of data sent to clients.
  2. Clients can request specific subsets of data, leading to quicker response times and improved network efficiency.

Efficient Handling of Large Datasets

  1. Pagination enables efficient handling of large datasets by breaking them into smaller chunks, making responses more manageable.
  2. Filtering allows clients to retrieve only the data that matches their criteria, further reducing the amount of transmitted data.

Metadata in Response Headers

  1. PaginationFilterMiddleware adds metadata to response headers, providing clients with information about the current page, total pages, total items, and page size.

Centralized and Modular Logic

  1. Using custom middleware centralizes pagination and filtering logic, making the codebase more organized and maintainable.
  2. Middleware encapsulates the process, enabling reuse across different controllers and actions.

Improved User Experience

  1. Optimized API responses provide a better user experience by reducing data transfer time and enabling clients to retrieve relevant data efficiently.

Testing and Flexibility

  1. Custom middleware for pagination and filtering can be thoroughly tested to ensure that it behaves as expected.
  2. The approach provides flexibility to adjust pagination and filtering behavior based on specific requirements.

By incorporating pagination and filtering with custom middleware, you've enhanced your ASP.NET Core Web API's performance and user experience. This methodology enables you to respond more efficiently to client requests, particularly when dealing with large amounts of data. By providing a streamlined API that delivers the right data in a user-friendly manner, you're well-prepared to offer a high-quality experience to your API users.

 


Similar Articles