Skip to content

Repository files navigation

GreenNide.FilterGenerator

NuGet Version NuGet Downloads

A Roslyn source generator that auto-generates EF Core filter classes from expression tree definitions. You describe filters with static expression properties — the generator creates a standalone POCO class with an Apply() extension method that builds IQueryable<T>.Where(...).

Install

dotnet add package GreenNide.FilterGenerator --prerelease

Or in .csproj:

<PackageReference Include="GreenNide.FilterGenerator" Version="0.0.1-alpha.5" />

Quick Start

1. Define a filter

using GreenNide.ExpressionFilter;

[GenerateFilter(typeof(Order))]
public partial class OrderFilterDefinition
{
    // Equal (default for Guid?)
    public static Expression<Func<Order, Guid?>>? CustomerId { get; } = o => o.CustomerId;

    // Contains (default for string)
    public static Expression<Func<Order, string>>? Description { get; } = o => o.Description;

    // GreaterThanOrEqual
    [Compare(CompareOperator.GreaterThanOrEqual)]
    public static Expression<Func<Order, decimal?>>? MinAmount { get; } = o => o.Amount;

    // LessThanOrEqual
    [Compare(CompareOperator.LessThanOrEqual)]
    public static Expression<Func<Order, decimal?>>? MaxAmount { get; } = o => o.Amount;

    // Navigation with automatic null-guard
    public static Expression<Func<Order, string>>? CustomerName { get; } = o => o.Customer.Name;

    // Multi-column search
    public static Expression<Func<Order, string[]>>? Search { get; } =
        o => new[] { o.Description, o.Customer.Name, o.Customer.Email };
}

2. Use it

The generator produces a standalone OrderFilterParams POCO class (no inheritance from the definition):

var filter = new OrderFilterParams
{
    CustomerId = someId,
    MinAmount = 100m,
    Description = "Premium"
};

var results = await dbContext.Orders
    .Apply(filter)
    .ToListAsync();

3. Bind from HTTP requests

Since OrderFilterParams is a plain POCO, it works directly with model binding. Use [FromQuery] for GET or [FromBody] for POST:

[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
    private readonly AppDbContext _db;
    public OrdersController(AppDbContext db) => _db = db;

    // GET /api/orders?customerId=...&minAmount=100&description=premium
    [HttpGet]
    public IActionResult Get([FromQuery] OrderFilterParams filter)
    {
        var results = _db.Orders.Apply(filter).ToList();
        return Ok(results);
    }

    // POST /api/orders/filter  { "minAmount": 100, "description": "premium" }
    [HttpPost("filter")]
    public IActionResult Filter([FromBody] OrderFilterParams filter)
    {
        var results = _db.Orders.Apply(filter).ToList();
        return Ok(results);
    }
}

A working example project is in Examples/.

Supported Operators

Operator Description
Equal ==
NotEqual !=
GreaterThan >
GreaterThanOrEqual >=
LessThan <
LessThanOrEqual <=
Contains .Contains()
StartsWith .StartsWith()
EndsWith .EndsWith()

Auto-detection: stringContains, everything else → Equal. Override with [Compare(...)].

Predicate Methods

Static methods with signature Expression<Func<TEntity, bool>>? Method(DefinitionType filter) let you write complex predicates:

[GenerateFilter(typeof(Order))]
public partial class OrderFilterDefinition
{
    public Guid? ItemId { get; set; }
    public int? MinItemCount { get; set; }

    public static Expression<Func<Order, bool>>? HasItem(OrderFilterDefinition filter) =>
        filter.ItemId.HasValue
            ? o => o.OrderItems.Any(i => i.Id == filter.ItemId.Value)
            : null;

    public static Expression<Func<Order, bool>>? HasMinItemCount(OrderFilterDefinition filter) =>
        filter.MinItemCount.HasValue
            ? o => o.OrderItems.Count >= filter.MinItemCount.Value
            : null;
}

Closure properties are automatically copied to the generated POCO class. This also works for collection types — e.g., filtering by multiple IDs:

[GenerateFilter(typeof(Product))]
public partial class ProductFilterDefinition
{
    // Closure property: a list of category IDs
    public List<Guid>? CategoryIds { get; set; }

    // Predicate: products whose CategoryId is in the list
    public static Expression<Func<Product, bool>>? InCategories(ProductFilterDefinition filter) =>
        filter.CategoryIds is { Count: > 0 }
            ? o => filter.CategoryIds.Contains(o.CategoryId!.Value)
            : null;
}

// Usage — the list binds from JSON body automatically:
// POST /api/products/filter  { "categoryIds": ["guid1", "guid2"] }
var filter = new ProductFilterParams
{
    CategoryIds = new List<Guid> { electronicsId, booksId }
};
var results = _db.Products.Apply(filter).ToList();

Custom Class Name

By default the generated class is named {EntityName}FilterParams. Override it with ClassName:

[GenerateFilter(typeof(Category), ClassName = "CategorySearchParams")]
public partial class CategoryFilterDefinition
{
    public static Expression<Func<Category, string>>? Name { get; } = o => o.Name;

    public static Expression<Func<Category, string[]>>? Search { get; } =
        o => new[] { o.Name };
}

// Generated class: CategorySearchParams (not CategoryFilterParams)
var filter = new CategorySearchParams { Name = "Electronics" };
var results = _db.Categories.Apply(filter).ToList();

Tests

dotnet test

About

C# Roslyn Codegen for generating filtering code for Enitity Framework

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages