AutoQuery.Generator
1.1.0
dotnet add package AutoQuery.Generator --version 1.1.0
NuGet\Install-Package AutoQuery.Generator -Version 1.1.0
<PackageReference Include="AutoQuery.Generator" Version="1.1.0" />
<PackageVersion Include="AutoQuery.Generator" Version="1.1.0" />
<PackageReference Include="AutoQuery.Generator" />
paket add AutoQuery.Generator --version 1.1.0
#r "nuget: AutoQuery.Generator, 1.1.0"
#:package AutoQuery.Generator@1.1.0
#addin nuget:?package=AutoQuery.Generator&version=1.1.0
#tool nuget:?package=AutoQuery.Generator&version=1.1.0
AutoQuery.Generator
๐ Documentation site ยท NuGet ยท Changelog
Compile-time query composition for IQueryable<T> via Roslyn incremental source generators.
Add [QuerySpec(typeof(Product))] to a partial query class and AutoQuery.Generator emits strongly-typed Apply(IQueryable<Product>), BindFromQuery(...), and FromQuery(...) methods at build time. No reflection. No runtime scanning. AOT-friendly.
using AutoQuery;
using System.Linq;
[QuerySpec(typeof(Product))]
public partial class ProductQuery
{
public string? Name { get; set; }
public decimal? MinPrice { get; set; }
public decimal? MaxPrice { get; set; }
[QuerySort] public string? SortBy { get; set; }
public bool SortDescending { get; set; }
public int PageNumber { get; set; } = 1;
public int PageSize { get; set; } = 20;
}
var filtered = new ProductQuery
{
Name = "Laptop",
MinPrice = 500,
SortBy = "Name",
PageNumber = 1,
PageSize = 10,
}.Apply(products);
// Or bind directly from HTTP query-string values:
var requestQuery = new Dictionary<string, string?>
{
["Name"] = "Laptop",
["MinPrice"] = "500",
["SortBy"] = "Name",
["PageNumber"] = "1",
["PageSize"] = "10"
};
var bound = ProductQuery.FromQuery(requestQuery);
var filteredFromQuery = bound.Apply(products);
Installation
dotnet add package AutoQuery.Generator
Targets netstandard2.0 and works with modern SDK-style .NET projects.
Quick start
1. Define an entity and query spec
using AutoQuery;
public sealed class Product
{
public string? Name { get; set; }
public decimal Price { get; set; }
public bool IsActive { get; set; }
}
[QuerySpec(typeof(Product))]
public partial class ProductQuery
{
public string? Name { get; set; }
public decimal? MinPrice { get; set; }
public bool? IsActive { get; set; }
}
2. Use the generated Apply method
IQueryable<Product> query = dbContext.Products;
var spec = new ProductQuery { Name = "Phone", MinPrice = 100, IsActive = true };
var result = spec.Apply(query);
3. Bind directly from query-string values
using AutoQuery;
[QuerySpec(typeof(Product))]
public partial class ProductQuery
{
public string? Name { get; set; }
public decimal? MinPrice { get; set; }
[QuerySort] public string? SortBy { get; set; }
public bool SortDescending { get; set; }
public int PageNumber { get; set; } = 1;
public int PageSize { get; set; } = 20;
}
// Works with Dictionary<string, string?>
var query = ProductQuery.FromQuery(new Dictionary<string, string?>
{
["Name"] = "Laptop",
["MinPrice"] = "500",
["SortBy"] = "Name",
["PageNumber"] = "1",
["PageSize"] = "10"
});
// Also works with ASP.NET Core Request.Query without taking an ASP.NET Core package dependency.
app.MapGet("/products", (AppDbContext db, HttpRequest request) =>
{
var spec = ProductQuery.FromQuery(request.Query);
return spec.Apply(db.Products);
});
Unknown keys are ignored, malformed values are skipped, and successful conversions use invariant culture for numeric/date parsing plus case-insensitive enum parsing.
Generated output resembles:
public partial class ProductQuery
{
public IQueryable<global::YourApp.Product> Apply(IQueryable<global::YourApp.Product> query)
{
if (Name is not null)
query = query.Where(x => x.Name != null && x.Name.Contains(Name));
if (MinPrice is not null)
query = query.Where(x => x.Price >= MinPrice.Value);
if (IsActive is not null)
query = query.Where(x => x.IsActive == IsActive.Value);
return query;
}
}
Attributes
[QuerySpec(typeof(TEntity))]
Marks a partial class as a query spec for the target entity.
[QuerySpec(typeof(Order))]
public partial class OrderQuery { }
[QueryFilter("x => x.Category.Name == value")]
Overrides the default convention and uses your custom LINQ predicate expression. The token value is replaced with the query property access.
[QueryFilter("x => x.Category.Name == value")]
public string? CategoryName { get; set; }
[QueryIgnore]
Skips a property entirely.
[QueryIgnore]
public string? DebugOnly { get; set; }
[QuerySort]
Marks a string? property as the requested sort field.
[QuerySort]
public string? SortBy { get; set; }
public bool SortDescending { get; set; }
[QueryPage]
Marks pagination properties when you are not using the conventional names PageNumber and PageSize.
[QueryPage] public int ResultsPageNumber { get; set; } = 1;
[QueryPage] public int ResultsPageSize { get; set; } = 25;
Convention-based filters
Nullable properties become filters automatically unless excluded.
| Spec property | Generated predicate |
|---|---|
string? Name |
x => x.Name != null && x.Name.Contains(Name) |
bool? IsActive |
x => x.IsActive == IsActive.Value |
int? CategoryId |
x => x.CategoryId == CategoryId.Value |
decimal? MinPrice |
x => x.Price >= MinPrice.Value |
decimal? MaxPrice |
x => x.Price <= MaxPrice.Value |
DateTime? CreatedFrom |
x => x.Created >= CreatedFrom.Value |
DateTime? CreatedTo |
x => x.Created <= CreatedTo.Value |
Prefix/suffix conventions:
Min...โ>=Max...โ<=...Fromโ>=...Toโ<=
Sorting
When the spec contains a [QuerySort] string property and a bool property named SortDescending or IsDescending, AutoQuery emits switch-based sorting.
[QuerySpec(typeof(Product))]
public partial class ProductQuery
{
public string? Name { get; set; }
public decimal? Price { get; set; }
[QuerySort] public string? SortBy { get; set; }
public bool SortDescending { get; set; }
}
Generated shape:
if (SortBy is not null)
{
query = (SortBy, SortDescending) switch
{
("Name", false) => query.OrderBy(x => x.Name),
("Name", true) => query.OrderByDescending(x => x.Name),
("Price", false) => query.OrderBy(x => x.Price),
("Price", true) => query.OrderByDescending(x => x.Price),
_ => query
};
}
Pagination
When PageNumber and PageSize are present (or [QueryPage] annotated equivalents are detected), AutoQuery emits:
query = query.Skip((PageNumber - 1) * PageSize).Take(PageSize);
Example:
[QuerySpec(typeof(Product))]
public partial class ProductQuery
{
public string? Name { get; set; }
public int PageNumber { get; set; } = 1;
public int PageSize { get; set; } = 20;
}
HTTP query-string binding
For every [QuerySpec] class with writable supported properties, AutoQuery emits:
public void BindFromQuery(IEnumerable<KeyValuePair<string, string?>> query);
public void BindFromQuery<TValue>(IEnumerable<KeyValuePair<string, TValue>> query)
where TValue : IEnumerable<string>;
public static ProductQuery FromQuery(IEnumerable<KeyValuePair<string, string?>> query);
public static ProductQuery FromQuery<TValue>(IEnumerable<KeyValuePair<string, TValue>> query)
where TValue : IEnumerable<string>;
Supported property conversions:
string- numeric types and nullable numeric types
bool/bool?DateTime/DateTime?- enums and nullable enums
The generic overload is what lets Request.Query bind cleanly: IQueryCollection enumerates as KeyValuePair<string, StringValues>, and StringValues implements IEnumerable<string>, so no AutoQuery runtime dependency on ASP.NET Core is required.
Diagnostics
| Id | Severity | Description |
|---|---|---|
AQ001 |
Error | [QuerySpec] entity type could not be resolved. |
AQ002 |
Error | [QuerySpec] target class must be declared partial. |
AQ003 |
Warning | Spec has no filterable properties. |
Comparison
| Capability | AutoQuery.Generator | Manual LINQ | Ardalis.Specification |
|---|---|---|---|
Compile-time generated Apply method |
โ | โ | โ |
| Compile-time generated query-string binding | โ | โ | โ |
| Reflection-free | โ | โ | Usually โ |
| Convention filters from DTO-like class | โ | โ | โ |
| Custom inline filter expressions | โ | Manual only | Via handwritten spec logic |
| Built-in sort switch generation | โ | Manual only | Manual only |
| Built-in pagination generation | โ | Manual only | Manual only |
| Runtime abstraction dependency | None | None | Package dependency |
Also by the same author
๐ Full suite overview: swevo.github.io
| Package | Description |
|---|---|
| AutoWire | Compile-time DI auto-registration โ [Scoped]/[Singleton]/[Transient] generates IServiceCollection code. Zero reflection. |
| AutoMap.Generator | Compile-time object mapping โ [Map(typeof(Dto))] generates ToDto() extension methods. Zero reflection, AOT-safe. |
| AutoValidate.Generator | Compile-time FluentValidation wiring โ discovers AbstractValidator<T> subclasses and generates AddValidators(). |
| AutoResult.Generator | Compile-time Result<T> monad โ [TryWrap] generates Try*() wrappers for sync, async and void methods. |
| AutoDispatch.Generator | Compile-time CQRS dispatcher โ [Handler] generates a strongly-typed IDispatcher. No IRequest<T>, no reflection. |
| AutoLog.Generator | Compile-time high-performance logging โ [Log(Level, Message)] on a partial method generates LoggerMessage.Define. AOT-safe. |
| AutoHttpClient.Generator | Compile-time typed HTTP client โ [HttpClient] on an interface generates a strongly-typed client. AOT-safe Refit alternative. |
License
MIT.
Learn more about Target Frameworks and .NET Standard.
-
.NETStandard 2.0
- 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.
Added generated FromQuery/BindFromQuery helpers for query-string binding without an ASP.NET Core runtime dependency.