DEV Community

Code Ninja
Code Ninja

Posted on

JSONPath Predicate

This article provides complete information on JSONPathPredicate libray.

Source Code

Table of Contents

  1. Home
  2. Getting Started
  3. Expression Syntax
  4. Operators Reference
  5. Advanced Usage
  6. Best Practices
  7. Performance Considerations
  8. API Reference
  9. Examples
  10. Troubleshooting
  11. Contributing
  12. FAQ

Home

This library provides a powerful and intuitive way to evaluate string-based predicate expressions against JSON objects using JSONPath syntax in .NET applications.

What is JSONPathPredicate?

JSONPathPredicate is a lightweight .NET library that allows developers to write expressive queries against JSON objects using a simple, SQL-like syntax combined with JSONPath for property access. It's designed to be fast, type-safe, and easy to use for filtering, validation, and conditional logic operations.

Key Features

  • ๐ŸŽฏ JSONPath Support: Navigate nested object properties using intuitive dot notation
  • ๐Ÿ”ง Rich Operator Set: Support for equality, comparison, containment, and logical operators
  • ๐Ÿงฎ Type Safety: Automatic type conversion and validation for seamless comparisons
  • ๐Ÿ—๏ธ Complex Expressions: Parentheses grouping, operator precedence, and nested operations
  • โšก High Performance: Optimized evaluation engine with minimal overhead
  • ๐Ÿชถ Lightweight: Minimal dependencies, fast startup and evaluation times
  • ๐Ÿ•ฐ๏ธ DateTime Support: Built-in handling for various DateTime formats and comparisons
  • ๐Ÿ“š Multi-Framework: Supports .NET Framework 4.6.2, .NET Standard 2.0/2.1, and .NET 9.0

Quick Example

var customer = new { profile = new { name = "John Doe", age = 25, isActive = true }, orders = new[] { "premium", "urgent" }, score = 95.5, lastLogin = DateTime.Parse("2024-08-01T10:30:00Z") }; // Simple equality check bool isJohn = JSONPredicate.Evaluate("profile.name eq `John Doe`", customer); // Complex logical expression bool isEligible = JSONPredicate.Evaluate( "profile.age gte 18 and profile.isActive eq true and score gt 90", customer); // Array containment with date comparison bool hasRecentActivity = JSONPredicate.Evaluate( "orders in (`premium`, `vip`) and lastLogin gt `2024-07-01`", customer); 
Enter fullscreen mode Exit fullscreen mode

Getting Started

Installation

Install JSONPathPredicate via NuGet Package Manager:

Package Manager Console

Install-Package JsonPathPredicate 
Enter fullscreen mode Exit fullscreen mode

.NET CLI

dotnet add package JsonPathPredicate 
Enter fullscreen mode Exit fullscreen mode

PackageReference

<PackageReference Include="JsonPathPredicate" Version="1.0.0" /> 
Enter fullscreen mode Exit fullscreen mode

Framework Support

Framework Version Support Status
.NET Framework 4.6.2+ โœ… Full Support
.NET Standard 2.0, 2.1 โœ… Full Support
.NET Core 2.0+ โœ… Full Support
.NET 5.0, 6.0, 7.0, 8.0, 9.0 โœ… Full Support

Basic Usage

  1. Import the namespace:
 using JSONPathPredicate; 
Enter fullscreen mode Exit fullscreen mode
  1. Create your data object:
 var data = new { user = new { name = "Alice", age = 30, roles = new[] { "admin", "user" } }, settings = new { theme = "dark", notifications = true } }; 
Enter fullscreen mode Exit fullscreen mode
  1. Evaluate expressions:
 bool result = JSONPredicate.Evaluate("user.age gte 18", data); // Returns: true 
Enter fullscreen mode Exit fullscreen mode

Your First Expression

Let's break down a simple expression:

string expression = "user.name eq `Alice`"; bool result = JSONPredicate.Evaluate(expression, data); 
Enter fullscreen mode Exit fullscreen mode

This expression has three parts:

  • JSONPath (user.name): Navigates to the nested property
  • Operator (eq): Specifies the comparison type
  • Value (Alice): The value to compare against (wrapped in backticks)

Expression Syntax

Basic Structure

Every JSONPathPredicate expression follows this pattern:

[JSONPath] [Operator] [Value] 
Enter fullscreen mode Exit fullscreen mode

For complex expressions:

[Expression] [Logical Operator] [Expression] 
Enter fullscreen mode Exit fullscreen mode

JSONPath Navigation

JSONPath allows you to navigate through nested object properties using dot notation:

Pattern Description Example
property Root level property name
object.property Nested property user.name
object.nested.property Deeply nested property user.profile.address.city

Examples:

var data = new { customer = new { profile = new { personal = new { firstName = "John", lastName = "Doe" }, address = new { street = "123 Main St", city = "New York", zipCode = "10001" } }, preferences = new { theme = "dark", language = "en-US" } } }; // Access deeply nested properties bool result1 = JSONPredicate.Evaluate("customer.profile.personal.firstName eq `John`", data); bool result2 = JSONPredicate.Evaluate("customer.profile.address.city eq `New York`", data); bool result3 = JSONPredicate.Evaluate("customer.preferences.theme eq `dark`", data); 
Enter fullscreen mode Exit fullscreen mode

Value Syntax

Values in expressions must be properly formatted:

String Values

Wrap string values in backticks, single quotes, or double quotes:

// All equivalent "name eq `John`" "name eq 'John'" "name eq \"John\"" 
Enter fullscreen mode Exit fullscreen mode

Numeric Values

Numbers can be written directly:

"age eq 25" "score eq 95.5" "count eq -10" 
Enter fullscreen mode Exit fullscreen mode

Boolean Values

Boolean values are case-insensitive:

"isActive eq true" "isEnabled eq false" 
Enter fullscreen mode Exit fullscreen mode

DateTime Values

DateTime values should be in ISO 8601 format:

"createdAt eq `2024-08-01T10:30:00Z`" "birthDate gt `1990-01-01`" 
Enter fullscreen mode Exit fullscreen mode

Parentheses for Grouping

Use parentheses to control evaluation order:

