-
Notifications
You must be signed in to change notification settings - Fork 0
Home
- Home
- Getting Started
- Expression Syntax
- Operators Reference
- Advanced Usage
- Best Practices
- Performance Considerations
- API Reference
- Examples
- Troubleshooting
- Contributing
- FAQ
Welcome to the JSONPathPredicate documentation! This library provides a powerful and intuitive way to evaluate string-based predicate expressions against JSON objects using JSONPath syntax in .NET applications.
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.
- 🎯 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
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);
Install JSONPathPredicate via NuGet Package Manager:
Install-Package JsonPathPredicate
dotnet add package JsonPathPredicate
<PackageReference Include="JsonPathPredicate" Version="1.0.0" />
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 |
-
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
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)
Every JSONPathPredicate expression follows this pattern:
[JSONPath] [Operator] [Value]
For complex expressions:
[Expression] [Logical Operator] [Expression]
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);
Values in expressions must be properly formatted:
Wrap string values in backticks, single quotes, or double quotes:
// All equivalent
"name eq `John`"
"name eq 'John'"
"name eq \"John\""
Numbers can be written directly:
"age eq 25"
"score eq 95.5"
"count eq -10"
Boolean values are case-insensitive:
"isActive eq true"
"isEnabled eq false"
DateTime values should be in ISO 8601 format:
"createdAt eq `2024-08-01T10:30:00Z`"
"birthDate gt `1990-01-01`"
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
Expressions are whitespace-tolerant:
// All equivalent
"user.name eq `John`"
"user.name eq `John`"
" user.name eq `John` "
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
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
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")
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"
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`"
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'
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
var data = new { nullableField = (string)null };
"nullableField eq `test`" // Returns false
"nullableField not `test`" // Returns true
var data = new { tags = new string[0] };
"tags in (`anything`)" // Returns false
var data = new { textNumber = "25", realNumber = 25 };
"textNumber eq 25" // Returns true (automatic conversion)
"textNumber gt 20" // Returns true (automatic conversion)
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);
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);
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)
JSONPathPredicate provides robust DateTime handling with support for multiple 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`"
// 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);
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);
JSONPathPredicate automatically handles type conversions for seamless comparisons:
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);
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
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);
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);
}
}
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`"
// Good: Clear property navigation
"user.profile.contactInfo.email eq `john@example.com`"
// Avoid: Unclear abbreviated paths
"u.p.ci.e eq `john@example.com`"
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'"
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`))";
// 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`"
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;
}
}
}
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;
}
}
}
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
}
}
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>();
}
}
[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));
}
}
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
}
}
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 |
-
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`"
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);
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);
}
}
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;
}
}
// 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 }
);
The main entry point for evaluating predicate expressions.
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);
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_]*
While these are internal implementation details, understanding them can help with troubleshooting:
Handles type comparisons and conversions.
Manages JSONPath navigation and property resolution.
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;
}
}
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";
}
}
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)
);
}
}
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();
}
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; }
}
}
// 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}");
}
}
}
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
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);
}
}
}
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)"
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`"
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 }));
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`)"
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}");
}
}
}
}
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;
}
}
}
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 | 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 |
-
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
- 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
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
}
}
-
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
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);
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.
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
)
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
The library provides a powerful, flexible way to evaluate complex conditional logic against JSON objects, making it ideal for business rules, filtering, validation, and many other use cases in .NET applications.