This article provides complete information on JSONPathPredicate
libray.
Table of Contents
- Home
- Getting Started
- Expression Syntax
- Operators Reference
- Advanced Usage
- Best Practices
- Performance Considerations
- API Reference
- Examples
- Troubleshooting
- Contributing
- 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);
Getting Started
Installation
Install JSONPathPredicate via NuGet Package Manager:
Package Manager Console
Install-Package JsonPathPredicate
.NET CLI
dotnet add package JsonPathPredicate
PackageReference
<PackageReference Include="JsonPathPredicate" Version="1.0.0" />
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
- Import the namespace:
using JSONPathPredicate;
- Create your data object:
var data = new { user = new { name = "Alice", age = 30, roles = new[] { "admin", "user" } }, settings = new { theme = "dark", notifications = true } };
- Evaluate expressions:
bool result = JSONPredicate.Evaluate("user.age gte 18", data); // Returns: true
Your First Expression
Let's break down a simple expression:
string expression = "user.name eq `Alice`"; bool result = JSONPredicate.Evaluate(expression, data);
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]
For complex expressions:
[Expression] [Logical Operator] [Expression]
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);
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\""
Numeric Values
Numbers can be written directly:
"age eq 25" "score eq 95.5" "count eq -10"
Boolean Values
Boolean values are case-insensitive:
"isActive eq true" "isEnabled eq false"
DateTime Values
DateTime values should be in ISO 8601 format:
"createdAt eq `2024-08-01T10:30:00Z`" "birthDate gt `1990-01-01`"
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
Comments and Whitespace
Expressions are whitespace-tolerant:
// All equivalent "user.name eq `John`" "user.name eq `John`" " user.name eq `John` "
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
Type Conversion Examples:
var data = new { count = "25" }; // String value bool result = JSONPredicate.Evaluate("count eq 25", data); // Returns true
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
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)
Single value membership:
var data = new { role = "admin" }; // Check if role is in allowed list "role in (`admin`, `moderator`, `user`)" // true
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")
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
Greater Than or Equal (gte
)
"age gte 18" // age >= 18 "rating gte 4.5" // rating >= 4.5
Less Than (lt
)
"age lt 65" // age < 65 "price lt 100.00" // price < 100.00
Less Than or Equal (lte
)
"discount lte 50" // discount <= 50 "temperature lte 32.0" // temperature <= 32.0
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"
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`"
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'
Operator Precedence
The evaluation order follows these precedence rules:
- Parentheses (highest precedence)
- Comparison operators (
eq
,not
,in
,gt
,gte
,lt
,lte
) - AND operator
- 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
Special Cases and Edge Conditions
Null Handling
var data = new { nullableField = (string)null }; "nullableField eq `test`" // Returns false "nullableField not `test`" // Returns true
Empty Collections
var data = new { tags = new string[0] }; "tags in (`anything`)" // Returns false
Type Mismatches
var data = new { textNumber = "25", realNumber = 25 }; "textNumber eq 25" // Returns true (automatic conversion) "textNumber gt 20" // Returns true (automatic conversion)
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);
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);
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)
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`"
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);
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);
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);
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
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);
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); } }
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`"
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`"
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'"
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`))";
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`"
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; } } }
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; } } }
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 } }
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>(); } }
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)); } }
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 } }
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
- 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`"
- Use Positive Conditions
// Preferred "status eq `active`" // Less efficient "status not `inactive`"
- Minimize Deep Nesting
// Consider flattening very deep paths "user.profile.settings.preferences.theme eq `dark`"
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);
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); } }
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; } }
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 } );
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);
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_]*
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; } }
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"; } }
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) ); } }
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(); }
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; } } }
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}"); } } }
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
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); } } }
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)"
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`"
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 }));
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`)"
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}"); } } } }
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; } } }
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}"); } }
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
- Clone the repository
git clone https://github.com/CodeShayk/JSONPathPredicate.git cd JSONPathPredicate
- Install dependencies
dotnet restore
- Build the solution
dotnet build
- Run tests
dotnet test
Contribution Guidelines
Code Standards
- Follow C# coding conventions
- Use meaningful variable and method names
- Add XML documentation for public APIs
- Include unit tests for new features
- 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 } }
Pull Request Process
- Create a feature branch
git checkout -b feature/your-feature-name
Make your changes with tests
Ensure all tests pass
dotnet test --verbosity normal
Update documentation if needed
Submit pull request with clear description
Reporting Issues
When reporting bugs, please include:
- Environment details (.NET version, OS)
- Minimal reproduction case
- Expected vs actual behavior
- 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);
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
returnstrue
-
null eq "anything"
returnsfalse
- 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:
- Use a rule engine pattern with named rules
- Break complex expressions into smaller, testable parts
- Use configuration files for business rules
- Implement rule versioning for changes over time
Q: How do I handle user-provided expressions safely?
A: Implement validation layers:
- Whitelist allowed JSONPaths
- Validate expression syntax before execution
- Use try-catch patterns for graceful error handling
- Consider rate limiting for user-provided expressions
Q: What's the best way to debug failing expressions?
A: Use these debugging techniques:
- Break complex expressions into parts
- Inspect your data structure with JSON serialization
- Test individual conditions separately
- Use the debugging utilities provided in the troubleshooting section
Top comments (0)