GreenNide.FilterGenerator 0.0.1-alpha.5

This is a prerelease version of GreenNide.FilterGenerator.
dotnet add package GreenNide.FilterGenerator --version 0.0.1-alpha.5
                    
NuGet\Install-Package GreenNide.FilterGenerator -Version 0.0.1-alpha.5
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="GreenNide.FilterGenerator" Version="0.0.1-alpha.5" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="GreenNide.FilterGenerator" Version="0.0.1-alpha.5" />
                    
Directory.Packages.props
<PackageReference Include="GreenNide.FilterGenerator" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add GreenNide.FilterGenerator --version 0.0.1-alpha.5
                    
#r "nuget: GreenNide.FilterGenerator, 0.0.1-alpha.5"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package GreenNide.FilterGenerator@0.0.1-alpha.5
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=GreenNide.FilterGenerator&version=0.0.1-alpha.5&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=GreenNide.FilterGenerator&version=0.0.1-alpha.5&prerelease
                    
Install as a Cake Tool

GreenNide.ExpressionFilter

NuGet Version NuGet Downloads

Roslyn source generator, который автоматически генерирует фильтры для запросов EF Core.

Вы описываете фильтр через Expression-свойства — генератор создаёт отдельный POCO-класс со свойствами и методом Apply(), который конструирует IQueryable<T>.Where(...) на основе заполненных свойств.

Установка

NuGet (рекомендуется)

dotnet add package GreenNide.FilterGenerator --prerelease

Или в .csproj:

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

Из исходников (для разработки)

<ProjectReference Include="..\GreenNide.FilterGenerator\GreenNide.FilterGenerator.csproj"
                  OutputItemType="Analyzer"
                  ReferenceOutputAssembly="false" />

Быстрый старт

1. Определите сущность

public class Order
{
    public Guid Id { get; set; }
    public string Description { get; set; } = "";
    public decimal Amount { get; set; }
    public DateTime CreatedAt { get; set; }
    public Guid? CustomerId { get; set; }
    public Customer? Customer { get; set; }
    public List<OrderItem> OrderItems { get; set; } = new();
    public List<OrderHistory> History { get; set; } = new();
}

2. Создайте определение фильтра

Описываем фильтр через статические Expression-свойства и (опционально) методы-предикаты:

using GreenNide.ExpressionFilter;

[GenerateFilter(typeof(Order))]
public partial class OrderFilterDefinition
{
    // Equal (по умолчанию для Guid?)
    public static Expression<Func<Order, Guid?>>? CustomerId { get; } = o => o.CustomerId;

    // Contains (по умолчанию для 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;

    // Навигация с null-guard
    public static Expression<Func<Order, string>>? CustomerName { get; } = o => o.Customer.Name;

    // Метод-предикат
    public static Expression<Func<Order, bool>>? HasItem(OrderFilterDefinition filter) =>
        filter.ItemId.HasValue
            ? o => o.OrderItems.Any(i => i.Id == filter.ItemId.Value)
            : null;

    // Instance-свойства для параметров методов-предикатов
    public Guid? ItemId { get; set; }
}

3. Используйте

Генератор создаёт отдельный POCO-класс OrderFilterParams (не наследуется от Definition) и расширение OrderFilterParamsExtensions:

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

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

Ключевое отличие: OrderFilterParams — standalone POCO, его можно свободно маппить в DTO, сериализовать, передавать на фронтенд.

Именование сгенерированного класса

Генератор использует простую конвенцию (2 варианта):

Исходный код Сгенерированный класс
[GenerateFilter(typeof(Order))] OrderFilterParams
[GenerateFilter(typeof(Order), ClassName = "MyFilter")] MyFilter

Правило: если ClassName не задан, используется {EntityName}FilterParams.

Типы фильтров

Простые поля

Каждое Expression-свойство описывает один фильтр:

// Equal (по умолчанию для числовых типов)
public static Expression<Func<Order, Guid?>>? CustomerId { get; } = o => o.CustomerId;

// Contains (по умолчанию для string)
public static Expression<Func<Order, string>>? Description { get; } = o => o.Description;

// GreaterThanOrEqual — через атрибут [Compare]
[Compare(CompareOperator.GreaterThanOrEqual)]
public static Expression<Func<Order, decimal?>>? MinAmount { get; } = o => o.Amount;

