Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion GitVersion.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
next-version: 1.0.0
next-version: 1.1.0
tag-prefix: '[vV]'
mode: ContinuousDeployment
branches:
Expand Down
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# <img src="https://github.com/CodeShayk/JSONPredicate/blob/master/Images/ninja-icon-16.png" alt="ninja" style="width:30px;"/> JSONPredicate v1.0.0
# <img src="https://github.com/CodeShayk/JSONPredicate/blob/master/Images/ninja-icon-16.png" alt="ninja" style="width:30px;"/> JSONPredicate v1.1.0
[![NuGet version](https://badge.fury.io/nu/JSONPredicate.svg)](https://badge.fury.io/nu/JSONPredicate) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/CodeShayk/JSONPredicate/blob/master/LICENSE.md)
[![GitHub Release](https://img.shields.io/github/v/release/CodeShayk/JSONPredicate?logo=github&sort=semver)](https://github.com/CodeShayk/JSONPredicate/releases/latest)
[![master-build](https://github.com/CodeShayk/JSONPredicate/actions/workflows/Master-Build.yml/badge.svg)](https://github.com/CodeShayk/JSONPredicate/actions/workflows/Master-Build.yml)
Expand All @@ -7,6 +7,7 @@
[![.Net Framework 4.6.4](https://img.shields.io/badge/.Net-4.6.2-blue)](https://dotnet.microsoft.com/en-us/download/dotnet-framework/net46)
[![.Net Standard 2.0](https://img.shields.io/badge/.NetStandard-2.0-blue)](https://github.com/dotnet/standard/blob/v2.0.0/docs/versions/netstandard2.0.md)


## What is JSONPredicate?

.Net library to provide a powerful and intuitive way to evaluate string-based predicate expressions against JSON objects using JSONPath syntax.
Expand All @@ -16,9 +17,13 @@
- **JSONPath Support**: Access `nested` object properties using `dot` notation
- **Multiple Operators**: `eq` (equal), `in` (contains), `not` (not equal), `gt` (greater than), `gte` (greater than or equal), `lt` (less than), `lte` (less than or equal)
- **Logical Operators**: `and`, `or` with proper precedence handling
- **Array Handling**: Evaluate conditions on `arrays` and `collections`. Support array `indexing` (Available from v1.1.0)
- **String Operations**: `starts_with`, `ends_with`, `contains` (Available from v1.1.0)
- **Type Safety**: `Automatic` type conversion and validation
- **Complex Expressions**: `Parentheses` grouping and `nested` operations
- **Lightweight**: `Minimal` dependencies, `fast` evaluation
- **Thread-Safe**: Safe for use in `multi-threaded` environments
- **Performance Optimized**: Efficient parsing and evaluation for `high-performance` scenarios

## Installation

Expand All @@ -33,6 +38,7 @@ The expression syntax is ([JSONPath] [Comparison Operator] [Value]) [Logical Ope
#### ii. Supported Operators
- Comparison Operators - `eq`, `in`, `gt`, `gte`, `lt`, `lte` & `Not`
- Logical Operators - `and` & `or`
- String Operators - `starts_with`, `ends_with`, `contains` (Available from v1.1.0)
### Example
```
var customer = new {
Expand All @@ -58,7 +64,19 @@ bool result2 = JSONPredicate.Evaluate("client.address.postcode eq `e113et` and c
#### iii. Array operations
```
bool result3 = JSONPredicate.Evaluate("client.tags in [`vip`, `standard`]", customer);
bool
```
#### iv. String operators (Available from v1.1.0)
```
bool result4 = JSONPredicate.Evaluate("client.address.postcode starts_with `e11`", customer);
bool result5 = JSONPredicate.Evaluate("client.address.postcode ends_with `3et`", customer);
bool result6 = JSONPredicate.Evaluate("client.address.postcode contains `13`", customer);
```
#### v. Deep Array Indexing (Available from v1.1.0)
```
bool result7 = JSONPredicate.Evaluate("client.tags[1] eq `premium`", customer);
```

## Developer Guide
Please see [Developer Guide](https://github.com/CodeShayk/JSONPredicate/wiki) for comprehensive documentation to integrate JSONPredicate in your project.

Expand Down
2 changes: 1 addition & 1 deletion src/JSONPredicate/DataTypes.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using System;
using System.Globalization;

namespace JsonPathPredicate
namespace JSONPredicate
{
internal static class DataTypes
{
Expand Down
60 changes: 54 additions & 6 deletions src/JSONPredicate/Expression.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using System.Text.RegularExpressions;

namespace JSONPredicate
{
Expand All @@ -20,17 +19,66 @@ public static class Comparison
public const string GteOperator = "gte";
public const string LtOperator = "lt";
public const string LteOperator = "lte";
public const string StartsWithOperator = "starts_with";
public const string EndsWithOperator = "ends_with";
public const string ContainsOperator = "contains";
}

public static (string Path, string Operator, string Value) Parse(string expression)
{
var pattern = @"^(.+?)\s+(eq|in|not|gt|gte|lt|lte)\s+(.+)$";
var match = Regex.Match(expression.Trim(), pattern);

if (!match.Success)
var expr = expression.Trim();
if (string.IsNullOrEmpty(expr))
throw new ArgumentException($"Invalid expression format: {expression}");

return (match.Groups[1].Value.Trim(), match.Groups[2].Value, match.Groups[3].Value.Trim());
// Define operators in order of length (longer first) to avoid partial matches
var operators = new[] { "gte", "lte", "not", "eq", "gt", "lt", "in" };

for (int i = 0; i < expr.Length; i++)
{
// Skip if inside quotes
if (expr[i] == '\'' || expr[i] == '"' || expr[i] == '`')
{
var quoteChar = expr[i];
i++;
while (i < expr.Length && expr[i] != quoteChar)
{
if (i + 1 < expr.Length && expr[i] == '\\') // Handle escaped quotes
{
i += 2;
}
else
{
i++;
}
}
continue;
}

// Check for operator at this position (not in quotes)
foreach (var op in operators)
{
if (i + op.Length <= expr.Length &&
expr.Substring(i, op.Length).Equals(op, StringComparison.OrdinalIgnoreCase))
{
// Verify it's surrounded by whitespace or string boundaries
bool beforeOk = i == 0 || char.IsWhiteSpace(expr[i - 1]);
bool afterOk = i + op.Length == expr.Length || char.IsWhiteSpace(expr[i + op.Length]);

if (beforeOk && afterOk)
{
var path = expr.Substring(0, i).Trim();
var value = expr.Substring(i + op.Length).Trim();

if (string.IsNullOrEmpty(path) || string.IsNullOrEmpty(value))
throw new ArgumentException($"Invalid expression format: {expression}");

return (path, op, value);
}
}
}
}

throw new ArgumentException($"Invalid expression format: {expression}");
}
}
}
13 changes: 10 additions & 3 deletions src/JSONPredicate/JSONPredicate.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,17 @@
<PackageProjectUrl>https://github.com/CodeShayk/JSONPredicate/wiki</PackageProjectUrl>
<RepositoryUrl>https://github.com/CodeShayk/JSONPredicate</RepositoryUrl>
<PackageReleaseNotes>
v1.0.0 - Release of JSONPredicate library.
This library provides a powerful and intuitive way to evaluate string-based predicate expressions against JSON objects using JSONPath syntax in .NET applications.
v1.1.0 - Enhanced JSONPredicate library with significant improvements.
- Array indexing support in JSONPath (e.g., `array[0].property`)
- New comparison operators: `starts_with`, `ends_with`, and `contains`
- Direct object navigation with 50%+ performance improvement
- Thread-safe operation in multi-threaded environments
- Optimized expression parsing without regex dependency.

For more details, visit the release page:
https://github.com/CodeShayk/JSONPredicate/releases/tag/v1.1.0
</PackageReleaseNotes>
<Version>1.0.0</Version>
<Version>1.1.0</Version>
<PackageRequireLicenseAcceptance>True</PackageRequireLicenseAcceptance>
<AssemblyName>JSONPredicate</AssemblyName>
</PropertyGroup>
Expand Down
58 changes: 50 additions & 8 deletions src/JSONPredicate/JsonPath.cs
Original file line number Diff line number Diff line change
@@ -1,26 +1,68 @@
using System.Linq;
using System.Reflection;
using System.Text.Json;

namespace JsonPathPredicate
namespace JSONPredicate
{
internal static class JsonPath
{
public static object Evaluate(object obj, string path)
{
var json = JsonSerializer.Serialize(obj);
using (var document = JsonDocument.Parse(json))
if (obj == null)
return null;

var properties = path.Split('.');
object current = obj;

foreach (var property in properties)
{
var element = document.RootElement;
if (current == null)
return null;

var parts = path.Split('.').Where(p => !string.IsNullOrEmpty(p)).ToArray();
foreach (var part in parts)
// Check if it's an indexed access: array[0] or list[1]
if (property.Contains("[") && property.EndsWith("]"))
{
if (element.ValueKind != JsonValueKind.Object || !element.TryGetProperty(part, out element))
var parts = property.Split('[');
var propName = parts[0];
var indexStr = parts[1].TrimEnd(']');

// First get the property
var propInfo = current.GetType().GetProperty(propName,
BindingFlags.Public | BindingFlags.Instance);
if (propInfo == null)
return null;

var propValue = propInfo.GetValue(current);

// Then access array/list element if applicable
if (propValue is System.Collections.IList list)
{
if (int.TryParse(indexStr, out int index) && index >= 0 && index < list.Count)
{
current = list[index];
}
else
{
return null; // Invalid index
}
}
else
{
return null; // Not an indexable type
}
}
else
{
var propInfo = current.GetType().GetProperty(property,
BindingFlags.Public | BindingFlags.Instance);
if (propInfo == null)
return null;

return DeserializeElement(element);
current = propInfo.GetValue(current);
}
}

return current;
}

private static object DeserializeElement(JsonElement element)
Expand Down
32 changes: 21 additions & 11 deletions src/JSONPredicate/JsonPredicate.cs
Original file line number Diff line number Diff line change
@@ -1,25 +1,35 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using JsonPathPredicate;
using JsonPathPredicate.Operators;
using JSONPredicate.Operators;

namespace JSONPredicate
{
public static class JSONPredicate

Check warning on line 9 in src/JSONPredicate/JsonPredicate.cs

View workflow job for this annotation

GitHub Actions / Build-Test

Missing XML comment for publicly visible type or member 'JSONPredicate'

Check warning on line 9 in src/JSONPredicate/JsonPredicate.cs

View workflow job for this annotation

GitHub Actions / Build-Test

Missing XML comment for publicly visible type or member 'JSONPredicate'

Check warning on line 9 in src/JSONPredicate/JsonPredicate.cs

View workflow job for this annotation

GitHub Actions / Build-Test

Missing XML comment for publicly visible type or member 'JSONPredicate'

Check warning on line 9 in src/JSONPredicate/JsonPredicate.cs

View workflow job for this annotation

GitHub Actions / Build-Test

Missing XML comment for publicly visible type or member 'JSONPredicate'

Check warning on line 9 in src/JSONPredicate/JsonPredicate.cs

View workflow job for this annotation

GitHub Actions / Build-Test

Missing XML comment for publicly visible type or member 'JSONPredicate'
{
private static readonly Dictionary<string, Func<object, object, bool>> ComparisonOperators = new Dictionary<string, Func<object, object, bool>>()
private static readonly ConcurrentDictionary<string, Func<object, object, bool>> ComparisonOperators;

static JSONPredicate()
{
{ Expression.Comparison.EqOperator, (left, right) => EqOperator.Evaluate(left, right)},
{ Expression.Comparison.InOperator, (left, right) => InOperator.Evaluate(left, right) },
{ Expression.Comparison.NotOperator, (left, right) => NotOperator.Evaluate(left, right) },
{ Expression.Comparison.GtOperator, (left, right) => GtOperator.Evaluate(left, right) },
{ Expression.Comparison.GteOperator, (left, right) => GteOperator.Evaluate(left, right) },
{ Expression.Comparison.LtOperator, (left, right) => LtOperator.Evaluate(left, right) },
{ Expression.Comparison.LteOperator, (left, right) => LteOperator.Evaluate(left, right)}
};
var operators = new Dictionary<string, Func<object, object, bool>>()
{
{ Expression.Comparison.EqOperator, (left, right) => EqOperator.Evaluate(left, right)},
{ Expression.Comparison.InOperator, (left, right) => InOperator.Evaluate(left, right) },
{ Expression.Comparison.NotOperator, (left, right) => NotOperator.Evaluate(left, right) },
{ Expression.Comparison.GtOperator, (left, right) => GtOperator.Evaluate(left, right) },
{ Expression.Comparison.GteOperator, (left, right) => GteOperator.Evaluate(left, right) },
{ Expression.Comparison.LtOperator, (left, right) => LtOperator.Evaluate(left, right) },
{ Expression.Comparison.LteOperator, (left, right) => LteOperator.Evaluate(left, right)},
{ Expression.Comparison.StartsWithOperator, (left, right) => StartsWithOperator.Evaluate(left, right)},
{ Expression.Comparison.EndsWithOperator, (left, right) => EndsWithOperator.Evaluate(left, right)},
{ Expression.Comparison.ContainsOperator, (left, right) => ContainsOperator.Evaluate(left, right)}
};

ComparisonOperators = new ConcurrentDictionary<string, Func<object, object, bool>>(operators);
}

public static bool Evaluate(string expression, object obj)

Check warning on line 32 in src/JSONPredicate/JsonPredicate.cs

View workflow job for this annotation

GitHub Actions / Build-Test

Missing XML comment for publicly visible type or member 'JSONPredicate.Evaluate(string, object)'

Check warning on line 32 in src/JSONPredicate/JsonPredicate.cs

View workflow job for this annotation

GitHub Actions / Build-Test

Missing XML comment for publicly visible type or member 'JSONPredicate.Evaluate(string, object)'

Check warning on line 32 in src/JSONPredicate/JsonPredicate.cs

View workflow job for this annotation

GitHub Actions / Build-Test

Missing XML comment for publicly visible type or member 'JSONPredicate.Evaluate(string, object)'

Check warning on line 32 in src/JSONPredicate/JsonPredicate.cs

View workflow job for this annotation

GitHub Actions / Build-Test

Missing XML comment for publicly visible type or member 'JSONPredicate.Evaluate(string, object)'
{
// Handle OR operations first (lower precedence)

Expand Down
14 changes: 14 additions & 0 deletions src/JSONPredicate/Operators/ContainsOperator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace JSONPredicate.Operators
{
internal static class ContainsOperator
{
public static bool Evaluate(object left, object right)
{
if (left == null || right == null)
return false;
var leftStr = left.ToString();
var rightStr = right.ToString();
return leftStr.IndexOf(rightStr, System.StringComparison.OrdinalIgnoreCase) >= 0;
}
}
}
14 changes: 14 additions & 0 deletions src/JSONPredicate/Operators/EndsWithOperator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace JSONPredicate.Operators
{
internal static class EndsWithOperator
{
public static bool Evaluate(object left, object right)
{
if (left == null || right == null)
return false;
var leftStr = left.ToString();
var rightStr = right.ToString();
return leftStr.EndsWith(rightStr, System.StringComparison.OrdinalIgnoreCase);
}
}
}
2 changes: 1 addition & 1 deletion src/JSONPredicate/Operators/EqOperator.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace JsonPathPredicate.Operators
namespace JSONPredicate.Operators
{
internal static class EqOperator
{
Expand Down
2 changes: 1 addition & 1 deletion src/JSONPredicate/Operators/GtOperator.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace JsonPathPredicate.Operators
namespace JSONPredicate.Operators
{
internal static class GtOperator
{
Expand Down
2 changes: 1 addition & 1 deletion src/JSONPredicate/Operators/GteOperator.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace JsonPathPredicate.Operators
namespace JSONPredicate.Operators
{
internal static class GteOperator
{
Expand Down
2 changes: 1 addition & 1 deletion src/JSONPredicate/Operators/InOperator.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System.Collections;

namespace JsonPathPredicate.Operators
namespace JSONPredicate.Operators
{
internal static class InOperator
{
Expand Down
2 changes: 1 addition & 1 deletion src/JSONPredicate/Operators/LtOperator.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace JsonPathPredicate.Operators
namespace JSONPredicate.Operators
{
internal static class LtOperator
{
Expand Down
2 changes: 1 addition & 1 deletion src/JSONPredicate/Operators/LteOperator.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace JsonPathPredicate.Operators
namespace JSONPredicate.Operators
{
internal static class LteOperator
{
Expand Down
2 changes: 1 addition & 1 deletion src/JSONPredicate/Operators/NotOperator.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace JsonPathPredicate.Operators
namespace JSONPredicate.Operators
{
internal static class NotOperator
{
Expand Down
14 changes: 14 additions & 0 deletions src/JSONPredicate/Operators/StartsWithOperator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace JSONPredicate.Operators
{
internal static class StartsWithOperator
{
public static bool Evaluate(object left, object right)
{
if (left == null || right == null)
return false;
var leftStr = left.ToString();
var rightStr = right.ToString();
return leftStr.StartsWith(rightStr, System.StringComparison.OrdinalIgnoreCase);
}
}
}
2 changes: 1 addition & 1 deletion src/JSONPredicate/Values.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;

namespace JsonPathPredicate
namespace JSONPredicate
{
internal static class Values
{
Expand Down
Loading