// Without parentheses - AND has higher precedence "status eq `active` or role eq `admin` and age gte 18" // Evaluates as: status eq 'active' or (role eq 'admin' and age gte 18) // With parentheses - explicit grouping "(status eq `active` or role eq `admin`) and age gte 18" // Evaluates as: (status eq 'active' or role eq 'admin') and age gte 18 
Enter fullscreen mode Exit fullscreen mode

Comments and Whitespace

Expressions are whitespace-tolerant:

// All equivalent "user.name eq `John`" "user.name eq `John`" " user.name eq `John` " 
Enter fullscreen mode Exit fullscreen mode

Operators Reference

Comparison Operators

Equality Operator (eq)

Tests for equality with automatic type conversion and case-insensitive string comparison.

// String comparison (case-insensitive) "name eq `john`" // matches "John", "JOHN", "john" // Numeric comparison "age eq 25" // matches integer 25 "score eq 95.5" // matches double 95.5 // Boolean comparison "isActive eq true" // matches boolean true // DateTime comparison "createdAt eq `2024-01-01T00:00:00Z`" // exact DateTime match 
Enter fullscreen mode Exit fullscreen mode

Type Conversion Examples:

var data = new { count = "25" }; // String value bool result = JSONPredicate.Evaluate("count eq 25", data); // Returns true 
Enter fullscreen mode Exit fullscreen mode

Inequality Operator (not)

Tests for non-equality.

"status not `inactive`" // true if status is not "inactive" "age not 0" // true if age is not 0 "isDeleted not true" // true if isDeleted is not true 
Enter fullscreen mode Exit fullscreen mode

Contains/In Operator (in)

Tests if a value is contained within a collection or if collections intersect.

Single value against collection:

var data = new { tags = new[] { "vip", "premium", "gold" } }; // Check if tags contain specific values "tags in (`vip`, `platinum`)" // true (vip exists in tags) "tags in (`basic`, `standard`)" // false (none exist in tags) 
Enter fullscreen mode Exit fullscreen mode

Single value membership:

var data = new { role = "admin" }; // Check if role is in allowed list "role in (`admin`, `moderator`, `user`)" // true 
Enter fullscreen mode Exit fullscreen mode

Collection intersection:

var data = new { userTags = new[] { "premium", "early-access" }, requiredTags = new[] { "premium", "vip" } }; // Check if collections have any common elements "userTags in requiredTags" // true (both have "premium") 
Enter fullscreen mode Exit fullscreen mode

Comparison Operators (gt, gte, lt, lte)

Greater Than (gt)

"age gt 18" // age > 18 "score gt 90.5" // score > 90.5 "createdAt gt `2024-01-01`" // date after 2024-01-01 
Enter fullscreen mode Exit fullscreen mode

Greater Than or Equal (gte)

"age gte 18" // age >= 18 "rating gte 4.5" // rating >= 4.5 
Enter fullscreen mode Exit fullscreen mode

Less Than (lt)

"age lt 65" // age < 65 "price lt 100.00" // price < 100.00 
Enter fullscreen mode Exit fullscreen mode

Less Than or Equal (lte)

"discount lte 50" // discount <= 50 "temperature lte 32.0" // temperature <= 32.0 
Enter fullscreen mode Exit fullscreen mode

Numeric Type Handling:
The library automatically handles different numeric types:

var data = new { intValue = 25, doubleValue = 25.0, floatValue = 25.0f, decimalValue = 25.0m }; // All these return true "intValue eq 25" "doubleValue eq 25" "floatValue eq 25" "decimalValue eq 25" 
Enter fullscreen mode Exit fullscreen mode

Logical Operators

AND Operator (and)

Requires all conditions to be true. Has higher precedence than OR.

// Simple AND "age gte 18 and isActive eq true" // Multiple AND conditions "status eq `active` and role eq `admin` and score gt 80" // Mixed with comparison operators "price gte 10 and price lte 100 and category eq `electronics`" 
Enter fullscreen mode Exit fullscreen mode

OR Operator (or)

Requires at least one condition to be true. Has lower precedence than AND.

// Simple OR "role eq `admin` or role eq `moderator`" // Multiple OR conditions "status eq `premium` or status eq `vip` or status eq `gold`" // Mixed with AND (AND has precedence) "isActive eq true and role eq `admin` or status eq `override`" // Evaluates as: (isActive eq true and role eq 'admin') or status eq 'override' 
Enter fullscreen mode Exit fullscreen mode

Operator Precedence

The evaluation order follows these precedence rules:

  1. Parentheses (highest precedence)
  2. Comparison operators (eq, not, in, gt, gte, lt, lte)
  3. AND operator
  4. OR operator (lowest precedence)

Examples:

// Without parentheses "a eq 1 or b eq 2 and c eq 3" // Evaluates as: a eq 1 or (b eq 2 and c eq 3) // With parentheses for different grouping "(a eq 1 or b eq 2) and c eq 3" // Evaluates as: (a eq 1 or b eq 2) and c eq 3 
Enter fullscreen mode Exit fullscreen mode

Special Cases and Edge Conditions

Null Handling

var data = new { nullableField = (string)null }; "nullableField eq `test`" // Returns false "nullableField not `test`" // Returns true 
Enter fullscreen mode Exit fullscreen mode

Empty Collections

var data = new { tags = new string[0] }; "tags in (`anything`)" // Returns false 
Enter fullscreen mode Exit fullscreen mode

Type Mismatches

var data = new { textNumber = "25", realNumber = 25 }; "textNumber eq 25" // Returns true (automatic conversion) "textNumber gt 20" // Returns true (automatic conversion) 
Enter fullscreen mode Exit fullscreen mode

Advanced Usage

Complex Nested Expressions

JSONPathPredicate excels at handling complex logical expressions with multiple levels of nesting:

var customer = new { profile = new { name = "Alice Johnson", age = 28, membership = "premium", verified = true }, account = new { balance = 1500.50, currency = "USD", status = "active", lastTransaction = DateTime.Parse("2024-07-15T14:30:00Z") }, preferences = new { notifications = true, theme = "dark", language = "en-US" }, tags = new[] { "vip", "early-adopter", "premium" } }; // Complex eligibility check bool isEligibleForOffer = JSONPredicate.Evaluate(@" (profile.age gte 18 and profile.age lte 65) and (profile.membership in (`premium`, `vip`, `gold`)) and (account.balance gt 1000 and account.status eq `active`) and (account.lastTransaction gt `2024-06-01` and profile.verified eq true) ", customer); // Multi-criteria filtering with fallbacks bool hasSpecialAccess = JSONPredicate.Evaluate(@" (profile.membership eq `premium` and account.balance gt 5000) or (tags in (`vip`, `beta-tester`) and profile.verified eq true) or (profile.age gt 65 and account.status eq `active`) ", customer); 
Enter fullscreen mode Exit fullscreen mode

