Muslim.Filter
10.0.1
dotnet add package Muslim.Filter --version 10.0.1
NuGet\Install-Package Muslim.Filter -Version 10.0.1
<PackageReference Include="Muslim.Filter" Version="10.0.1" />
<PackageVersion Include="Muslim.Filter" Version="10.0.1" />
<PackageReference Include="Muslim.Filter" />
paket add Muslim.Filter --version 10.0.1
#r "nuget: Muslim.Filter, 10.0.1"
#:package Muslim.Filter@10.0.1
#addin nuget:?package=Muslim.Filter&version=10.0.1
#tool nuget:?package=Muslim.Filter&version=10.0.1
Muslim.Filter
Filtering, sorting and paging for IQueryable<T>, driven by whatever the client actually sent.
Declare one filter per row type, register a predicate for each supplied value, and get a page back with its total count.
dotnet add package Muslim.Filter
No dependencies beyond the framework. Works with EF Core, LINQ to Objects, or any provider.
Defining a filter
Mark the row type with IHaveFilter, then derive from Filter<T>:
public class User : IHaveFilter
{
public int Id { get; set; }
public string? Name { get; set; }
public string? Code { get; set; }
public int BranchId { get; set; }
public Branch? Branch { get; set; }
}
public class Branch
{
public int Id { get; set; }
public string? Name { get; set; }
}
public class UserFilter : Filter<User>
{
public string? Name { get; set; }
public int? BranchId { get; set; }
public string? Term { get; set; }
protected override void ApplyFilter()
{
WhereIfNotEmpty(Name, name => x => x.Name!.Contains(name));
WhereIfHasValue(BranchId, id => x => x.BranchId == id);
Search(Term, x => x.Name, x => x.Code, x => x.Branch!.Name);
}
}
A property left null contributes no predicate, so the same filter serves every combination of criteria the client sends.
Bind it straight from the query string — PageIndex, PageSize, SortColumn and SortDirection
are inherited properties.
Running it
var page = filter.ToPagedResult(repository.QueryAsNoTracking);
return new
{
page.Items, // this page
page.TotalCount, // rows matching the filter, ignoring paging
page.TotalPages,
page.HasNext
};
ToPagedResult(query) |
one page plus totals — two queries, a COUNT and the page |
Apply(query) |
predicates, then ordering, then paging; returns the composed IQueryable |
ApplyFilterOnly(query) |
predicates only, no ordering or paging |
ToExpression() |
the combined predicate, for a repository Count/Any overload |
Nothing executes until you enumerate, so Apply can be composed further.
Registering predicates
AddFilter(condition, predicate) |
the original form; still supported unchanged |
WhereIf(condition, predicate) |
same thing, reads better |
WhereIfNotEmpty(text, build) |
runs only when the string has content, and hands you the non-null value |
WhereIfHasValue(nullable, build) |
runs only when the nullable has one, and hands you the unwrapped value |
Search(term, ...columns) |
one term across several text columns, joined with || |
WhereIfNotEmpty and WhereIfHasValue pass the unwrapped value into the lambda, so the predicate
needs no null-forgiving !:
AddFilter(!string.IsNullOrEmpty(Name), x => x.Name!.Contains(Name!)); // before
WhereIfNotEmpty(Name, name => x => x.Name!.Contains(name)); // after
Search skips null columns — including a column reached through a null navigation such as
x.Branch!.Name — rather than throwing, and adds nothing when the term is empty. Under EF Core the
term is sent as a SQL parameter. Case sensitivity is the provider's: the database collation, or
ordinal comparison in memory.
Fluent form
AndIf and OrIf build a single expression instead of a list — use them when you need ||
between whole clauses:
protected override void ApplyFilter()
{
AndIf(BranchId.HasValue, x => x.BranchId == BranchId);
OrIf(IncludeArchived, x => x.IsArchived);
}
Both forms can be mixed; everything registered is combined.
Sorting
SortColumn accepts three forms:
filter.SortColumn = "Name"; // uses SortDirection
filter.SortColumn = "Customer.Name"; // walks navigation properties
filter.SortColumn = "BranchId asc, Name desc"; // several columns, each with its own direction
Column names are matched case-insensitively against public properties, preferring an exact-case
match and then the most derived declaration (a property hidden with new). An unknown column is
ignored rather than throwing, so a stale grid state cannot break the request — and a typo silently
returns unordered rows. Set IsApplySorting = false to skip ordering entirely.
Paging
PageIndex is 1-based; values below 1 are treated as 1, and a page past the end returns no rows
however large the index. Set IsApplyPagination = false to return everything.
MaxPageSize caps what a request may ask for. It defaults to 0, meaning no limit — set it on
anything a client can reach, since PageSize normally arrives straight off the query string and an
uncapped request for a million rows is honoured.
Defaults
Configure once at startup. Binding from IConfiguration is the reliable route, because it honours
environment overrides, environment variables and user secrets:
FilterDefaults.Configure(
builder.Configuration.GetSection("DefaultFilterValue").Get<DefaultFilterValue>()
?? new DefaultFilterValue());
Or set them directly:
FilterDefaults.Configure(d =>
{
d.PageSize = 20;
d.MaxPageSize = 200;
});
If you configure nothing, DefaultFilterValue is read from appsettings.json, as in earlier
versions:
{
"DefaultFilterValue": {
"PageIndex": 1,
"PageSize": 20,
"MaxPageSize": 200,
"SortColumn": "CreatedDate",
"SortDirection": "Desc",
"ApplyPagination": true,
"ApplySorting": true
}
}
That path reads the file off disk directly, so it cannot see anything supplied through
IConfiguration. It accepts what the configuration JSON provider accepts — comments, trailing
commas, any key casing, and "true"/"20" written as strings — and keeps the built-in default for
any value it cannot read.
Composing predicates
And, Or and Not join predicates into one lambda over a single parameter, which EF Core can
translate to SQL:
Expression<Func<User, bool>> active = x => x.IsActive;
var query = repository.Query.Where(active.And(x => x.BranchId == id));
Reusing a filter object
A filter can be run more than once. Each run rebuilds its predicates from scratch, so calling
Apply twice, or ApplyFilterOnly then Apply, gives the same result both times.
Inside ApplyFilter, the protected Query property holds the query of the current run under
Apply, ApplyFilterOnly and ToPagedResult, and is null under ToExpression.
Async with EF Core
ToPagedResult executes synchronously. With EF Core's async operators:
var total = await filter.ApplyFilterOnly(db.Users.AsNoTracking()).CountAsync(ct);
var items = await filter.Apply(db.Users.AsNoTracking()).ToListAsync(ct);
Related packages
| Package | Use it for |
|---|---|
| Muslim.EntityAndDto | Base entity and DTO classes |
| Muslim.Result | One result type for every API response |
Coding-agent guidance
When you build a project that uses this package, a short guide for AI coding agents is copied to
.agents/skills/muslim-filter/ at the root of your repository. Agents that read skills from
.agents/skills, such as Codex and GitHub Copilot in VS Code, use it to write correct code against
this package. Your own files and other skills are never touched, and nothing is deleted
automatically.
To turn it off, add this to your project or Directory.Build.props:
<PropertyGroup>
<MuslimFilterGenerateSkill>false</MuslimFilterGenerateSkill>
</PropertyGroup>
License
See LICENSE.txt.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. 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. |
-
net10.0
- No dependencies.
NuGet packages (3)
Showing the top 3 NuGet packages that depend on Muslim.Filter:
| Package | Downloads |
|---|---|
|
Muslim.EntityAndDto
Shared entity, audit and DTO contracts for .NET applications. Includes repository-scoped agent guidance generated on consumer builds. |
|
|
Muslim.GenericRepository.Interface
Repository abstraction over EF Core: IRepository<TEntity> and the interfaces it composes — reads, writes, soft delete, bulk operations, counts and raw SQL. Reference this to depend on the contract without the implementation. |
|
|
Muslim.GenericService.Interface
Service abstraction for Muslim.GenericService: IService<TEntity> and the interfaces it composes — reads, finds with paging, create, update, delete, soft delete and restore, counts and raw SQL — all returning Muslim.Result Result<T>. Reference this to depend on the contract without the implementation. |
GitHub repositories
This package is not used by any popular GitHub repositories.
Fixes a large PageIndex overflowing into a negative offset (first page returned in memory, SQL error on EF Core), Search throwing on a null navigation in memory and inlining the search term into SQL, sorting failing on properties hidden with new or differing only by case, and appsettings.json defaults being ignored when the file has comments, trailing commas or different key casing. ApplyFilterOnly now sets Query. Ships version-matched agent guidance and generates .agents/skills/muslim-filter during normal consuming builds; preserves user files and supports MuslimFilterGenerateSkill=false. See CHANGELOG.md and README.md.