JsonToLinq 1.0.0

dotnet add package JsonToLinq --version 1.0.0
                    
NuGet\Install-Package JsonToLinq -Version 1.0.0
                    
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="JsonToLinq" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="JsonToLinq" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="JsonToLinq" />
                    
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 JsonToLinq --version 1.0.0
                    
#r "nuget: JsonToLinq, 1.0.0"
                    
#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 JsonToLinq@1.0.0
                    
#: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=JsonToLinq&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=JsonToLinq&version=1.0.0
                    
Install as a Cake Tool

JsonToLinq

JsonToLinq - lightweight C# library that converts JSON-based query definitions into LINQ expressions. Ideal for building dynamic filters, predicates, and queries.

โšก TL;DR

Use JSON to build LINQ expressions!

๐Ÿคฉ Advantages

  1. Friendliness

    JSON is a simple and widely known format, easy to read not only for developers.

  2. Broad applicability

    JSON-based filters can be used in tests, specifications, test plans, and other technical documentation, lowering the entry barrier for readers.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

๐Ÿš€ Quick Start

  1. Install the NuGet package.
  2. Pass JSON filter into Where() or other filtering methods.
  3. To use with IQueryable, first create a predicate via JsonLinq.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\"")));

๐Ÿ› ๏ธ Operators

  1. Built-in operators and their handling logic are encapsulated in the ExpressionOperatorMapper class.
  2. The mapping between operators and their processing methods is available via the ExpressionOperatorMapper.Pairs property.
  3. Operator keys are case-sensitive.

๐Ÿ“Œ Built-in Operators

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
Negated Operators
  • !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

The word not is an alias for !.

It improves readability but is not suitable for all operators:

  1. Operators expressed as words become even longer.
  2. Using not is not always grammatically correct.
    • โŒ not contains
    • โœ… does not contain

๐Ÿ” SQL Translation

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.

๐Ÿ”  Case Sensitivity and Normalization

  1. String comparisons are case-sensitive, depending on the database collation, not on the operators.
  2. The case of string values specified in the filter is never changed automatically.
  3. Case normalization is the clientโ€™s responsibility.
  4. Operators with as lower / as upper apply case transformation to the expression being evaluated, not to the filter value. The filter value is used exactly as provided.

๐Ÿ“ฆ Filter Collections

  1. A filter collection can be empty, but it is never null.
  2. A filter collection and its elements are never automatically changed.
  3. Operators with as lower / as upper apply string transformations to each element in the source collection before evaluation, leaving the filter collection elements unchanged.

๐ŸŒŸ Add Custom Operators

Fully Custom Operators
JsonLinq.Configure(options =>
{
  options.OperatorMapper = new ExpressionOperatorMapper()
    .Add("=", Expression.Equal)
      .WithAliases("eq", "EQ")
    .AddNot("!=", "=")
      .WithAliases("neq", "NEQ")
    .AddAlias("==", "=")
      .WithNot("<>");
});
Extend Default Operators
JsonLinq.Configure(options =>
{
  options.OperatorMapper = ExpressionOperatorMapper.OnDefault()
    .Add(...
});
Negated Operators

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"
SQL Operators

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));
});

๐ŸŽ› Configuration

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.

๐Ÿงช Testing

Unit tests cover:

  1. Everything involved in parsing JSON filters.
  2. Operators with custom expressions (in, contains, etc.).
  3. Configuration methods.
  4. IEnumerable extension methods.

Full unit test coverage will be relevant after the library has been used in real projects.

๐Ÿ”ฌ Demos and Experiments

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:

๐Ÿ”ค Filter Notation

Syntax
expr = logic[expr(, expr)*]
  • expr - a single rule or a combination of rules
  • logic - an operator used to combine multiple rules, e.g. &&, &, ||, |, or a custom one
Examples
  1. &&[x = null]
  2. &&[a < 0, b > 0]
  3. &&[x = null, ||[a < 0, b > 0]]

๐Ÿ’ป Demo Project

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.

โ–ถ๏ธ Before running the demos, select the first menu item, Prepare Data, to apply migrations and populate the tables with test data.

๐Ÿšง Limitations

  1. No IDE syntax highlighting for JSON filter arguments in LINQ methods

    The library targets netstandard2.1, which does not support StringSyntaxAttribute. Care is needed when writing filters manually. In practice, this is not critical, as filters typically come from client applications.

  2. No IntelliSense for JSON filters

    Without suggestions, it is harder to ensure filter correctness.

  3. 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.

๐Ÿ”ฎ Potential

Filtering is just the first stage in the development of JSON-LINQ infrastructure. Possible future directions include:

  1. Data selecting - JSON for Select()
  2. Data grouping - JSON for GroupBy()
  3. GraphQL engine using standard JSON (as opposed to HotChocolate)
  4. Server-side equivalents of RxJS/NgRx for reactive data processing
  5. Interactive query-building studios - visual builders with canvas, drag-and-drop, flowcharts, node-based UI, etc.
  6. Semantic search and NLP
  7. A new standard for data exchange between services
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 netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.1 is compatible. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen 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.

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
1.0.0 96 1/3/2026
1.0.0-rc923 91 1/2/2026
1.0.0-rc922 86 1/2/2026
1.0.0-rc921 84 1/2/2026
1.0.0-rc920 85 1/2/2026
1.0.0-rc918 82 1/2/2026
1.0.0-rc917 87 1/2/2026
1.0.0-rc916 121 12/20/2025
1.0.0-rc915 131 12/20/2025
1.0.0-rc914 173 12/19/2025
1.0.0-rc913 231 12/19/2025
1.0.0-rc912 236 12/19/2025
1.0.0-rc911 264 12/18/2025
1.0.0-rc910 271 12/17/2025
1.0.0-rc909 262 12/17/2025
1.0.0-rc908 114 12/13/2025
1.0.0-rc907 114 12/13/2025
1.0.0-rc906 117 12/12/2025
1.0.0-rc905 405 12/11/2025
1.0.0-rc904 443 12/9/2025
1.0.0-rc903 142 12/6/2025
1.0.0-rc901 140 12/6/2025
1.0.0-rc9 671 12/2/2025
1.0.0-rc8 264 11/30/2025
1.0.0-rc7 259 11/30/2025
1.0.0-rc6 260 11/30/2025
1.0.0-rc5 258 11/30/2025
1.0.0-rc4 208 11/9/2025
1.0.0-rc3 162 11/2/2025
1.0.0-rc2 177 11/2/2025
1.0.0-rc13 189 12/5/2025
1.0.0-rc12 188 12/5/2025
1.0.0-rc11 184 12/5/2025
1.0.0-rc10 188 12/5/2025
1.0.0-rc1 133 11/1/2025