Поддерживаемые операторы:

Оператор Описание
Equal ==
NotEqual !=
GreaterThan >
GreaterThanOrEqual >=
LessThan <
LessThanOrEqual <=
Contains .Contains()
StartsWith .StartsWith()
EndsWith .EndsWith()

Авто-определение оператора:

  • stringContains
  • Все остальные типы → Equal
  • Можно переопределить через [Compare(...)]

Навигационные свойства

Для обращений через навигации генератор автоматически добавляет null-guard:

// o => o.Customer.Name
// Сгенерируется:
// if (!string.IsNullOrWhiteSpace(filter.CustomerName))
//     query = query.Where(e => e.Customer != null && e.Customer.Name.Contains(filter.CustomerName));

Многоуровневые навигации тоже работают:

// o => o.Order.Customer.Address.City
// Сгенерируется:
// e.Order != null && e.Order.Customer != null && e.Order.Customer.Address != null

Subquery (подзапросы)

Expression с вызовами LINQ-методов передаются в SQL как подзапросы:

public static Expression<Func<Order, OrderStatus?>>? CurrentStatus { get; } =
    o => o.History
        .OrderByDescending(h => h.Timestamp)
        .Select(h => (OrderStatus?)h.Status)
        .FirstOrDefault();

Массив string[] в Expression задаёт поиск по нескольким колонкам через ||:

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

Сгенерируется:

if (!string.IsNullOrWhiteSpace(filter.Search))
{
    query = query.Where(e =>
        e.Description.Contains(filter.Search) ||
        (e.Customer != null && e.Customer.Name.Contains(filter.Search)) ||
        (e.Customer != null && e.Customer.Email.Contains(filter.Search)));
}

Методы-предикаты

Статические методы с сигнатурой Expression<Func<TEntity, bool>>? Method(DefinitionType filter) позволяют писать сложные предикаты:

[GenerateFilter(typeof(Order))]
public partial class OrderFilterDefinition
{
    // Closure-свойства (instance)
    public Guid? ItemId { get; set; }
    public int? MinItemCount { get; set; }
    public decimal? MinItemPrice { 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;

    public static Expression<Func<Order, bool>>? AllItemsExpensive(OrderFilterDefinition filter) =>
        filter.MinItemPrice.HasValue
            ? o => o.OrderItems.All(i => i.Price >= filter.MinItemPrice.Value)
            : null;
}

Генератор автоматически:

  • Копирует closure-свойства в сгенерированный POCO-класс
  • В Apply() создаёт промежуточный объект Definition, копирует closure-свойства и вызывает методы-предикаты ( bridge-паттерн)
var filter = new OrderFilterParams { MinItemCount = 2, MinItemPrice = 100m };
var results = await dbContext.Orders.Apply(filter).ToListAsync();

Архитектура: Definition vs Generated

OrderFilterDefinition                    OrderFilterParams (генерируется)
┌─────────────────────────────┐         ┌─────────────────────────────┐
│ static Expression-свойства  │         │ public свойства             │
│ static методы-предикаты     │         │   (Expression + closure)    │
│ instance closure-свойства   │         │                             │
└─────────────────────────────┘         └─────────────────────────────┘
        ▲                                          │
        │         new Definition()                 │
        └──────────────────────────────────────────┘
              Apply() создаёт мост: копирует
              closure-свойства, вызывает предикаты
  • OrderFilterDefinition — определение фильтра с Expression-свойствами и методами-предикатами
  • OrderFilterParams — standalone POCO со всеми свойствами (можно маппить в DTO)
  • Между ними нет наследования — связь через bridge-паттерн в Apply()

Поддерживаемые nullable-типы

Генератор корректно работает с nullable value types:

  • int?, decimal?, Guid?, DateTime? и т.д. — используется .HasValue для проверки + .Value для доступа к значению
  • string? — используется string.IsNullOrWhiteSpace()
  • Ссылочные типы (классы) — используется != null

Тесты

dotnet test

Тесты включают:

  • Unit-тесты генератора — проверка генерации кода через Roslyn API
  • Интеграционные тесты EF Core — проверка трансляции в SQL через Testcontainers (PostgreSQL)
Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

This package has no dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.0.1-alpha.5 32 7/26/2026

Initial prerelease. Source generator for type-safe collection filtering.