astrolabe-search-state
Generic search, filtering, sorting, and pagination utilities for .NET. Use when implementing search APIs with dynamic filtering, sorting, and pagination for list/grid endpoints.
When & Why to Use This Skill
Astrolabe.SearchState is a comprehensive .NET utility library designed to simplify the implementation of robust search, filtering, sorting, and pagination for API endpoints. By providing standardized classes like SearchOptions and SearchHelper, it enables developers to build flexible, high-performance backends for data grids and list views with minimal boilerplate code, supporting both dynamic property-based operations and custom filtering logic.
Use Cases
- Building e-commerce product catalogs that require dynamic filtering by price range, category, and specifications alongside text-based search.
- Developing administrative dashboard backends that need efficient server-side pagination and multi-column sorting for large datasets.
- Implementing optimized API endpoints that use DTO projection to return only necessary data fields while maintaining complex query capabilities.
- Creating standardized search interfaces for enterprise applications that require consistent handling of search states across multiple entities.
| name | astrolabe-search-state |
|---|---|
| description | Generic search, filtering, sorting, and pagination utilities for .NET. Use when implementing search APIs with dynamic filtering, sorting, and pagination for list/grid endpoints. |
Astrolabe.SearchState - Generic Search & Filtering
Overview
Astrolabe.SearchState provides generic classes and utilities for implementing robust search, filtering, sorting, and pagination functionality in .NET applications. Build flexible search capabilities with minimal code using dynamic property-based operations.
When to use: Use this library when you need to implement search APIs with dynamic filtering, sorting, and pagination. Perfect for building list/grid endpoints with flexible query capabilities.
Package: Astrolabe.SearchState
Dependencies: Astrolabe.Common, LINQ
TypeScript Client: astrolabe-searchstate
Target Framework: .NET 7-8
Key Concepts
1. Search Options
SearchOptions encapsulates all search parameters:
- Query: Text search query
- Sort: Dynamic sorting by property name and direction
- Filters: Key-value filters applied to properties
- Pagination: Offset and length control
2. Search Results
SearchResults<T> contains:
- Total: Total count of matching items (for pagination UI)
- Entries: Current page of results
3. SearchHelper
Utility class providing:
- Dynamic property filtering
- Dynamic property sorting
- Generic search builders
- Customizable filter and sort logic
Common Patterns
Basic Search Implementation
using Astrolabe.SearchState;
using Microsoft.EntityFrameworkCore;
public class ProductService
{
private readonly AppDbContext _context;
public ProductService(AppDbContext context)
{
_context = context;
}
public async Task<SearchResults<Product>> SearchProducts(
SearchOptions options,
bool includeTotal = true)
{
// Create searcher with default sorters and filterers
var searcher = SearchHelper.CreateSearcher<Product, Product>(
query => query.ToListAsync(),
query => query.CountAsync()
);
return await searcher(_context.Products, options, includeTotal);
}
}
Search with Projection (DTO Mapping)
using Astrolabe.SearchState;
public class ProductService
{
public async Task<SearchResults<ProductDto>> SearchProducts(SearchOptions options)
{
var searcher = SearchHelper.CreateSearcher<Product, ProductDto>(
// Select - project to DTO
query => query.Select(p => new ProductDto
{
Id = p.Id,
Name = p.Name,
Price = p.Price,
CategoryName = p.Category.Name
}).ToListAsync(),
// Count - count source entities
query => query.CountAsync()
);
return await searcher(_context.Products, options, includeTotal: true);
}
}
public class ProductDto
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
public string CategoryName { get; set; } = string.Empty;
}
Custom Filtering Logic
using Astrolabe.SearchState;
// Create custom filterer
var filterer = SearchHelper.MakeFilterer<Product>(field =>
{
// Handle price range filter
if (field == "priceRange")
{
return (query, values) =>
{
if (values.Count != 2) return query;
if (!decimal.TryParse(values[0], out var min)) return query;
if (!decimal.TryParse(values[1], out var max)) return query;
return query.Where(p => p.Price >= min && p.Price <= max);
};
}
// Handle text search
if (field == "search")
{
return (query, values) =>
{
var searchTerm = values.FirstOrDefault();
if (string.IsNullOrEmpty(searchTerm)) return query;
return query.Where(p =>
p.Name.Contains(searchTerm) ||
p.Description.Contains(searchTerm) ||
p.Category.Name.Contains(searchTerm)
);
};
}
// Return null to use default property-based filtering
return null;
});
// Use custom filterer
var searcher = SearchHelper.CreateSearcher<Product, Product>(
query => query.ToListAsync(),
query => query.CountAsync(),
null, // Default sorter
filterer // Custom filterer
);
ASP.NET Core API Controller
using Astrolabe.SearchState;
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/products")]
public class ProductsController : ControllerBase
{
private readonly ProductService _productService;
public ProductsController(ProductService productService)
{
_productService = productService;
}
[HttpGet]
public async Task<ActionResult<SearchResults<ProductDto>>> Search(
[FromQuery] SearchOptions options)
{
var results = await _productService.SearchProducts(options);
return Ok(results);
}
}
// Example requests:
// GET /api/products?offset=0&length=10
// GET /api/products?query=laptop&sort=price&sortDesc=false
// GET /api/products?filters[category]=electronics&filters[priceRange]=100,500
Best Practices
1. Always Set Maximum Page Size
// ✅ DO - Limit page size to prevent excessive queries
SearchHelper.CreateSearcher<Product, Product>(
/* ... */,
maxLength: 100 // Max 100 items
);
// ❌ DON'T - Allow unlimited page size
SearchHelper.CreateSearcher<Product, Product>(/* ... */); // No limit!
2. Use Projection for Performance
// ✅ DO - Project to DTOs to select only needed columns
var searcher = SearchHelper.CreateSearcher<Product, ProductDto>(
query => query.Select(p => new ProductDto
{
Id = p.Id,
Name = p.Name
}).ToListAsync(),
/* ... */
);
// ❌ DON'T - Return full entities with all navigations
var searcher = SearchHelper.CreateSearcher<Product, Product>(
query => query.Include(p => p.Category)
.Include(p => p.Reviews)
.ToListAsync(), // Loads too much data!
/* ... */
);
3. Only Include Total When Needed
// ✅ DO - Skip total count when not needed for performance
var results = await searcher(query, options, includeTotal: false);
// ⚠️ CAUTION - Total count requires a separate COUNT query
var results = await searcher(query, options, includeTotal: true);
4. Validate Filter Values
// ✅ DO - Validate and sanitize filter values
if (field == "priceRange")
{
return (query, values) =>
{
if (values.Count != 2) return query;
if (!decimal.TryParse(values[0], out var min) || min < 0) return query;
if (!decimal.TryParse(values[1], out var max) || max < min) return query;
return query.Where(p => p.Price >= min && p.Price <= max);
};
}
// ❌ DON'T - Blindly parse and use values
return (query, values) => query.Where(p =>
p.Price >= decimal.Parse(values[0]) && // Throws on invalid input!
p.Price <= decimal.Parse(values[1])
);
Troubleshooting
Common Issues
Issue: Sorting by property doesn't work
- Cause: Property name case mismatch or property doesn't exist
- Solution: Property names are case-sensitive. Verify exact property name. Consider making custom sorter case-insensitive.
Issue: Filters not being applied
- Cause: Custom filterer returning null instead of filtered query
- Solution: Always return the query from filter functions, even if unchanged
Issue: Count query is slow
- Cause: Complex joins or lack of indexes
- Solution: Add database indexes on filtered/sorted columns, simplify count query if possible
Project Structure Location
- Path:
Astrolabe.SearchState/ - Project File:
Astrolabe.SearchState.csproj - Namespace:
Astrolabe.SearchState