JsonToLinq - lightweight C# library that converts JSON-based query definitions into LINQ expressions. Ideal for building dynamic filters, predicates, and queries.
Use JSON to build LINQ expressions!
-
Friendliness
JSON is a simple and widely known format, easy to read not only for developers.
-
Broad applicability
JSON-based filters can be used in tests, specifications, test plans, and other technical documentation, lowering the entry barrier for readers.
-
Universality
To retrieve data from a .NET application using EF, it is enough to request it in JSON format. This makes the approach accessible to any client that knows the field names and their types.
-
Client-server independence
To build filters, the client only needs to know the names and types of data fields, which are usually already present in DTOs.
-
Flexibility
Custom operators can be added out of the box, and the core functionality can serve as a foundation for other projects - for example, building an alternative to HotChocolate with standard JSON and different design choices.
-
Simplicity
Install the NuGet package and pass JSON filters into
Where()and other filtering methods. This enables not a minimal subset, but the full functionality.No need to:
- Register anything in
Program.cs. - Create schemas, framework-specific DTOs, filters, resolvers, etc.
- Generate a schema for the client.
Everything required for filtering is already contained in the DTOs.
- Register anything in
- Install the NuGet package.
- Pass JSON filter into
Where()or other filtering methods. - To use with
IQueryable, first create a predicate viaJsonLinq.ParseFilterExpression().
using Neomaster.JsonToLinq;
var users = source.Where(
"""
{
"Logic": "&&",
"Rules": [
{ "Field": "balance", "Operator": "=", "Value": 0 },
{ "Field": "status", "Operator": "in", "Value": [ 1, 3 ] },
{ "Field": "country", "Operator": "as lower contains", "Value": "islands" },
{
"Logic": "||",
"Rules": [
{ "Field": "lastVisitAt", "Operator": "=", "Value": null },
{ "Field": "lastVisitAt", "Operator": "<=", "Value": "2026-01-01T00:00:00Z" }
]
}
]
}
""");Equivalent LINQ query:
var users = source.Where(u =>
(u.Balance == 0
&& new[] { 1, 3 }.Contains(u.Status)
&& u.Country.ToLower().Contains("islands"))
&&
(u.LastVisitAt == null
|| u.LastVisitAt <= JsonSerializer.Deserialize<DateTime?>("\"2026-01-01T00:00:00Z\"")));- Built-in operators and their handling logic
are encapsulated in the
ExpressionOperatorMapperclass. - The mapping between operators and their processing methods
is available via the
ExpressionOperatorMapper.Pairsproperty. - Operator keys are case-sensitive.
| Operator | Description |
|---|---|
& |
Bitwise AND |
| |
Bitwise OR |
&& / and |
Logical AND |
|| / or |
Logical OR |
= / eq |
Equal |
!= / neq |
Not equal |
> / gt |
Greater than |
>= / gte |
Greater than or equal |
< / lt |
Less than |
<= / lte |
Less than or equal |
in |
In collection |
as lower in |
Element lower-cased in collection |
as upper in |
Element upper-cased in collection |
contains |
Contains substring |
as lower contains |
Element lower-cased contains substring |
as upper contains |
Element upper-cased contains substring |
starts with |
Starts with |
as lower starts with |
Element lower-cased starts with |
as upper starts with |
Element upper-cased starts with |
ends with |
Ends with |
as lower ends with |
Element lower-cased ends with |
as upper ends with |
Element upper-cased ends with |
!in/not in! as lower in! as upper in!contains! as lower contains! as upper contains! starts with! as lower starts with! as upper starts with! ends with! as lower ends with! as upper ends with
The word not is an alias for !.
It improves readability but is not suitable for all operators:
- Operators expressed as words become even longer.
- Using
notis not always grammatically correct.- โ not contains
- โ does not contain
All built-in operators are designed to be translatable by LINQ providers (e.g. Entity Framework Core) and do not rely on client-side evaluation.
- String comparisons are case-sensitive, depending on the database collation, not on the operators.
- The case of string values specified in the filter is never changed automatically.
- Case normalization is the clientโs responsibility.
- Operators with
as lower/as upperapply case transformation to the expression being evaluated, not to the filter value. The filter value is used exactly as provided.
- A filter collection can be empty, but it is never
null. - A filter collection and its elements are never automatically changed.
- Operators with
as lower/as upperapply string transformations to each element in the source collection before evaluation, leaving the filter collection elements unchanged.
JsonLinq.Configure(options =>
{
options.OperatorMapper = new ExpressionOperatorMapper()
.Add("=", Expression.Equal)
.WithAliases("eq", "EQ")
.AddNot("!=", "=")
.WithAliases("neq", "NEQ")
.AddAlias("==", "=")
.WithNot("<>");
});JsonLinq.Configure(options =>
{
options.OperatorMapper = ExpressionOperatorMapper.OnDefault()
.Add(...
});Negated operators can be defined without explicitly providing a key.
In this case, the key is automatically generated using the NegatedKeyProvider.
This provider can be set via SetNegatedKeyProvider().
new ExpressionOperatorMapper()
// Default provider
.Add("a", ...).WithNot() // "!a"
.Add("b c", ...).WithNot() // "! b c"
// Custom provider
.SetNegatedKeyProvider(key => (key.Contains(' ') ? "~ " : "~") + key)
.Add("x", ...).WithNot() // "~x"
.Add("y z", ...).WithNot() // "~ y z"The library is built on netstandard2.1 and does not depend on EF or any other ORM.
To perform case-insensitive string comparisons,
use the built-in operators with as lower or as upper.
They differ only in the preferred case for filter values.
JsonLinq.Configure(options =>
{
options.OperatorMapper = ExpressionOperatorMapper.OnDefault()
.Add("like", (element, pattern) =>
Expression.Call(
typeof(DbFunctionsExtensions).GetMethod(
nameof(DbFunctionsExtensions.Like),
[typeof(DbFunctions), typeof(string), typeof(string)]),
Expression.Constant(EF.Functions),
element,
pattern))
.Add("ilike", (element, pattern) =>
Expression.Call(
typeof(NpgsqlDbFunctionsExtensions).GetMethod(
nameof(NpgsqlDbFunctionsExtensions.ILike),
[typeof(DbFunctions), typeof(string), typeof(string)]),
Expression.Constant(EF.Functions),
element,
pattern));
});You can define your own settings via JsonLinq.Configure().
To reset the configuration, call JsonLinq.ResetConfiguration().
This is necessary for testing.
JsonLinq.Configure(options =>
{
// Property names in JSON filters
options.LogicOperatorPropertyName = "๐";
options.RulesPropertyName = "โ๏ธ";
options.OperatorPropertyName = "โก";
options.FieldPropertyName = "๐";
options.ValuePropertyName = "๐ฌ";
// Operator definitions and aliases
options.OperatorMapper = ExpressionOperatorMapper.OnDefault()
.AddAlias("does not contain", "!contains");
// Handling logic for expressions with null
options.BindBuilder = ExpressionBindBuilders.NullAsFalse;
// How C# property names appear in JSON
options.ConvertPropertyNameForJson = JsonNamingPolicy.SnakeCaseUpper.ConvertName;
});The JSON filter structure settings are relevant.
For example, adding syntactic sugar:
"Logic": "&&", "Rules": [...] โ "&&": [...]
Or specifying the property order to support TONL filters.
This may be implemented in future versions.
Unit tests cover:
- Everything involved in parsing JSON filters.
- Operators with custom expressions (
in,contains, etc.). - Configuration methods.
IEnumerableextension methods.
Full unit test coverage will be relevant after the library has been used in real projects.
This repository includes the JsonToLinq.Demo project with working examples. You are welcome to submit a PR with your own examples, bug reports, or new features. To describe a filter, use the following notation:
expr = logic[expr(, expr)*]
expr- a single rule or a combination of ruleslogic- an operator used to combine multiple rules, e.g.&&,&,||,|, or a custom one
&&[x = null]&&[a < 0, b > 0]&&[x = null, ||[a < 0, b > 0]]
This project provides examples of working with EF Core and a PostgreSQL database.
๐งช A real database is used instead of in-memory storage, ensuring clean and realistic experiments.
โ You can add your own examples with other databases or ORMs via a PR.
-
No IDE syntax highlighting for JSON filter arguments in LINQ methods
The library targets
netstandard2.1, which does not supportStringSyntaxAttribute. Care is needed when writing filters manually. In practice, this is not critical, as filters typically come from client applications. -
No IntelliSense for JSON filters
Without suggestions, it is harder to ensure filter correctness.
-
Aggregate fields are not supported
For now, it is recommended to use flat DTOs and database views.
{ "Rules": [ { "Field": "โ user_id", "Operator": "=", "Value": 123 }, { "Field": "โ user.id", "Operator": "=", "Value": 123 } ] }These features may be implemented in future versions.
Filtering is just the first stage in the development of JSON-LINQ infrastructure. Possible future directions include:
- Data selecting - JSON for
Select() - Data grouping - JSON for
GroupBy() - GraphQL engine using standard JSON (as opposed to HotChocolate)
- Server-side equivalents of RxJS/NgRx for reactive data processing
- Interactive query-building studios - visual builders with canvas, drag-and-drop, flowcharts, node-based UI, etc.
- Semantic search and NLP
- A new standard for data exchange between services
