High-performance LINQ provider for JSON data that processes JSON directly without full deserialization. Supports standard string, UTF-8, streaming, and RFC 9535 compliant JSONPath operations. Dramatically improves performance and memory efficiency for medium to large JSON files through early termination and constant memory usage. Includes native IAsyncEnumerable support and .NET 10 async LINQ integration.
$ dotnet add package Blazing.Json.QueryableBlazing.Json.Queryable is a high-performance LINQ provider for JSON data that processes JSON directly without full deserialization. Unlike traditional approaches that load entire JSON files into memory and then apply LINQ queries, this library processes JSON as a stream, providing dramatic performance improvements and memory efficiency for medium to large JSON files.
This custom JSON LINQ provider supports standard string, UTF-8, streaming, and RFC 9535 compliant JSONPath operations powered by Blazing.Json.JSONPath. Whether you're working with multi-megabyte API responses, large data exports, or log files, Blazing.Json.Queryable enables you to query JSON data efficiently with the familiar LINQ syntax you already know.
IAsyncEnumerable<T> for real-time processingWhen working with medium to large JSON files, the traditional approach of deserializing to objects and then querying has significant disadvantages:
Traditional Approach:
// Traditional: Load ALL data into memory, then query
var json = File.ReadAllText("large-file.json"); // Load entire file
var all = JsonSerializer.Deserialize<List<Person>>(json); // Deserialize ALL records
var results = all.Where(p => p.Age > 25).Take(10).ToList(); // Query in-memory
Blazing.Json.Queryable Approach:
// Blazing.Json.Queryable: Stream and query simultaneously
await using var stream = File.OpenRead("large-file.json");
var results = await JsonQueryable<Person>.FromStream(stream)
.Where(p => p.Age > 25)
.Take(10)
.AsAsyncEnumerable()
.ToListAsync(); // Only deserializes matching records!
| Feature | Traditional | Blazing.Json.Queryable |
|---|---|---|
| Memory Usage | Loads entire file | Constant memory usage |
| Early Exit | Processes all records | Stops after Take(N) |
| Large Files | OutOfMemoryException risk | Handles files > RAM |
| Speed (Large + Take) | Slow (full deserialize) | 10-20x faster |
| Async Support | Manual implementation | Native IAsyncEnumerable |
| UTF-8 Processing | String conversion overhead | Zero-allocation Span<T> |
Use Blazing.Json.Queryable When:
Use Traditional JsonSerializer + LINQ When:
Install via NuGet Package Manager:
<PackageReference Include="Blazing.Json.Queryable" Version="1.1.0" />
Or via the .NET CLI:
dotnet add package Blazing.Json.Queryable
Or via the Package Manager Console:
Install-Package Blazing.Json.Queryable
using Blazing.Json.Queryable.Providers;
// From JSON string
var jsonString = """[{"Name":"Alice","Age":30},{"Name":"Bob","Age":25}]""";
var results = JsonQueryable<Person>.FromString(jsonString)
.Where(p => p.Age > 25)
.ToList();
// From file stream (memory-efficient)
await using var stream = File.OpenRead("data.json");
var streamResults = await JsonQueryable<Person>.FromStream(stream)
.Where(p => p.Age > 25)
.Take(10)
.AsAsyncEnumerable()
.ToListAsync();
// From UTF-8 bytes (zero-allocation)
byte[] utf8Json = Encoding.UTF8.GetBytes(jsonString);
var utf8Results = JsonQueryable<Person>.FromUtf8(utf8Json)
.Where(p => p.Age > 25)
.ToList();
public record Person(string Name, int Age);
Standard LINQ operations work as expected:
var json = """
[
{"Id":1,"Name":"Alice","Age":30,"City":"London","IsActive":true},
{"Id":2,"Name":"Bob","Age":25,"City":"Paris","IsActive":true},
{"Id":3,"Name":"Charlie","Age":35,"City":"London","IsActive":false}
]
""
// Filtering
var adults = JsonQueryable<Person>.FromString(json)
.Where(p => p.Age >= 18)
.ToList();
// Projection
var names = JsonQueryable<Person>.FromString(json)
.Select(p => p.Name)
.ToList();
// Ordering
var sorted = JsonQueryable<Person>.FromString(json)
.OrderBy(p => p.Age)
.ThenBy(p => p.Name)
.ToList();
// Aggregation
var avgAge = JsonQueryable<Person>.FromString(json)
.Average(p => p.Age);
var totalActive = JsonQueryable<Person>.FromString(json)
.Count(p => p.IsActive);
// Combined operations
var results = JsonQueryable<Person>.FromString(json)
.Where(p => p.Age >= 25 && p.City == "London")
.OrderByDescending(p => p.Age)
.Select(p => new { p.Name, p.Age })
.Take(5)
.ToList();
Process large files with constant memory usage:
// Traditional approach - loads entire file into memory
var traditionalResults = new List<Person>();
var allJson = File.ReadAllText("huge-dataset.json"); // Loads ALL 500MB
var allPeople = JsonSerializer.Deserialize<List<Person>>(allJson); // Deserializes ALL
traditionalResults = allPeople.Where(p => p.Age > 25).Take(100).ToList();
// Blazing.Json.Queryable - streams and stops early
var streamingResults = new List<Person>();
await using (var stream = File.OpenRead("huge-dataset.json"))
{
await foreach (var person in JsonQueryable<Person>.FromStream(stream)
.Where(p => p.Age > 25)
.Take(100) // Stops reading after 100 matches!
.AsAsyncEnumerable())
{
streamingResults.Add(person);
}
}
// Result: 10-20x faster, uses constant memory
.NET 10 includes built-in async LINQ support, enabling powerful async transformations:
await using var stream = File.OpenRead("data.json");
// Async predicates - call async methods in Where clauses
await foreach (var person in JsonQueryable<Person>.FromStream(stream)
.AsAsyncEnumerable()
.Where(async (p, ct) => await IsValidUserAsync(p, ct)) // Async predicate!
.OrderBy(p => p.Name)
.Take(100))
{
Console.WriteLine(person.Name);
}
// Async transformations with Select
await foreach (var enriched in JsonQueryable<Person>.FromStream(stream)
.AsAsyncEnumerable()
.Select(async (p, ct) => await EnrichPersonDataAsync(p, ct)) // Async select!
.Where(p => p.Score > 50))
{
await ProcessAsync(enriched);
}
// Cancellation support
using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(5));
await foreach (var item in JsonQueryable<Person>.FromStream(stream)
.AsAsyncEnumerable()
.WithCancellation(cts.Token))
{
// Automatically cancelled after 5 minutes
}
Process UTF-8 bytes without string conversion overhead:
// From UTF-8 byte array
byte[] utf8Json = Encoding.UTF8.GetBytes(jsonString);
var results = JsonQueryable<Person>.FromUtf8(utf8Json)
.Where(p => p.Age > 25)
.ToList();
// From ReadOnlySpan<byte> (zero-allocation)
ReadOnlySpan<byte> utf8Span = utf8Json.AsSpan();
var spanResults = JsonQueryable<Person>.FromUtf8(utf8Span)
.Where(p => p.Age > 25)
.ToList();
// From UTF-8 stream (most efficient for large data)
await using var utf8Stream = File.OpenRead("data.json");
await foreach (var person in JsonQueryable<Person>.FromStream(utf8Stream)
.AsAsyncEnumerable())
{
// Process each person with zero string allocations
}
Blazing.Json.Queryable supports both method syntax (fluent) and query syntax (query expression) for LINQ queries. Query syntax provides a declarative, SQL-like alternative that can be more readable for complex queries.
[!TIP] 📚 Learn More About LINQ Query Syntax:
var json = """[{"Name":"Alice","Age":30},{"Name":"Bob","Age":25},{"Name":"Charlie","Age":35}]""";
// METHOD SYNTAX (fluent) - Chain methods
var methodResults = JsonQueryable<Person>.FromString(json)
.Where(p => p.Age > 25)
.OrderBy(p => p.Name)
.Select(p => new { p.Name, p.Age })
.ToList();
// QUERY SYNTAX (declarative) - SQL-like keywords
var queryResults = (from p in JsonQueryable<Person>.FromString(json)
where p.Age > 25
orderby p.Name
select new { p.Name, p.Age })
.ToList();
// Both produce identical results!
1. FromString - Basic Query Syntax:
var results = (from p in JsonQueryable<Person>.FromString(json)
where p.Age > 25 && p.IsActive
select new { p.Name, p.City })
.ToList();
2. FromUtf8 - Zero-allocation UTF-8 Processing:
byte[] utf8Bytes = Encoding.UTF8.GetBytes(jsonString);
var results = (from p in JsonQueryable<Person>.FromUtf8(utf8Bytes)
where p.Age >= 30
orderby p.Name
select p)
.ToList();
3. FromFile - Direct File Access:
var results = (from p in JsonQueryable<Person>.FromFile("data.json")
where p.IsActive
orderby p.Age descending
select new { p.Name, p.Age })
.Take(10)
.ToList();
4. FromStream - Async Streaming:
await using var stream = File.OpenRead("data.json");
await foreach (var person in (from p in JsonQueryable<Person>.FromStream(stream)
where p.Age > 25
orderby p.Name
select p)
.Take(10)
.AsAsyncEnumerable())
{
Console.WriteLine($"{person.Name}, {person.Age}");
}
5. Multi-level Sorting:
var results = (from p in JsonQueryable<Person>.FromString(json)
orderby p.City, p.Age descending, p.Name
select p)
.ToList();
6. Grouping with Aggregations:
var results = (from p in JsonQueryable<Person>.FromString(json)
group p by p.City into cityGroup
select new
{
City = cityGroup.Key,
Count = cityGroup.Count(),
AvgAge = cityGroup.Average(p => p.Age),
MinAge = cityGroup.Min(p => p.Age),
MaxAge = cityGroup.Max(p => p.Age)
})
.ToList();
7. JSONPath Pre-filtering with Query Syntax:
// Combine RFC 9535 JSONPath filters with query syntax
var results = (from p in JsonQueryable<Product>
.FromString(json, "$[?@.price < 100 && @.stock > 0]")
group p by p.Category into catGroup
orderby catGroup.Key
select new
{
Category = catGroup.Key,
Count = catGroup.Count(),
AvgPrice = catGroup.Average(p => p.Price),
Products = catGroup.Select(p => p.Name).ToList()
})
.ToList();
8. Complex Real-World Query:
// Employee department analysis with filtering, grouping, and sorting
var results = (from e in JsonQueryable<Employee>.FromString(json)
where e.IsActive && e.Salary > 60000
group e by e.Department into deptGroup
where deptGroup.Count() > 1
orderby deptGroup.Average(e => e.Salary) descending
select new
{
Department = deptGroup.Key,
Count = deptGroup.Count(),
AvgSalary = deptGroup.Average(e => e.Salary),
TopEarner = deptGroup.OrderByDescending(e => e.Salary).First().Name
})
.ToList();
Both query syntax and method syntax are equally powerful and produce identical compiled code. The choice between them is purely a matter of readability and personal/team preference. Here's a side-by-side comparison to help you decide:
| Use Query Syntax When: | Use Method Syntax When: |
|---|---|
| Complex queries with joins and grouping (more SQL-like readability) | Simple filtering and projection |
Multiple from clauses (SelectMany scenarios) | Chaining many operations (more fluent) |
| Team prefers declarative style | Using methods not available in query syntax (Take, Skip, Distinct, etc.) |
Using let keyword for intermediate results | Personal/team preference for fluent style |
[!NOTE] Both syntaxes are fully supported and produce identical expression trees. You can even mix them:
var mixed = (from p in JsonQueryable<Person>.FromString(json) where p.Age > 25 select p) .Take(10) // Method syntax at the end .ToList();
[!TIP] See Full Examples: Check out
samples/Blazing.Json.Queryable.Samples/Examples/QuerySyntaxSamples.csfor comprehensive demonstrations of query syntax with all library features including FromUtf8, FromFile, FromStream, and JSONPath integration.
| Method | Description | Example |
|---|---|---|
Where | Filters elements based on a predicate | .Where(p => p.Age > 18) |
OfType<T> | Filters elements by type | .OfType<Employee>() |
| Method | Description | Example |
|---|---|---|
Select | Projects each element to a new form | .Select(p => p.Name) |
SelectMany | Flattens nested sequences | .SelectMany(p => p.Orders) |
Cast<T> | Casts elements to specified type | .Cast<Employee>() |
| Method | Description | Example |
|---|---|---|
OrderBy | Sorts elements in ascending order | .OrderBy(p => p.Age) |
OrderByDescending | Sorts elements in descending order | .OrderByDescending(p => p.Age) |
ThenBy | Secondary ascending sort | .ThenBy(p => p.Name) |
ThenByDescending | Secondary descending sort | .ThenByDescending(p => p.Name) |
Order | Sorts elements in ascending order (C# 14) | .Order() |
OrderDescending | Sorts elements in descending order (C# 14) | .OrderDescending() |
Reverse | Reverses the order of elements | .Reverse() |
| Method | Description | Example |
|---|---|---|
All | Tests if all elements satisfy a condition | .All(p => p.Age >= 18) |
Any | Tests if any element satisfies a condition | .Any(p => p.City == "London") |
Contains | Tests if sequence contains an element | .Contains(person) |
SequenceEqual | Tests if two sequences are equal | .SequenceEqual(other) |
| Method | Description | Example |
|---|---|---|
First | Returns first element | .First() |
FirstOrDefault | Returns first element or default | .FirstOrDefault() |
Last | Returns last element | .Last() |
LastOrDefault | Returns last element or default | .LastOrDefault() |
Single | Returns the only element | .Single() |
SingleOrDefault | Returns the only element or default | .SingleOrDefault() |
ElementAt | Returns element at specified index | .ElementAt(5) |
ElementAtOrDefault | Returns element at index or default | .ElementAtOrDefault(5) |
| Method | Description | Example |
|---|---|---|
Count | Counts elements | .Count() |
LongCount | Counts elements (64-bit) | .LongCount() |
Sum | Sums numeric values | .Sum(p => p.Salary) |
Average | Calculates average | .Average(p => p.Age) |
Min | Finds minimum value | .Min(p => p.Age) |
Max | Finds maximum value | .Max(p => p.Salary) |
MinBy | Finds element with minimum key value | .MinBy(p => p.Age) |
MaxBy | Finds element with maximum key value | .MaxBy(p => p.Salary) |
Aggregate | Applies custom accumulator | .Aggregate((a, b) => a + b) |
| Method | Description | Example |
|---|---|---|
Distinct | Removes duplicate elements | .Distinct() |
DistinctBy | Removes duplicates by key | .DistinctBy(p => p.Email) |
Union | Combines two sequences | .Union(otherPeople) |
UnionBy | Combines sequences by key | .UnionBy(other, p => p.Id) |
Intersect | Finds common elements | .Intersect(otherPeople) |
IntersectBy | Finds common elements by key | .IntersectBy(ids, p => p.Id) |
Except | Finds elements not in second sequence | .Except(otherPeople) |
ExceptBy | Finds elements not in second by key | .ExceptBy(ids, p => p.Id) |
| Method | Description | Example |
|---|---|---|
Take | Takes first N elements | .Take(10) |
TakeLast | Takes last N elements | .TakeLast(10) |
TakeWhile | Takes elements while condition is true | .TakeWhile(p => p.Age < 30) |
Skip | Skips first N elements | .Skip(20) |
SkipLast | Skips last N elements | .SkipLast(5) |
SkipWhile | Skips elements while condition is true | .SkipWhile(p => p.Age < 18) |
Chunk | Divides elements into chunks of specified size | .Chunk(10) |
| Method | Description | Example |
|---|---|---|
GroupBy | Groups elements by key | .GroupBy(p => p.City) |
GroupJoin | Groups and joins sequences | .GroupJoin(orders, ...) |
Join | Joins two sequences | .Join(orders, ...) |
| Method | Description | Example |
|---|---|---|
ToList | Converts to List<T> (synchronous) | .ToList() |
ToArray | Converts to array (synchronous) | .ToArray() |
ToDictionary | Converts to dictionary | .ToDictionary(p => p.Id) |
ToHashSet | Converts to hash set | .ToHashSet() |
ToLookup | Converts to lookup | .ToLookup(p => p.City) |
AsEnumerable | Returns as IEnumerable<T> | .AsEnumerable() |
AsQueryable | Returns as IQueryable<T> | .AsQueryable() |
AsAsyncEnumerable | Returns as IAsyncEnumerable<T> for async operations | .AsAsyncEnumerable() |
[!NOTE] For async conversion operations, use
.AsAsyncEnumerable()followed by .NET 10's built-in async LINQ methods:
await query.AsAsyncEnumerable().ToListAsync()- Async conversion to List<T>await query.AsAsyncEnumerable().ToArrayAsync()- Async conversion to arrayawait query.AsAsyncEnumerable().ToDictionaryAsync(...)- Async conversion to dictionary
| Method | Description | Example |
|---|---|---|
Concat | Concatenates two sequences | .Concat(otherPeople) |
Append | Appends element to end | .Append(person) |
Prepend | Prepends element to start | .Prepend(person) |
Zip | Combines sequences pairwise | .Zip(ages, (p, a) => ...) |
DefaultIfEmpty | Returns default if empty | .DefaultIfEmpty() |
Blazing.Json.Queryable provides powerful JSON filtering capabilities through Blazing.Json.JSONPath - a high-performance, 100% RFC 9535 compliant JSONPath implementation.
[!NOTE] 📖 Full JSONPath Documentation: For complete details on JSONPath syntax, features, and RFC 9535 compliance, visit the Blazing.Json.JSONPath repository. This library is automatically included as a dependency when you install Blazing.Json.Queryable.
JSONPath provides a powerful query language for JSON documents, similar to XPath for XML. When used with Blazing.Json.Queryable, JSONPath expressions pre-filter JSON data before deserialization, dramatically improving performance and reducing memory usage.
The RFC 9535 standard defines a consistent, interoperable syntax for:
==, !=, <, >, etc.) and logical operators (&&, ||, !)length(), count(), match(), search(), value()[start:end:step] with negative indicesPerformance Benefits:
// Traditional LINQ: Deserialize ALL 1M products, then filter in C#
var traditional = JsonQueryable<Product>.FromString(json)
.Where(p => p.Price < 100 && p.InStock && p.Category == "Electronics")
.ToList();
// Deserializes: 1,000,000 objects
// Memory: ~500MB, Time: ~2000ms
// JSONPath Pre-filter: Filter in JSON BEFORE deserialization (1M → 500 items)
var optimized = JsonQueryable<Product>
.FromString(json, "$[?@.price < 100 && @.inStock == true && @.category == 'Electronics']")
.ToList();
// Deserializes: 500 objects (only matches!)
// Memory: ~25MB (20x less!), Time: ~200ms (10x faster!)
Key Advantages:
| Feature | Syntax | Example | Description |
|---|---|---|---|
| Root | $ | $ | Root JSON element |
| Child | .property or ['property'] | $.data.users | Access nested property |
| Wildcard | * or [*] | $.users[*] | All array elements |
| Filter | [?expression] | $[?@.age > 25] | Filter by condition |
| Slice | [start:end] | $[2:10] | Array slice |
| Step | :step | $[::2] | Every 2nd element |
| Current | @ | @.price < 100 | Current element in filter |
| Comparison | ==, !=, <, <=, >, >= | @.price >= 50 | Comparison operators |
| Logical | &&, ||, ! | @.age > 18 && @.active | Logical operators |
| length() | length(value) | length(@.name) > 5 | String, array, or object length |
| match() | match(string, pattern) | match(@.email, '.*@example\\.com') | Full regex match (I-Regexp) |
| search() | search(string, pattern) | search(@.desc, 'wireless') | Substring regex search |
| count() | count(nodelist) | count($.items[*]) | Count nodes in nodelist |
| value() | value(nodelist) | value(@.tags[0]) | Convert singular nodelist to value |
Navigate to nested arrays using simple JSONPath expressions:
// API response: { "data": { "user": { "repositories": [...] } } }
var repos = JsonQueryable<Repository>
.FromString(apiResponse, "$.data.user.repositories[*]")
.Where(r => !r.IsPrivate)
.ToList();
Apply powerful filters directly in JSONPath (powered by Blazing.Json.JSONPath):
// Comparison operators: ==, !=, <, <=, >, >=
var highEarners = JsonQueryable<Employee>
.FromString(json, "$[?@.salary > 100000]")
.ToList();
// Logical operators: &&, ||, !
var seniorHighEarners = JsonQueryable<Employee>
.FromString(json, "$[?@.salary > 90000 && @.yearsEmployed >= 5]")
.ToList();
Blazing.Json.JSONPath provides all RFC 9535 standard functions:
// length() - string, array, or object length
var longNames = JsonQueryable<Product>
.FromString(json, "$[?length(@.name) > 10]")
.ToList();
// match() - full regex match (I-Regexp RFC 9485)
var electronics = JsonQueryable<Product>
.FromString(json, "$[?match(@.code, '^ELEC-.*')]")
.ToList();
// search() - substring regex search
var wireless = JsonQueryable<Product>
.FromString(json, "$[?search(@.name, 'Wireless')]")
.ToList();
Advanced array slicing with step support:
// [start:end] - elements from start to end (exclusive)
var middle = JsonQueryable<Item>
.FromString(json, "$[2:5]") // Elements 2, 3, 4
.ToList();
// [start:end:step] - every Nth element
var everyOther = JsonQueryable<Item>
.FromString(json, "$[0:9:2]") // Elements 0, 2, 4, 6, 8
.ToList();
// Negative indices - count from end
var last3 = JsonQueryable<Item>
.FromString(json, "$[-3:]") // Last 3 elements
.ToList();
// Reverse with negative step
var reversed = JsonQueryable<Item>
.FromString(json, "$[::-1]") // All elements in reverse
.ToList();
The real power: JSONPath pre-filtering + LINQ rich operations
var employeesJson = """
[
{"id": 1, "name": "Alice Johnson", "department": "Engineering", "salary": 95000, "yearsEmployed": 5},
{"id": 2, "name": "Bob Smith", "department": "Engineering", "salary": 105000, "yearsEmployed": 8},
{"id": 3, "name": "Charlie Brown", "department": "Sales", "salary": 75000, "yearsEmployed": 3},
{"id": 4, "name": "Diana Prince", "department": "Engineering", "salary": 120000, "yearsEmployed": 10},
{"id": 5, "name": "Eve Davis", "department": "Marketing", "salary": 70000, "yearsEmployed": 2},
{"id": 6, "name": "Frank Miller", "department": "Engineering", "salary": 98000, "yearsEmployed": 6}
]
""";
Console.WriteLine("\nDepartment workforce analysis [MIXED JSONPATH + LINQ]:");
var deptAnalysis = JsonQueryable<Employee>
.FromString(employeesJson, "$[?@.salary > 60000]") // JSONPath: Filter employees with salary > $60K
.GroupBy(e => e.Department) // LINQ: Group by department
.Select(g => new
{
Department = g.Key,
EmployeeCount = g.Count(),
AvgSalary = g.Average(e => e.Salary),
TotalYearsExperience = g.Sum(e => e.YearsEmployed),
TopEarner = g.OrderByDescending(e => e.Salary).First().Name
})
.OrderByDescending(x => x.AvgSalary) // LINQ: Primary sort by avg salary (descending)
.ThenBy(x => x.Department) // LINQ: Secondary sort by department name (ascending)
.ToList();
foreach (var dept in deptAnalysis)
{
Console.WriteLine($" - {dept.Department}: {dept.EmployeeCount} employees, " +
$"${dept.AvgSalary:N0} avg salary, " +
$"{dept.TotalYearsExperience} total years, " +
$"top earner: {dept.TopEarner}");
}
Output:
Department workforce analysis [MIXED JSONPATH + LINQ]:
- Engineering: 4 employees, $104,500 avg salary, 29 total years, top earner: Diana Prince
- Sales: 1 employees, $75,000 avg salary, 3 total years, top earner: Charlie Brown
- Marketing: 1 employees, $70,000 avg salary, 2 total years, top earner: Eve Davis
Performance Analysis:
$[?@.salary > 60000]): Filters in JSON before deserialization[!WARNING] JSONPath Memory Considerations: The RFC 9535 specification requires the entire JSON document to be loaded into memory as a
JsonDocumentfor advanced JSONPath features. Advanced filters, functions, and array slicing (e.g.,$[?@.price < 100],$[0:10],length(@.name)) will load the entire document before parsing, regardless of streaming.Exception: Simple wildcard-only paths (e.g.,
$.data[*],$.departments[*].employees[*]) are handled differently by Blazing.Json.Queryable and maintain true streaming without loading the full document, even with multiple levels of nesting.
Blazing.Json.JSONPath requires the entire JSON document to be loaded into memory as a JsonDocument when using advanced RFC 9535 features:
$[?@.age > 25], $[?@.price < 100 && @.inStock]$[0:10], $[2:5:2], $[-3:]length(), count(), match(), search(), value()However, simple wildcard-only paths use streaming and maintain constant memory usage:
$.data[*], $.users[*]$.departments[*].employees[*], $.organization[*].divisions[*].departments[*].employees[*]Memory Usage Guidelines:
DO: Use Simple Wildcard Paths for True Streaming (All File Sizes)
// GOOD: True streaming with simple wildcard paths (constant memory)
await using var stream = File.OpenRead("huge-file.json"); // 2GB file
// Single-level wildcard
var results1 = await JsonQueryable<Product>
.FromStream(stream, "$.data[*]") // Simple wildcard - maintains streaming!
.Where(p => p.Price < 100 && p.InStock) // LINQ filtering (streamed)
.Take(10)
.AsAsyncEnumerable()
.ToListAsync();
// Multi-level wildcards - ALSO streams!
var results2 = await JsonQueryable<Employee>
.FromStream(stream, "$.departments[*].employees[*]") // Multi-level - still streams!
.Where(e => e.Salary > 60000) // LINQ filtering (streamed)
.Take(10)
.AsAsyncEnumerable()
.ToListAsync();
// Deep nesting - STILL streams!
var results3 = await JsonQueryable<Employee>
.FromStream(stream, "$.organization[*].divisions[*].departments[*].employees[*]")
.Where(e => e.IsActive) // LINQ filtering (streamed)
.Take(10)
.AsAsyncEnumerable()
.ToListAsync();
// Memory: ~25MB (constant), processes 2GB file safely with ANY level of nesting
DO: Use JSONPath Advanced Filters for Selective Filtering (Small/Medium Files)
// GOOD: Pre-filter with JSONPath advanced filters (100MB file, 1M → 500 items)
var results = JsonQueryable<Product>
.FromString(mediumJson, "$[?@.price < 100 && @.inStock == true]")
.OrderBy(p => p.Name)
.Take(10)
.ToList();
// Memory: Loads 100MB + filters → ~25MB result (acceptable for medium files)
❌ DON'T: Use Advanced JSONPath Filters on Very Large Files (>1GB)
// BAD: Advanced filter loads ENTIRE 2GB file into memory!
var results = JsonQueryable<Product>
.FromString(hugeJson, "$[?@.price < 100]") // Loads ALL 2GB!
.ToList();
// Memory: 2GB+ (OutOfMemoryException risk!)
❌ DON'T: Use LINQ Where for Initial Filtering on Large Datasets (1M+) Without JSONPath
// BAD: Deserializes ALL 1M first, then filters
var results = JsonQueryable<Product>
.FromString(largeJson) // No JSONPath pre-filter!
.Where(p => p.Price < 100 && p.InStock) // Deserializes 1,000,000 objects
.OrderBy(p => p.Name)
.Take(10)
.ToList();
// Memory: ~500MB (all objects deserialized before filtering)
| Document Size | Recommended Approach | JSONPath Pattern | Memory Impact |
|---|---|---|---|
| < 1MB | Any JSONPath feature | $[?@.price < 100] or $.data[*] | Minimal |
| 1-100MB | Advanced filters (monitor) or simple wildcards | $[?@.price < 100] or $.data[*] | Document size + overhead OR constant |
| 100MB-1GB | Simple wildcard paths (preferred) | $.data[*] or $.dept[*].emp[*] | Constant (~25MB) |
| > 1GB | Simple wildcard paths (required) | $.data[*] or $.dept[*].emp[*] | Constant (~25MB) |
Key Features:
Key Takeaway:
$.data[*], $.departments[*].employees[*]) maintain true streaming regardless of nesting depth - perfect for large/very large files$[?@.price < 100], $[0:10], length()) load entire document - use only for small/medium filesSample Code:
samples/Blazing.Json.Queryable.Samples/Examples/AdvancedJsonPathSamples.csBlazing.Json.Queryable uses a custom LINQ provider that translates LINQ expressions into efficient JSON processing operations:
System.Text.Json.Utf8JsonReader for efficient parsingSpan<byte> and ReadOnlySpan<byte>IAsyncEnumerable<T> support for non-blocking I/O// Query: Find first 10 active users over 25 in London
var results = await JsonQueryable<Person>.FromStream(stream)
.Where(p => p.Age > 25)
.Where(p => p.City == "London")
.Where(p => p.IsActive)
.OrderBy(p => p.Name)
.Take(10)
.AsAsyncEnumerable()
.ToListAsync();
// Internal execution:
// 1. Build execution plan from LINQ expression tree
// 2. Open JSON stream and start parsing
// 3. For each JSON object:
// a. Check Age > 25 (filter early)
// b. Check City == "London" (filter)
// c. Check IsActive (filter)
// d. If all pass, deserialize to Person object
// e. Add to ordered buffer
// 4. Stop reading after 10th match (early termination!)
// 5. Return results without processing entire file
Scenario: 100,000 record JSON file (52MB), find first 10 matching records
| Approach | Time | Memory | Notes |
|---|---|---|---|
| Traditional | 1,200ms | 450MB | Loads entire file, deserializes all |
| Blazing.Json.Queryable | 120ms | 25MB | Streams, stops after 10 matches |
| Improvement | 10x faster | 18x less | 90% time reduction |
Early Termination:
Memory Efficiency:
Zero-Allocation UTF-8:
Async Streaming:
The library includes comprehensive sample and benchmark projects demonstrating different usage patterns and performance characteristics:
All samples are located in the samples/Blazing.Json.Queryable.Samples directory (basic to advanced order):
IAsyncEnumerable<T>All benchmarks are located in the benchmarks/Blazing.Json.Queryable.Benchmarks directory:
# Clone the repository
git clone https://github.com/gragra33/Blazing.Json.Queryable.git
cd Blazing.Json.Queryable
# Run the samples project (interactive menu)
dotnet run --project samples/Blazing.Json.Queryable.Samples
The samples include:
# Run benchmarks (interactive mode)
cd benchmarks/Blazing.Json.Queryable.Benchmarks
dotnet run -c Release
# Or run specific benchmark suite
dotnet run -c Release -- --filter *SyncInMemory*
dotnet run -c Release -- --filter *AsyncStream*
dotnet run -c Release -- --filter *Comprehensive*
# List all available benchmarks
dotnet run -c Release -- --list flat
The benchmarks provide:
BenchmarkDotNet.ArtifactsIf you like or are using this project to learn or start your solution, please give it a star. Thanks!
Also, if you find this library useful and you're feeling really generous, please consider buying me a coffee ☕.
$.departments[*].employees[*]) now stream with constant memory usageInitial Release - Full production release
IAsyncEnumerable<T>License: MIT License - see LICENSE file for details
Copyright © 2026 Graeme Grant. All rights reserved.