Skip to content
Najaf Shaikh edited this page Aug 12, 2025 · 4 revisions

JSONPathPredicate - Developer Guide

Table of Contents

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

Home

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.

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

  1. Import the namespace:

    using JSONPathPredicate;
  2. Create your data object:

    var data = new {
        user = new {
            name = "Alice",
            age = 30,
            roles = new[] { "admin", "user" }
        },
        settings = new {
            theme = "dark",
            notifications = true
        }
    };
  3. 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:

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

Examples:

// Without parentheses
"a eq 1 or b eq 2 and c eq 3"
// Evaluates as: a eq 1 or (b eq 2 and c eq 3)

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

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

  1. Order Conditions by Selectivity

    // Good: Most selective first
    "user.role eq `admin` and user.isActive eq true"
    
    // Less optimal: Less selective first
    "user.isActive eq true and user.role eq `admin`"
  2. Use Positive Conditions

    // Preferred
    "status eq `active`"
    
    // Less efficient
    "status not `inactive`"
  3. 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

  1. Clone the repository

    git clone https://github.com/CodeShayk/JSONPathPredicate.git
    cd JSONPathPredicate
  2. Install dependencies

    dotnet restore
  3. Build the solution

    dotnet build
  4. Run tests

    dotnet test

Contribution Guidelines

Code Standards

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

Testing Requirements

All contributions must include comprehensive tests:

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

Pull Request Process

  1. Create a feature branch

    git checkout -b feature/your-feature-name
  2. Make your changes with tests

  3. Ensure all tests pass

    dotnet test --verbosity normal
  4. Update documentation if needed

  5. Submit pull request with clear description

Reporting Issues

When reporting bugs, please include:

  1. Environment details (.NET version, OS)
  2. Minimal reproduction case
  3. Expected vs actual behavior
  4. Error messages or stack traces
// Example bug report template
var testData = new { /* minimal object */ };
var expression = "/* problematic expression */";

// Expected: true
// Actual: false (or exception)
var result = JSONPredicate.Evaluate(expression, testData);

FAQ

General Questions

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

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

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

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

Technical Questions

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

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

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

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

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

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

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

Not yet supported:

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

Best Practices Questions

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

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

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

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

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

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

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.

Clone this wiki locally