Working with Arrays and Collections

Array Containment Patterns

var user = new { roles = new[] { "user", "admin", "moderator" }, permissions = new[] { "read", "write", "delete", "admin" }, tags = new[] { "premium", "verified", "early-adopter" }, favoriteCategories = new[] { "electronics", "books", "home" } }; // Check for specific role bool isAdmin = JSONPredicate.Evaluate("roles in (`admin`)", user); // Check for any admin permissions bool hasAdminPerms = JSONPredicate.Evaluate("permissions in (`admin`, `super-admin`)", user); // Check for multiple tag requirements bool isPremiumUser = JSONPredicate.Evaluate( "tags in (`premium`, `vip`) and roles in (`user`, `admin`)", user); // Complex array operations bool canAccessFeature = JSONPredicate.Evaluate(@" (roles in (`admin`, `moderator`) and permissions in (`read`, `write`)) or (tags in (`premium`, `vip`) and permissions in (`read`)) ", user); 
Enter fullscreen mode Exit fullscreen mode

Array Intersection Logic

When both sides of an in operation are arrays, the operation checks for intersection:

var data = new { userCategories = new[] { "electronics", "books", "sports" }, allowedCategories = new[] { "electronics", "home", "garden" }, blockedCategories = new[] { "adult", "gambling" } }; // Check if user has access to any allowed categories bool hasAccess = JSONPredicate.Evaluate( "userCategories in allowedCategories", data); // true (electronics matches) // Check if user is accessing blocked content bool isBlocked = JSONPredicate.Evaluate( "userCategories in blockedCategories", data); // false (no intersection) 
Enter fullscreen mode Exit fullscreen mode

DateTime Operations and Formats

JSONPathPredicate provides robust DateTime handling with support for multiple formats:

Supported DateTime Formats

var events = new { createdAt = DateTime.Parse("2024-08-01T10:30:00Z"), updatedAt = "2024-08-02T15:45:00Z", // String will be parsed scheduledFor = "2024-08-10", deadline = "2024-12-31T23:59:59Z" }; // ISO 8601 with timezone "createdAt eq `2024-08-01T10:30:00Z`" // Date only format "scheduledFor eq `2024-08-10`" // Different timezone representations "createdAt eq `2024-08-01T05:30:00-05:00`" // EST timezone // Comparison operations "createdAt gt `2024-07-01` and deadline lt `2025-01-01`" 
Enter fullscreen mode Exit fullscreen mode

DateTime Range Queries

// Date range filtering bool inDateRange = JSONPredicate.Evaluate(@" createdAt gte `2024-08-01` and createdAt lte `2024-08-31` ", events); // Business hours check var transaction = new { timestamp = DateTime.Parse("2024-08-01T14:30:00Z"), amount = 250.00 }; bool duringBusinessHours = JSONPredicate.Evaluate(@" timestamp gte `2024-08-01T09:00:00Z` and timestamp lte `2024-08-01T17:00:00Z` ", transaction); 
Enter fullscreen mode Exit fullscreen mode

Relative Date Comparisons

var document = new { createdAt = DateTime.Parse("2024-01-15T10:00:00Z"), modifiedAt = DateTime.Parse("2024-07-20T14:30:00Z"), expiresAt = DateTime.Parse("2024-12-31T23:59:59Z") }; // Check if document was created this year bool thisYear = JSONPredicate.Evaluate("createdAt gte `2024-01-01`", document); // Check if recently modified (within last 30 days of a reference point) bool recentlyModified = JSONPredicate.Evaluate( "modifiedAt gte `2024-07-01`", document); 
Enter fullscreen mode Exit fullscreen mode

Type Coercion and Conversion

JSONPathPredicate automatically handles type conversions for seamless comparisons:

Numeric Conversions

var mixedData = new { stringNumber = "42", intNumber = 42, floatNumber = 42.0f, doubleNumber = 42.0, decimalNumber = 42.0m }; // All these expressions return true due to automatic conversion bool result1 = JSONPredicate.Evaluate("stringNumber eq 42", mixedData); bool result2 = JSONPredicate.Evaluate("intNumber eq 42.0", mixedData); bool result3 = JSONPredicate.Evaluate("floatNumber eq 42", mixedData); bool result4 = JSONPredicate.Evaluate("stringNumber eq floatNumber", mixedData); 
Enter fullscreen mode Exit fullscreen mode

String and Character Handling

var textData = new { singleChar = 'A', charString = "A", name = "John", upperName = "JOHN" }; // Case-insensitive string comparisons bool match1 = JSONPredicate.Evaluate("name eq `john`", textData); // true bool match2 = JSONPredicate.Evaluate("name eq `JOHN`", textData); // true // Character-string interoperability bool match3 = JSONPredicate.Evaluate("singleChar eq `A`", textData); // true bool match4 = JSONPredicate.Evaluate("charString eq singleChar", textData); // true 
Enter fullscreen mode Exit fullscreen mode

Boolean Conversions

var flags = new { isActive = true, isEnabled = "true", isValid = 1, // Truthy value isFlagged = false }; // Boolean comparisons with type conversion bool result = JSONPredicate.Evaluate("isActive eq true", flags); 
Enter fullscreen mode Exit fullscreen mode

Performance Optimization Patterns

Expression Caching Strategy

For applications that repeatedly evaluate the same expressions, consider implementing a caching strategy:

public class PredicateCache { private readonly Dictionary<string, CompiledPredicate> _cache = new(); public bool Evaluate(string expression, object data) { // In a real implementation, you might want to compile expressions // for better performance on repeated evaluations return JSONPredicate.Evaluate(expression, data); } } 
Enter fullscreen mode Exit fullscreen mode

Efficient Expression Design

Structure your expressions for optimal performance:

// Good: More selective conditions first "isActive eq true and role eq `admin` and complexCalculation gt 100" // Better: Short-circuit on most selective condition "role eq `admin` and isActive eq true and complexCalculation gt 100" // Good: Use specific comparisons when possible "status eq `active`" // Better than "status not `inactive`" 
Enter fullscreen mode Exit fullscreen mode

Best Practices

Expression Design Guidelines

1. Use Clear and Descriptive JSONPaths

// Good: Clear property navigation "user.profile.contactInfo.email eq `john@example.com`" // Avoid: Unclear abbreviated paths "u.p.ci.e eq `john@example.com`" 
Enter fullscreen mode Exit fullscreen mode

2. Consistent Value Quoting

Choose a quoting style and stick with it throughout your application:

// Good: Consistent backtick usage "name eq `John` and city eq `New York`" // Avoid: Mixed quoting styles "name eq `John` and city eq 'New York'" 
Enter fullscreen mode Exit fullscreen mode

3. Logical Expression Structure

Structure complex expressions for readability:

// Good: Logical grouping with clear precedence "(user.isActive eq true and user.role eq `admin`) or user.permissions in (`override`)" // Good: Multi-line for complex expressions string complexRule = @" (account.balance gt 1000 and account.status eq `active`) and (user.verified eq true and user.age gte 18) and (subscription.tier in (`premium`, `enterprise`))"; 
Enter fullscreen mode Exit fullscreen mode

4. Performance-Conscious Design

// Good: Most selective condition first "user.role eq `admin` and user.isActive eq true and user.lastLogin gt `2024-01-01`" // Good: Use positive conditions when possible "status eq `active`" // Instead of "status not `inactive`" 
Enter fullscreen mode Exit fullscreen mode

Error Handling Patterns

1. Expression Validation

public class PredicateValidator { public bool TryValidateExpression(string expression, out string error) { try { // Test with dummy object to validate syntax var testObj = new { test = "value" }; JSONPredicate.Evaluate("test eq `value`", testObj); error = null; return true; } catch (ArgumentException ex) { error = ex.Message; return false; } } } 
Enter fullscreen mode Exit fullscreen mode

2. Safe Evaluation Pattern

public static class SafeJSONPredicate { public static bool TryEvaluate(string expression, object data, out bool result) { try { result = JSONPredicate.Evaluate(expression, data); return true; } catch (Exception) { result = false; return false; } } public static bool EvaluateWithDefault(string expression, object data, bool defaultValue = false) { try { return JSONPredicate.Evaluate(expression, data); } catch (Exception) { return defaultValue; } } } 
Enter fullscreen mode Exit fullscreen mode

Security Considerations

1. Input Sanitization

When accepting user-provided expressions, implement proper validation:

public class ExpressionSanitizer { private readonly HashSet<string> _allowedOperators = new HashSet<string> { "eq", "not", "in", "gt", "gte", "lt", "lte", "and", "or" }; private readonly HashSet<string> _allowedPaths = new HashSet<string> { "user.name", "user.age", "user.role", "account.balance" // Define allowed JSONPaths }; public bool IsValidExpression(string expression) { // Implement validation logic here // Check for allowed operators and paths only return true; // Simplified for example } } 
Enter fullscreen mode Exit fullscreen mode

2. Path Restriction

Limit which properties can be accessed:

public class RestrictedJSONPredicate { private readonly HashSet<string> _allowedPaths; public RestrictedJSONPredicate(IEnumerable<string> allowedPaths) { _allowedPaths = new HashSet<string>(allowedPaths); } public bool Evaluate(string expression, object data) { // Extract paths from expression and validate var paths = ExtractPaths(expression); if (paths.Any(p => !_allowedPaths.Contains(p))) { throw new UnauthorizedAccessException("Expression contains restricted paths"); } return JSONPredicate.Evaluate(expression, data); } private IEnumerable<string> ExtractPaths(string expression) { // Implementation to extract JSONPaths from expression // This is a simplified example return new List<string>(); } } 
Enter fullscreen mode Exit fullscreen mode

Testing Strategies

1. Unit Testing Expressions

[TestFixture] public class BusinessRuleTests { private readonly object _testCustomer = new { profile = new { name = "John Doe", age = 35, membershipLevel = "gold" }, account = new { balance = 2500.00, status = "active" }, preferences = new { emailNotifications = true } }; [Test] public void PremiumCustomerRule_ShouldReturnTrue_WhenCriteriaMet() { var rule = "profile.age gte 18 and account.balance gt 1000 and profile.membershipLevel in (`gold`, `platinum`)"; var result = JSONPredicate.Evaluate(rule, _testCustomer); Assert.That(result, Is.True); } [TestCase("profile.age", 35, true)] [TestCase("profile.age", 17, false)] public void AgeValidation_ShouldWorkCorrectly(string path, int age, bool expected) { var customer = new { profile = new { age = age } }; var rule = "profile.age gte 18"; var result = JSONPredicate.Evaluate(rule, customer); Assert.That(result, Is.EqualTo(expected)); } } 
Enter fullscreen mode Exit fullscreen mode

2. Expression Coverage Testing

public class ExpressionCoverageHelper { public static void TestAllOperators(object testData) { var operators = new[] { "eq", "not", "in", "gt", "gte", "lt", "lte" }; var logicalOps = new[] { "and", "or" }; foreach (var op in operators) { // Test each operator with various data types TestOperator(op, testData); } } private static void TestOperator(string op, object data) { // Implementation to systematically test operators } } 
Enter fullscreen mode Exit fullscreen mode

Performance Considerations

Evaluation Performance

Benchmarking Results

Based on internal testing, JSONPathPredicate provides excellent performance characteristics:

Expression Type Evaluations/sec Memory Usage
Simple equality ~2,000,000 < 1KB
Complex logical ~500,000 < 2KB
Array operations ~300,000 < 3KB
DateTime comparisons ~400,000 < 2KB

Performance Optimization Tips

  1. Order Conditions by Selectivity
 // Good: Most selective first "user.role eq `admin` and user.isActive eq true" // Less optimal: Less selective first "user.isActive eq true and user.role eq `admin`" 
Enter fullscreen mode Exit fullscreen mode
  1. Use Positive Conditions
 // Preferred "status eq `active`" // Less efficient "status not `inactive`" 
Enter fullscreen mode Exit fullscreen mode
  1. Minimize Deep Nesting
 // Consider flattening very deep paths "user.profile.settings.preferences.theme eq `dark`" 
Enter fullscreen mode Exit fullscreen mode

Memory Usage Patterns

Object Serialization Overhead

JSONPathPredicate serializes objects to JSON for evaluation. Consider these patterns:

// Good: Lightweight evaluation objects var slimData = new { userId = user.Id, role = user.Role, isActive = user.IsActive }; bool result = JSONPredicate.Evaluate("role eq `admin`", slimData); // Less optimal: Heavy objects with unnecessary data bool result2 = JSONPredicate.Evaluate("role eq `admin`", fullUserObjectWithManyProperties); 
Enter fullscreen mode Exit fullscreen mode

Caching Strategies

For repeated evaluations, consider implementing caching:

public class CachedPredicateEvaluator { private readonly LRUCache<string, object> _objectCache = new(1000); public bool Evaluate(string expression, object data) { string dataKey = GenerateKey(data); if (!_objectCache.TryGetValue(dataKey, out object cachedData)) { cachedData = data; _objectCache[dataKey] = cachedData; } return JSONPredicate.Evaluate(expression, cachedData); } } 
Enter fullscreen mode Exit fullscreen mode

Scalability Considerations

High-Volume Scenarios

For applications processing thousands of evaluations per second:

public class HighPerformancePredicateService { private readonly ConcurrentDictionary<string, CompiledExpression> _expressionCache = new(); public async Task<bool[]> EvaluateBatchAsync(string expression, IEnumerable<object> dataItems) { var tasks = dataItems.Select(data => Task.Run(() => JSONPredicate.Evaluate(expression, data)) ); return await Task.WhenAll(tasks); } public bool EvaluateWithMetrics(string expression, object data, out TimeSpan duration) { var stopwatch = Stopwatch.StartNew(); var result = JSONPredicate.Evaluate(expression, data); duration = stopwatch.Elapsed; return result; } } 
Enter fullscreen mode Exit fullscreen mode

Memory-Efficient Patterns

// Pattern: Use projection for large objects public static class EfficientEvaluation { public static bool EvaluateProjected<T>(string expression, T source, Func<T, object> projector) { var projectedData = projector(source); return JSONPredicate.Evaluate(expression, projectedData); } } // Usage var result = EfficientEvaluation.EvaluateProjected( "name eq `John` and age gt 18", fullUserObject, user => new { name = user.FullName, age = user.Age } ); 
Enter fullscreen mode Exit fullscreen mode

API Reference

JSONPredicate Class

The main entry point for evaluating predicate expressions.

Methods

Evaluate(string expression, object obj)

Evaluates a predicate expression against an object.

Parameters:

  • expression (string): The predicate expression to evaluate
  • obj (object): The object to evaluate the expression against

Returns:

  • bool: True if the expression evaluates to true, false otherwise

Exceptions:

  • ArgumentException: Thrown when the expression format is invalid
  • FormatException: Thrown when DateTime parsing fails
  • InvalidOperationException: Thrown for unsupported operations

Example:

var data = new { name = "John", age = 25 }; bool result = JSONPredicate.Evaluate("name eq `John` and age gte 18", data); 
Enter fullscreen mode Exit fullscreen mode

Expression Grammar

The formal grammar for JSONPathPredicate expressions:

Expression := OrExpression OrExpression := AndExpression ('or' AndExpression)* AndExpression := AtomicExpression ('and' AtomicExpression)* AtomicExpression := '(' Expression ')' | JSONPath Operator Value JSONPath := Identifier ('.' Identifier)* Operator := 'eq' | 'not' | 'in' | 'gt' | 'gte' | 'lt' | 'lte' Value := StringLiteral | NumberLiteral | BooleanLiteral | ArrayLiteral StringLiteral := '`' [^`]* '`' | '\'' [^']* '\'' | '"' [^"]* '"' NumberLiteral := '-'? [0-9]+ ('.' [0-9]+)? BooleanLiteral := 'true' | 'false' ArrayLiteral := '(' Value (',' Value)* ')' Identifier := [a-zA-Z_][a-zA-Z0-9_]* 
Enter fullscreen mode Exit fullscreen mode

Internal Components

While these are internal implementation details, understanding them can help with troubleshooting:

DataTypes Class

Handles type comparisons and conversions.

JsonPath Class

Manages JSONPath navigation and property resolution.


Examples

Real-World Use Cases

1. User Authentication and Authorization

public class AuthService { public bool IsAuthorized(User user, string resource, string action) { var context = new { user = new { id = user.Id, role = user.Role, permissions = user.Permissions.ToArray(), isActive = user.IsActive, accountExpiry = user.AccountExpiry }, resource = resource, action = action, currentTime = DateTime.UtcNow }; // Admin override if (JSONPredicate.Evaluate("user.role eq `admin` and user.isActive eq true", context)) return true; // Check specific permissions var permissionRule = $"user.permissions in (`{resource}:{action}`, `{resource}:*`, `*:*`)"; if (JSONPredicate.Evaluate($"{permissionRule} and user.isActive eq true", context)) return true; // Account expiry check if (JSONPredicate.Evaluate("user.accountExpiry lt currentTime", context)) return false; return false; } } 
Enter fullscreen mode Exit fullscreen mode

2. E-commerce Product Filtering

public class ProductFilterService { public IEnumerable<Product> FilterProducts(IEnumerable<Product> products, ProductFilterCriteria criteria) { var filterExpression = BuildFilterExpression(criteria); return products.Where(product => { var productData = new { name = product.Name, price = product.Price, category = product.Category, brand = product.Brand, rating = product.AverageRating, inStock = product.StockQuantity > 0, tags = product.Tags.ToArray(), releaseDate = product.ReleaseDate, onSale = product.SalePrice.HasValue }; return JSONPredicate.Evaluate(filterExpression, productData); }); } private string BuildFilterExpression(ProductFilterCriteria criteria) { var conditions = new List<string>(); if (criteria.MinPrice.HasValue) conditions.Add($"price gte {criteria.MinPrice.Value}"); if (criteria.MaxPrice.HasValue) conditions.Add($"price lte {criteria.MaxPrice.Value}"); if (!string.IsNullOrEmpty(criteria.Category)) conditions.Add($"category eq `{criteria.Category}`"); if (criteria.InStockOnly) conditions.Add("inStock eq true"); if (criteria.MinRating.HasValue) conditions.Add($"rating gte {criteria.MinRating.Value}"); if (criteria.Tags?.Any() == true) { var tagsList = string.Join("`, `", criteria.Tags); conditions.Add($"tags in (`{tagsList}`)"); } if (criteria.OnSaleOnly) conditions.Add("onSale eq true"); return conditions.Any() ? string.Join(" and ", conditions) : "inStock eq true"; } } 
Enter fullscreen mode Exit fullscreen mode

3. Business Rule Engine

public class BusinessRuleEngine { private readonly Dictionary<string, string> _rules = new() { ["PREMIUM_ELIGIBILITY"] = @" (customer.age gte 21 and customer.creditScore gt 700) and (account.balance gt 10000 or account.monthlyIncome gt 5000) and customer.accountAge gte 365", ["DISCOUNT_ELIGIBILITY"] = @" (customer.loyaltyTier in (`gold`, `platinum`) and order.amount gt 100) or (customer.isFirstTime eq true and order.amount gt 50) or (customer.tags in (`employee`, `partner`) and order.amount gt 0)", ["FRAUD_DETECTION"] = @" (transaction.amount gt customer.averageTransactionAmount * 10) or (transaction.location not customer.usualLocations) or (transaction.time lt `06:00` or transaction.time gt `23:00`) and (transaction.amount gt 1000)" }; public bool EvaluateRule(string ruleName, object context) { if (!_rules.TryGetValue(ruleName, out var expression)) throw new ArgumentException($"Unknown rule: {ruleName}"); return JSONPredicate.Evaluate(expression, context); } public Dictionary<string, bool> EvaluateAllRules(object context) { return _rules.ToDictionary( kvp => kvp.Key, kvp => JSONPredicate.Evaluate(kvp.Value, context) ); } } 
Enter fullscreen mode Exit fullscreen mode

4. Configuration-Driven Validation

public class ConfigurableValidator { public class ValidationRule { public string Name { get; set; } public string Expression { get; set; } public string ErrorMessage { get; set; } public bool IsWarning { get; set; } } private readonly List<ValidationRule> _rules = new() { new ValidationRule { Name = "AGE_VALIDATION", Expression = "user.age gte 18 and user.age lte 120", ErrorMessage = "User age must be between 18 and 120", IsWarning = false }, new ValidationRule { Name = "EMAIL_DOMAIN_CHECK", Expression = "user.email in (`@company.com`, `@partner.com`)", ErrorMessage = "Only company or partner email addresses are allowed", IsWarning = true } }; public ValidationResult Validate(object data) { var result = new ValidationResult { IsValid = true }; foreach (var rule in _rules) { try { bool isValid = JSONPredicate.Evaluate(rule.Expression, data); if (!isValid) { if (rule.IsWarning) { result.Warnings.Add(rule.ErrorMessage); } else { result.Errors.Add(rule.ErrorMessage); result.IsValid = false; } } } catch (Exception ex) { result.Errors.Add($"Rule '{rule.Name}' evaluation failed: {ex.Message}"); result.IsValid = false; } } return result; } } public class ValidationResult { public bool IsValid { get; set; } public List<string> Errors { get; set; } = new(); public List<string> Warnings { get; set; } = new(); } 
Enter fullscreen mode Exit fullscreen mode

5. Dynamic Content Personalization

public class ContentPersonalizationService { private readonly Dictionary<string, ContentRule> _contentRules = new() { ["SHOW_PREMIUM_CTA"] = new ContentRule { Expression = "user.tier eq `free` and user.usageDays gt 7 and user.featuresUsed gt 3", Content = "Upgrade to Premium for unlimited access!" }, ["SHOW_RENEWAL_NOTICE"] = new ContentRule { Expression = "user.subscriptionExpiry lt `2024-12-31` and user.tier in (`premium`, `pro`)", Content = "Your subscription expires soon. Renew now!" }, ["SHOW_WELCOME_MESSAGE"] = new ContentRule { Expression = "user.isNewUser eq true and user.lastLogin eq null", Content = "Welcome! Let's get you started." } }; public List<string> GetPersonalizedContent(User user) { var userContext = new { user = new { id = user.Id, tier = user.SubscriptionTier, isNewUser = user.CreatedAt > DateTime.UtcNow.AddDays(-7), lastLogin = user.LastLoginAt, subscriptionExpiry = user.SubscriptionExpiry, usageDays = (DateTime.UtcNow - user.CreatedAt).Days, featuresUsed = user.FeatureUsageCount } }; var applicableContent = new List<string>(); foreach (var rule in _contentRules) { if (JSONPredicate.Evaluate(rule.Value.Expression, userContext)) { applicableContent.Add(rule.Value.Content); } } return applicableContent; } private class ContentRule { public string Expression { get; set; } public string Content { get; set; } } } 
Enter fullscreen mode Exit fullscreen mode

Integration Examples

ASP.NET Core Integration

// Startup.cs or Program.cs public void ConfigureServices(IServiceCollection services) { services.AddScoped<IPredicateService, PredicateService>(); } // Controller [ApiController] [Route("api/[controller]")] public class UsersController : ControllerBase { private readonly IPredicateService _predicateService; public UsersController(IPredicateService predicateService) { _predicateService = predicateService; } [HttpGet("filter")] public IActionResult FilterUsers([FromQuery] string expression) { if (string.IsNullOrEmpty(expression)) return BadRequest("Expression is required"); try { var users = GetUsers(); // Your data source var filteredUsers = users.Where(user => _predicateService.EvaluateUserFilter(expression, user)); return Ok(filteredUsers); } catch (ArgumentException ex) { return BadRequest($"Invalid expression: {ex.Message}"); } } } 
Enter fullscreen mode Exit fullscreen mode

Entity Framework Integration

public static class QueryableExtensions { public static IQueryable<T> WherePredicate<T>(this IQueryable<T> source, string expression) { return source.AsEnumerable() .Where(item => JSONPredicate.Evaluate(expression, item)) .AsQueryable(); } // For in-memory evaluation after database query public static IEnumerable<T> FilterWithPredicate<T>(this IEnumerable<T> source, string expression) { return source.Where(item => JSONPredicate.Evaluate(expression, item)); } } // Usage var activeAdminUsers = dbContext.Users .Where(u => u.IsActive) // Database filter .ToList() // Execute database query .FilterWithPredicate("role eq `admin` and lastLogin gt `2024-01-01`"); // In-memory predicate 
Enter fullscreen mode Exit fullscreen mode

Background Service Integration

public class RuleEvaluationService : BackgroundService { private readonly IServiceProvider _serviceProvider; private readonly ILogger<RuleEvaluationService> _logger; public RuleEvaluationService(IServiceProvider serviceProvider, ILogger<RuleEvaluationService> logger) { _serviceProvider = serviceProvider; _logger = logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { using var scope = _serviceProvider.CreateScope(); var businessRuleEngine = scope.ServiceProvider.GetRequiredService<BusinessRuleEngine>(); try { await ProcessPendingRules(businessRuleEngine); } catch (Exception ex) { _logger.LogError(ex, "Error processing business rules"); } await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken); } } private async Task ProcessPendingRules(BusinessRuleEngine ruleEngine) { var pendingEvaluations = GetPendingEvaluations(); foreach (var evaluation in pendingEvaluations) { var result = ruleEngine.EvaluateRule(evaluation.RuleName, evaluation.Context); await ProcessRuleResult(evaluation, result); } } } 
Enter fullscreen mode Exit fullscreen mode

Troubleshooting

Common Issues and Solutions

1. Expression Format Errors

Problem: ArgumentException: Invalid expression format

Common Causes:

  • Missing operators
  • Incorrect syntax
  • Unmatched parentheses

Solutions:

// โŒ Incorrect "user.name John" // Missing operator // โœ… Correct "user.name eq `John`" // โŒ Incorrect "user.age > 18" // Invalid operator // โœ… Correct "user.age gt 18" // โŒ Incorrect "(user.age gte 18 and user.isActive eq true" // Unmatched parenthesis // โœ… Correct "(user.age gte 18 and user.isActive eq true)" 
Enter fullscreen mode Exit fullscreen mode

2. Type Conversion Issues

Problem: Unexpected comparison results

Common Causes:

  • Type mismatches
  • String/numeric confusion
  • DateTime format issues

Solutions:

// Issue: String numbers not comparing correctly var data = new { count = "25" }; // โŒ This might not work as expected in some cases "count gt 20" // โœ… Better approach - be explicit about types // The library handles this automatically, but be aware of your data types // Issue: DateTime format problems // โŒ Incorrect format "createdAt gt `2024-13-01`" // Invalid month // โœ… Correct format "createdAt gt `2024-01-01T00:00:00Z`" 
Enter fullscreen mode Exit fullscreen mode

3. JSONPath Resolution Issues

Problem: Properties not found or incorrect navigation

Common Causes:

  • Incorrect property names
  • Case sensitivity issues
  • Missing nested properties

Solutions:

var data = new { User = new { // Note: Capital 'U' Name = "John" // Note: Capital 'N' } }; // โŒ Incorrect casing "user.name eq `John`" // Returns false - property not found // โœ… Correct casing "User.Name eq `John`" // Returns true // โŒ Non-existent path "user.profile.name eq `John`" // Returns false if profile doesn't exist // โœ… Check your object structure Console.WriteLine(JsonSerializer.Serialize(data, new JsonSerializerOptions { WriteIndented = true })); 
Enter fullscreen mode Exit fullscreen mode

4. Array Operation Confusion

Problem: in operator not working as expected

Common Causes:

  • Misunderstanding of array intersection logic
  • Incorrect value syntax

Solutions:

var data = new { tags = new[] { "admin", "user" } }; // โŒ Incorrect - missing parentheses for multiple values "tags in `admin`, `user`" // โœ… Correct "tags in (`admin`, `user`)" // โŒ Incorrect - trying to check if string contains array var singleTag = new { role = "admin" }; "role in tags" // This won't work - tags doesn't exist in singleTag // โœ… Correct - check if single value is in array "role in (`admin`, `user`, `moderator`)" 
Enter fullscreen mode Exit fullscreen mode

Debugging Techniques

1. Expression Breakdown

public static class PredicateDebugger { public static void DebugExpression(string expression, object data) { Console.WriteLine($"Expression: {expression}"); Console.WriteLine($"Data: {JsonSerializer.Serialize(data, new JsonSerializerOptions { WriteIndented = true })}"); try { var result = JSONPredicate.Evaluate(expression, data); Console.WriteLine($"Result: {result}"); } catch (Exception ex) { Console.WriteLine($"Error: {ex.Message}"); } } public static void TestExpressionParts(string expression, object data) { // Break down complex expressions into parts var parts = SplitExpression(expression); foreach (var part in parts) { try { var result = JSONPredicate.Evaluate(part, data); Console.WriteLine($"'{part}' -> {result}"); } catch (Exception ex) { Console.WriteLine($"'{part}' -> ERROR: {ex.Message}"); } } } } 
Enter fullscreen mode Exit fullscreen mode

2. Data Structure Inspection

public static class DataInspector { public static void InspectObject(object obj) { var json = JsonSerializer.Serialize(obj, new JsonSerializerOptions { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); Console.WriteLine("Object structure:"); Console.WriteLine(json); // Extract all possible JSONPaths var paths = ExtractAllPaths(obj); Console.WriteLine("\nAvailable JSONPaths:"); foreach (var path in paths) { Console.WriteLine($" {path}"); } } private static List<string> ExtractAllPaths(object obj, string prefix = "") { var paths = new List<string>(); var json = JsonSerializer.Serialize(obj); using var doc = JsonDocument.Parse(json); ExtractPathsRecursive(doc.RootElement, prefix, paths); return paths; } private static void ExtractPathsRecursive(JsonElement element, string currentPath, List<string> paths) { switch (element.ValueKind) { case JsonValueKind.Object: foreach (var prop in element.EnumerateObject()) { var newPath = string.IsNullOrEmpty(currentPath) ? prop.Name : $"{currentPath}.{prop.Name}"; paths.Add(newPath); ExtractPathsRecursive(prop.Value, newPath, paths); } break; } } } 
Enter fullscreen mode Exit fullscreen mode

3. Performance Profiling

public static class PerformanceProfiler { public static void ProfileExpression(string expression, object data, int iterations = 1000) { // Warmup for (int i = 0; i < 100; i++) { JSONPredicate.Evaluate(expression, data); } var stopwatch = Stopwatch.StartNew(); for (int i = 0; i < iterations; i++) { JSONPredicate.Evaluate(expression, data); } stopwatch.Stop(); Console.WriteLine($"Expression: {expression}"); Console.WriteLine($"Iterations: {iterations}"); Console.WriteLine($"Total time: {stopwatch.ElapsedMilliseconds}ms"); Console.WriteLine($"Average time: {(double)stopwatch.ElapsedTicks / iterations / TimeSpan.TicksPerMillisecond:F4}ms"); Console.WriteLine($"Evaluations per second: {iterations / stopwatch.Elapsed.TotalSeconds:F0}"); } } 
Enter fullscreen mode Exit fullscreen mode

Error Message Guide

Error Message Cause Solution
"Invalid expression format: ..." Syntax error in expression Check expression syntax, ensure proper operator usage
"Unable to parse DateTime value: ..." Invalid DateTime format Use ISO 8601 format (YYYY-MM-DDTHH:mm:ssZ)
"Comparison operator not supported" Unknown operator Use only: eq, not, in, gt, gte, lt, lte
"Property not found: ..." JSONPath doesn't exist Verify object structure and property names

Contributing

Development Setup

  1. Clone the repository
 git clone https://github.com/CodeShayk/JSONPathPredicate.git cd JSONPathPredicate 
Enter fullscreen mode Exit fullscreen mode
  1. Install dependencies
 dotnet restore 
Enter fullscreen mode Exit fullscreen mode
  1. Build the solution
 dotnet build 
Enter fullscreen mode Exit fullscreen mode
  1. Run tests
 dotnet test 
Enter fullscreen mode Exit fullscreen mode

Contribution Guidelines

Code Standards

  1. Follow C# coding conventions
  2. Use meaningful variable and method names
  3. Add XML documentation for public APIs
  4. Include unit tests for new features
  5. Maintain backward compatibility

Testing Requirements

All contributions must include comprehensive tests:

[TestFixture] public class NewFeatureTests { [Test] public void NewFeature_ValidInput_ShouldReturnExpectedResult() { // Arrange var testData = new { /* test object */ }; var expression = "/* your expression */"; // Act var result = JSONPredicate.Evaluate(expression, testData); // Assert Assert.That(result, Is.True); } [Test] public void NewFeature_InvalidInput_ShouldThrowException() { // Test error conditions } [TestCase(/* parameters */)] public void NewFeature_ParameterizedTests(/* parameters */) { // Parameterized test cases } } 
Enter fullscreen mode Exit fullscreen mode

Pull Request Process

  1. Create a feature branch
 git checkout -b feature/your-feature-name 
Enter fullscreen mode Exit fullscreen mode
  1. Make your changes with tests

  2. Ensure all tests pass

 dotnet test --verbosity normal 
Enter fullscreen mode Exit fullscreen mode
  1. Update documentation if needed

  2. Submit pull request with clear description

Reporting Issues

When reporting bugs, please include:

  1. Environment details (.NET version, OS)
  2. Minimal reproduction case
  3. Expected vs actual behavior
  4. Error messages or stack traces
// Example bug report template var testData = new { /* minimal object */ }; var expression = "/* problematic expression */"; // Expected: true // Actual: false (or exception) var result = JSONPredicate.Evaluate(expression, testData); 
Enter fullscreen mode Exit fullscreen mode

FAQ

General Questions

Q: What's the performance impact of using JSONPathPredicate?
A: JSONPathPredicate is highly optimized for performance. Simple expressions evaluate at ~2M ops/second, complex expressions at ~500K ops/second. The main overhead is JSON serialization, so consider using lightweight projection objects for high-volume scenarios.

Q: Can I use JSONPathPredicate with Entity Framework?
A: JSONPathPredicate works with in-memory collections. For Entity Framework, first execute your database query, then apply JSONPathPredicate filters to the results in memory.

Q: Is JSONPathPredicate thread-safe?
A: Yes, JSONPathPredicate is stateless and thread-safe. You can safely call JSONPredicate.Evaluate() from multiple threads simultaneously.

Q: What's the maximum expression complexity supported?
A: There's no hard limit on expression complexity. However, very deep nesting or extremely long expressions may impact performance. Consider breaking complex rules into smaller, composed expressions.

Technical Questions

Q: How does type conversion work?
A: JSONPathPredicate automatically converts between compatible types:

  • String numbers to numeric types for comparisons
  • Case-insensitive string comparisons
  • DateTime string parsing with multiple format support
  • Boolean value interpretation

Q: Can I extend JSONPathPredicate with custom operators?
A: The current version doesn't support custom operators. This is a planned feature for future releases. Consider using composition of existing operators for now.

Q: How are null values handled?
A: Null values are handled gracefully:

  • null eq null returns true
  • null eq "anything" returns false
  • Missing properties are treated as null

Q: What JSONPath features are supported?
A: Currently supports:

  • Property navigation with dot notation (object.property)
  • Nested object access (object.nested.property)

Not yet supported:

  • Array indexing (array[0])
  • Wildcards (object.*)
  • Recursive descent (object..property)

Best Practices Questions

Q: How should I structure complex business rules?
A: Consider these patterns:

  1. Use a rule engine pattern with named rules
  2. Break complex expressions into smaller, testable parts
  3. Use configuration files for business rules
  4. Implement rule versioning for changes over time

Q: How do I handle user-provided expressions safely?
A: Implement validation layers:

  1. Whitelist allowed JSONPaths
  2. Validate expression syntax before execution
  3. Use try-catch patterns for graceful error handling
  4. Consider rate limiting for user-provided expressions

Q: What's the best way to debug failing expressions?
A: Use these debugging techniques:

  1. Break complex expressions into parts
  2. Inspect your data structure with JSON serialization
  3. Test individual conditions separately
  4. Use the debugging utilities provided in the troubleshooting section

Top comments (0)