Claude Code Plugins

Community-maintained marketplace

Feedback

astrolabe-search-state

@astrolabe-apps/astrolabe-common
0
0

Generic search, filtering, sorting, and pagination utilities for .NET. Use when implementing search APIs with dynamic filtering, sorting, and pagination for list/grid endpoints.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

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