Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 0
Home
Welcome to the JSONPredicate documentation! This library provides a powerful and intuitive way to evaluate string-based predicate expressions against JSON objects using JSONPath syntax in .NET applications.
- Getting Started
- Expression Syntax
- Operators Reference
- Advanced Usage
- Best Practices
- Performance Considerations
- API Reference
- Examples
- Troubleshooting
- Contributing
- FAQ
JSONPredicate 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
- 🔗 Array Indexing: NEW in v1.2.0: Access array elements using
array[0].propertynotation - 🔤 Extended Operators: NEW in v1.2.0:
starts_with,ends_with, andcontainsstring operators - 🧵 Thread Safety: NEW in v1.2.0: Thread-safe operation in multi-threaded environments
varcustomer=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"),tags=new[]{"vip","early-adopter","premium"}// NEW in v1.2.0: Array for indexing};// Simple equality checkboolisJohn=JSONPredicate.Evaluate("profile.name eq `John Doe`",customer);// Complex logical expressionboolisEligible=JSONPredicate.Evaluate("profile.age gte 18 and profile.isActive eq true and score gt 90",customer);// Array containment with date comparisonboolhasRecentActivity=JSONPredicate.Evaluate("orders in (`premium`, `vip`) and lastLogin gt `2024-07-01`",customer);// NEW in v1.2.0: Array indexingboolisFirstOrderPremium=JSONPredicate.Evaluate("orders[0] eq `premium`",customer);// NEW in v1.2.0: String operatorsboolisVipUser=JSONPredicate.Evaluate("tags[0] starts_with `v`",customer);boolisEarlyAdopter=JSONPredicate.Evaluate("tags contains `early`",customer);boolhasPremiumTag=JSONPredicate.Evaluate("tags[2] ends_with `ium`",customer);Install JSONPredicate via NuGet Package Manager:
Install-Package JsonPathPredicatedotnet add package JsonPathPredicate<PackageReferenceInclude="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:
usingJSONPredicate;
Create your data object:
vardata=new{user=new{name="Alice",age=30,roles=new[]{"admin","user"}},settings=new{theme="dark",notifications=true}};
Evaluate expressions:
boolresult=JSONPredicate.Evaluate("user.age gte 18",data);// Returns: true
Let's break down a simple expression:
stringexpression="user.name eq `Alice`";boolresult=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 JSONPredicate 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:
vardata=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 propertiesboolresult1=JSONPredicate.Evaluate("customer.profile.personal.firstName eq `John`",data);boolresult2=JSONPredicate.Evaluate("customer.profile.address.city eq `New York`",data);boolresult3=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 18Expressions 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 matchType Conversion Examples:
vardata=new{count="25"};// String valueboolresult=JSONPredicate.Evaluate("count eq 25",data);// Returns trueTests 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 trueTests if a value is contained within a collection or if collections intersect.
Single value against collection:
vardata=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:
vardata=new{role="admin"};// Check if role is in allowed list"role in (`admin`, `moderator`, `user`)"// trueCollection intersection:
vardata=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-01Greater Than or Equal (gte)
"age gte 18"// age >= 18"rating gte 4.5"// rating >= 4.5Less Than (lt)
"age lt 65"// age < 65"price lt 100.00"// price < 100.00Less Than or Equal (lte)
"discount lte 50"// discount <= 50"temperature lte 32.0"// temperature <= 32.0Numeric Type Handling: The library automatically handles different numeric types:
vardata=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 3vardata=new{nullableField=(string)null};"nullableField eq `test`"// Returns false"nullableField not `test`"// Returns truevardata=new{tags=newstring[0]};"tags in (`anything`)"// Returns falsevardata=new{textNumber="25",realNumber=25};"textNumber eq 25"// Returns true (automatic conversion)"textNumber gt 20"// Returns true (automatic conversion)JSONPredicate excels at handling complex logical expressions with multiple levels of nesting:
varcustomer=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 checkboolisEligibleForOffer=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 fallbacksboolhasSpecialAccess=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);varuser=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 roleboolisAdmin=JSONPredicate.Evaluate("roles in (`admin`)",user);// Check for any admin permissionsboolhasAdminPerms=JSONPredicate.Evaluate("permissions in (`admin`, `super-admin`)",user);// Check for multiple tag requirementsboolisPremiumUser=JSONPredicate.Evaluate("tags in (`premium`, `vip`) and roles in (`user`, `admin`)",user);// Complex array operationsboolcanAccessFeature=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:
vardata=new{userCategories=new[]{"electronics","books","sports"},allowedCategories=new[]{"electronics","home","garden"},blockedCategories=new[]{"adult","gambling"}};// Check if user has access to any allowed categoriesboolhasAccess=JSONPredicate.Evaluate("userCategories in allowedCategories",data);// true (electronics matches)// Check if user is accessing blocked contentboolisBlocked=JSONPredicate.Evaluate("userCategories in blockedCategories",data);// false (no intersection)JSONPredicate provides robust DateTime handling with support for multiple formats:
varevents=new{createdAt=DateTime.Parse("2024-08-01T10:30:00Z"),updatedAt="2024-08-02T15:45:00Z",// String will be parsedscheduledFor="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 filteringboolinDateRange=JSONPredicate.Evaluate(@" createdAt gte `2024-08-01` and createdAt lte `2024-08-31`",events);// Business hours checkvartransaction=new{timestamp=DateTime.Parse("2024-08-01T14:30:00Z"),amount=250.00};boolduringBusinessHours=JSONPredicate.Evaluate(@" timestamp gte `2024-08-01T09:00:00Z` and timestamp lte `2024-08-01T17:00:00Z`",transaction);vardocument=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 yearboolthisYear=JSONPredicate.Evaluate("createdAt gte `2024-01-01`",document);// Check if recently modified (within last 30 days of a reference point)boolrecentlyModified=JSONPredicate.Evaluate("modifiedAt gte `2024-07-01`",document);JSONPredicate automatically handles type conversions for seamless comparisons:
varmixedData=new{stringNumber="42",intNumber=42,floatNumber=42.0f,doubleNumber=42.0,decimalNumber=42.0m};// All these expressions return true due to automatic conversionboolresult1=JSONPredicate.Evaluate("stringNumber eq 42",mixedData);boolresult2=JSONPredicate.Evaluate("intNumber eq 42.0",mixedData);boolresult3=JSONPredicate.Evaluate("floatNumber eq 42",mixedData);boolresult4=JSONPredicate.Evaluate("stringNumber eq floatNumber",mixedData);vartextData=new{singleChar='A',charString="A",name="John",upperName="JOHN"};// Case-insensitive string comparisonsboolmatch1=JSONPredicate.Evaluate("name eq `john`",textData);// trueboolmatch2=JSONPredicate.Evaluate("name eq `JOHN`",textData);// true// Character-string interoperabilityboolmatch3=JSONPredicate.Evaluate("singleChar eq `A`",textData);// trueboolmatch4=JSONPredicate.Evaluate("charString eq singleChar",textData);// truevarflags=new{isActive=true,isEnabled="true",isValid=1,// Truthy valueisFlagged=false};// Boolean comparisons with type conversionboolresult=JSONPredicate.Evaluate("isActive eq true",flags);For applications that repeatedly evaluate the same expressions, consider implementing a caching strategy:
publicclassPredicateCache{privatereadonlyDictionary<string,CompiledPredicate>_cache=new();publicboolEvaluate(stringexpression,objectdata){// In a real implementation, you might want to compile expressions// for better performance on repeated evaluationsreturnJSONPredicate.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 expressionsstringcomplexRule=@" (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`"publicclassPredicateValidator{publicboolTryValidateExpression(stringexpression,outstringerror){try{// Test with dummy object to validate syntaxvartestObj=new{test="value"};JSONPredicate.Evaluate("test eq `value`",testObj);error=null;returntrue;}catch(ArgumentExceptionex){error=ex.Message;returnfalse;}}}publicstaticclassSafeJSONPredicate{publicstaticboolTryEvaluate(stringexpression,objectdata,outboolresult){try{result=JSONPredicate.Evaluate(expression,data);returntrue;}catch(Exception){result=false;returnfalse;}}publicstaticboolEvaluateWithDefault(stringexpression,objectdata,booldefaultValue=false){try{returnJSONPredicate.Evaluate(expression,data);}catch(Exception){returndefaultValue;}}}When accepting user-provided expressions, implement proper validation:
publicclassExpressionSanitizer{privatereadonlyHashSet<string>_allowedOperators=newHashSet<string>{"eq","not","in","gt","gte","lt","lte","and","or"};privatereadonlyHashSet<string>_allowedPaths=newHashSet<string>{"user.name","user.age","user.role","account.balance"// Define allowed JSONPaths};publicboolIsValidExpression(stringexpression){// Implement validation logic here// Check for allowed operators and paths onlyreturntrue;// Simplified for example}}Limit which properties can be accessed:
publicclassRestrictedJSONPredicate{privatereadonlyHashSet<string>_allowedPaths;publicRestrictedJSONPredicate(IEnumerable<string>allowedPaths){_allowedPaths=newHashSet<string>(allowedPaths);}publicboolEvaluate(stringexpression,objectdata){// Extract paths from expression and validatevarpaths=ExtractPaths(expression);if(paths.Any(p =>!_allowedPaths.Contains(p))){thrownewUnauthorizedAccessException("Expression contains restricted paths");}returnJSONPredicate.Evaluate(expression,data);}privateIEnumerable<string>ExtractPaths(stringexpression){// Implementation to extract JSONPaths from expression// This is a simplified examplereturnnewList<string>();}}[TestFixture]publicclassBusinessRuleTests{privatereadonlyobject_testCustomer=new{profile=new{name="John Doe",age=35,membershipLevel="gold"},account=new{balance=2500.00,status="active"},preferences=new{emailNotifications=true}};[Test]publicvoidPremiumCustomerRule_ShouldReturnTrue_WhenCriteriaMet(){varrule="profile.age gte 18 and account.balance gt 1000 and profile.membershipLevel in (`gold`, `platinum`)";varresult=JSONPredicate.Evaluate(rule,_testCustomer);Assert.That(result,Is.True);}[TestCase("profile.age",35,true)][TestCase("profile.age",17,false)]publicvoidAgeValidation_ShouldWorkCorrectly(stringpath,intage,boolexpected){varcustomer=new{profile=new{age=age}};varrule="profile.age gte 18";varresult=JSONPredicate.Evaluate(rule,customer);Assert.That(result,Is.EqualTo(expected));}}publicclassExpressionCoverageHelper{publicstaticvoidTestAllOperators(objecttestData){varoperators=new[]{"eq","not","in","gt","gte","lt","lte"};varlogicalOps=new[]{"and","or"};foreach(varopinoperators){// Test each operator with various data typesTestOperator(op,testData);}}privatestaticvoidTestOperator(stringop,objectdata){// Implementation to systematically test operators}}Based on internal testing, JSONPredicate 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`"
JSONPredicate serializes objects to JSON for evaluation. Consider these patterns:
// Good: Lightweight evaluation objectsvarslimData=new{userId=user.Id,role=user.Role,isActive=user.IsActive};boolresult=JSONPredicate.Evaluate("role eq `admin`",slimData);// Less optimal: Heavy objects with unnecessary databoolresult2=JSONPredicate.Evaluate("role eq `admin`",fullUserObjectWithManyProperties);For repeated evaluations, consider implementing caching:
publicclassCachedPredicateEvaluator{privatereadonlyLRUCache<string,object>_objectCache=new(1000);publicboolEvaluate(stringexpression,objectdata){stringdataKey=GenerateKey(data);if(!_objectCache.TryGetValue(dataKey,outobjectcachedData)){cachedData=data;_objectCache[dataKey]=cachedData;}returnJSONPredicate.Evaluate(expression,cachedData);}}For applications processing thousands of evaluations per second:
publicclassHighPerformancePredicateService{privatereadonlyConcurrentDictionary<string,CompiledExpression>_expressionCache=new();publicasyncTask<bool[]>EvaluateBatchAsync(stringexpression,IEnumerable<object>dataItems){vartasks=dataItems.Select(data =>Task.Run(()=>JSONPredicate.Evaluate(expression,data)));returnawaitTask.WhenAll(tasks);}publicboolEvaluateWithMetrics(stringexpression,objectdata,outTimeSpanduration){varstopwatch=Stopwatch.StartNew();varresult=JSONPredicate.Evaluate(expression,data);duration=stopwatch.Elapsed;returnresult;}}// Pattern: Use projection for large objectspublicstaticclassEfficientEvaluation{publicstaticboolEvaluateProjected<T>(stringexpression,Tsource,Func<T,object>projector){varprojectedData=projector(source);returnJSONPredicate.Evaluate(expression,projectedData);}}// Usagevarresult=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 evaluateobj(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 invalidFormatException: Thrown when DateTime parsing failsInvalidOperationException: Thrown for unsupported operations
Example:
vardata=new{name="John",age=25};boolresult=JSONPredicate.Evaluate("name eq `John` and age gte 18",data);The formal grammar for JSONPredicate 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.
publicclassAuthService{publicboolIsAuthorized(Useruser,stringresource,stringaction){varcontext=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 overrideif(JSONPredicate.Evaluate("user.role eq `admin` and user.isActive eq true",context))returntrue;// Check specific permissionsvarpermissionRule=$"user.permissions in (`{resource}:{action}`, `{resource}:*`, `*:*`)";if(JSONPredicate.Evaluate($"{permissionRule} and user.isActive eq true",context))returntrue;// Account expiry checkif(JSONPredicate.Evaluate("user.accountExpiry lt currentTime",context))returnfalse;returnfalse;}}publicclassProductFilterService{publicIEnumerable<Product>FilterProducts(IEnumerable<Product>products,ProductFilterCriteriacriteria){varfilterExpression=BuildFilterExpression(criteria);returnproducts.Where(product =>{varproductData=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};returnJSONPredicate.Evaluate(filterExpression,productData);});}privatestringBuildFilterExpression(ProductFilterCriteriacriteria){varconditions=newList<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){vartagsList=string.Join("`, `",criteria.Tags);conditions.Add($"tags in (`{tagsList}`)");}if(criteria.OnSaleOnly)conditions.Add("onSale eq true");returnconditions.Any()?string.Join(" and ",conditions):"inStock eq true";}}publicclassBusinessRuleEngine{privatereadonlyDictionary<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)"};publicboolEvaluateRule(stringruleName,objectcontext){if(!_rules.TryGetValue(ruleName,outvarexpression))thrownewArgumentException($"Unknown rule: {ruleName}");returnJSONPredicate.Evaluate(expression,context);}publicDictionary<string,bool>EvaluateAllRules(objectcontext){return_rules.ToDictionary(
kvp =>kvp.Key,
kvp =>JSONPredicate.Evaluate(kvp.Value,context));}}publicclassConfigurableValidator{publicclassValidationRule{publicstringName{get;set;}publicstringExpression{get;set;}publicstringErrorMessage{get;set;}publicboolIsWarning{get;set;}}privatereadonlyList<ValidationRule>_rules=new(){newValidationRule{Name="AGE_VALIDATION",Expression="user.age gte 18 and user.age lte 120",ErrorMessage="User age must be between 18 and 120",IsWarning=false},newValidationRule{Name="EMAIL_DOMAIN_CHECK",Expression="user.email in (`@company.com`, `@partner.com`)",ErrorMessage="Only company or partner email addresses are allowed",IsWarning=true}};publicValidationResultValidate(objectdata){varresult=newValidationResult{IsValid=true};foreach(varrulein_rules){try{boolisValid=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(Exceptionex){result.Errors.Add($"Rule '{rule.Name}' evaluation failed: {ex.Message}");result.IsValid=false;}}returnresult;}}publicclassValidationResult{publicboolIsValid{get;set;}publicList<string>Errors{get;set;}=new();publicList<string>Warnings{get;set;}=new();}publicclassContentPersonalizationService{privatereadonlyDictionary<string,ContentRule>_contentRules=new(){["SHOW_PREMIUM_CTA"]=newContentRule{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"]=newContentRule{Expression="user.subscriptionExpiry lt `2024-12-31` and user.tier in (`premium`, `pro`)",Content="Your subscription expires soon. Renew now!"},["SHOW_WELCOME_MESSAGE"]=newContentRule{Expression="user.isNewUser eq true and user.lastLogin eq null",Content="Welcome! Let's get you started."}};publicList<string>GetPersonalizedContent(Useruser){varuserContext=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}};varapplicableContent=newList<string>();foreach(varrulein_contentRules){if(JSONPredicate.Evaluate(rule.Value.Expression,userContext)){applicableContent.Add(rule.Value.Content);}}returnapplicableContent;}privateclassContentRule{publicstringExpression{get;set;}publicstringContent{get;set;}}}// Startup.cs or Program.cspublicvoidConfigureServices(IServiceCollectionservices){services.AddScoped<IPredicateService,PredicateService>();}// Controller[ApiController][Route("api/[controller]")]publicclassUsersController:ControllerBase{privatereadonlyIPredicateService_predicateService;publicUsersController(IPredicateServicepredicateService){_predicateService=predicateService;}[HttpGet("filter")]publicIActionResultFilterUsers([FromQuery]stringexpression){if(string.IsNullOrEmpty(expression))returnBadRequest("Expression is required");try{varusers=GetUsers();// Your data sourcevarfilteredUsers=users.Where(user =>_predicateService.EvaluateUserFilter(expression,user));returnOk(filteredUsers);}catch(ArgumentExceptionex){returnBadRequest($"Invalid expression: {ex.Message}");}}}publicstaticclassQueryableExtensions{publicstaticIQueryable<T>WherePredicate<T>(thisIQueryable<T>source,stringexpression){returnsource.AsEnumerable().Where(item =>JSONPredicate.Evaluate(expression,item)).AsQueryable();}// For in-memory evaluation after database querypublicstaticIEnumerable<T>FilterWithPredicate<T>(thisIEnumerable<T>source,stringexpression){returnsource.Where(item =>JSONPredicate.Evaluate(expression,item));}}// UsagevaractiveAdminUsers=dbContext.Users.Where(u =>u.IsActive)// Database filter.ToList()// Execute database query.FilterWithPredicate("role eq `admin` and lastLogin gt `2024-01-01`");// In-memory predicatepublicclassRuleEvaluationService:BackgroundService{privatereadonlyIServiceProvider_serviceProvider;privatereadonlyILogger<RuleEvaluationService>_logger;publicRuleEvaluationService(IServiceProviderserviceProvider,ILogger<RuleEvaluationService>logger){_serviceProvider=serviceProvider;_logger=logger;}protectedoverrideasyncTaskExecuteAsync(CancellationTokenstoppingToken){while(!stoppingToken.IsCancellationRequested){usingvarscope=_serviceProvider.CreateScope();varbusinessRuleEngine=scope.ServiceProvider.GetRequiredService<BusinessRuleEngine>();try{awaitProcessPendingRules(businessRuleEngine);}catch(Exceptionex){_logger.LogError(ex,"Error processing business rules");}awaitTask.Delay(TimeSpan.FromMinutes(5),stoppingToken);}}privateasyncTaskProcessPendingRules(BusinessRuleEngineruleEngine){varpendingEvaluations=GetPendingEvaluations();foreach(varevaluationinpendingEvaluations){varresult=ruleEngine.EvaluateRule(evaluation.RuleName,evaluation.Context);awaitProcessRuleResult(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 correctlyvardata=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:
vardata=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,newJsonSerializerOptions{WriteIndented=true}));Problem:in operator not working as expected
Common Causes:
- Misunderstanding of array intersection logic
- Incorrect value syntax
Solutions:
vardata=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 arrayvarsingleTag=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`)"publicstaticclassPredicateDebugger{publicstaticvoidDebugExpression(stringexpression,objectdata){Console.WriteLine($"Expression: {expression}");Console.WriteLine($"Data: {JsonSerializer.Serialize(data,newJsonSerializerOptions{WriteIndented=true})}");try{varresult=JSONPredicate.Evaluate(expression,data);Console.WriteLine($"Result: {result}");}catch(Exceptionex){Console.WriteLine($"Error: {ex.Message}");}}publicstaticvoidTestExpressionParts(stringexpression,objectdata){// Break down complex expressions into partsvarparts=SplitExpression(expression);foreach(varpartinparts){try{varresult=JSONPredicate.Evaluate(part,data);Console.WriteLine($"'{part}' -> {result}");}catch(Exceptionex){Console.WriteLine($"'{part}' -> ERROR: {ex.Message}");}}}}publicstaticclassDataInspector{publicstaticvoidInspectObject(objectobj){varjson=JsonSerializer.Serialize(obj,newJsonSerializerOptions{WriteIndented=true,PropertyNamingPolicy=JsonNamingPolicy.CamelCase});Console.WriteLine("Object structure:");Console.WriteLine(json);// Extract all possible JSONPathsvarpaths=ExtractAllPaths(obj);Console.WriteLine("\nAvailable JSONPaths:");foreach(varpathinpaths){Console.WriteLine($" {path}");}}privatestaticList<string>ExtractAllPaths(objectobj,stringprefix=""){varpaths=newList<string>();varjson=JsonSerializer.Serialize(obj);usingvardoc=JsonDocument.Parse(json);ExtractPathsRecursive(doc.RootElement,prefix,paths);returnpaths;}privatestaticvoidExtractPathsRecursive(JsonElementelement,stringcurrentPath,List<string>paths){switch(element.ValueKind){caseJsonValueKind.Object:foreach(varpropinelement.EnumerateObject()){varnewPath=string.IsNullOrEmpty(currentPath)?prop.Name:$"{currentPath}.{prop.Name}";paths.Add(newPath);ExtractPathsRecursive(prop.Value,newPath,paths);}break;}}}publicstaticclassPerformanceProfiler{publicstaticvoidProfileExpression(stringexpression,objectdata,intiterations=1000){// Warmupfor(inti=0;i<100;i++){JSONPredicate.Evaluate(expression,data);}varstopwatch=Stopwatch.StartNew();for(inti=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/JSONPredicate.git cd JSONPredicateInstall 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]publicclassNewFeatureTests{[Test]publicvoidNewFeature_ValidInput_ShouldReturnExpectedResult(){// ArrangevartestData=new{/* test object */};varexpression="/* your expression */";// Actvarresult=JSONPredicate.Evaluate(expression,testData);// AssertAssert.That(result,Is.True);}[Test]publicvoidNewFeature_InvalidInput_ShouldThrowException(){// Test error conditions}[TestCase(/* parameters */)]publicvoidNewFeature_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 normalUpdate 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 templatevartestData=new{/* minimal object */};varexpression="/* problematic expression */";// Expected: true// Actual: false (or exception)varresult=JSONPredicate.Evaluate(expression,testData);Q: What's the performance impact of using JSONPredicate? A: JSONPredicate 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 JSONPredicate with Entity Framework? A: JSONPredicate works with in-memory collections. For Entity Framework, first execute your database query, then apply JSONPredicate filters to the results in memory.
Q: Is JSONPredicate thread-safe?
A: Yes, JSONPredicate 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: JSONPredicate 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 JSONPredicate 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 nullreturnstruenull 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.