Skip to content

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

Notifications You must be signed in to change notification settings

neomasterhub/Neomaster.JsonToLinq

Repository files navigation


License Telegram Channel .NET Version
Habr NuGet

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!

Table of Contents

  1. Advantages
  2. Quick Start
  3. Operators
  4. Configuration
  5. Testing
  6. Demos and Experiments
  7. Limitations
  8. Potential

๐Ÿคฉ 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.

Demo project

๐Ÿšง 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

About

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

Topics

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages