Troubleshooting
This guide covers common issues and their solutions when using Foundatio.Parsers.
Parse Errors
Unexpected Character
Error: Unexpected character at position X
Cause: The query contains invalid syntax.
Solution:
try
{
var result = await parser.ParseAsync(query);
}
catch (FormatException ex)
{
// Get position information
var cursor = ex.Data["cursor"] as Cursor;
if (cursor != null)
{
Console.WriteLine($"Error at line {cursor.Line}, column {cursor.Column}");
}
Console.WriteLine($"Message: {ex.Message}");
}Common causes:
- Unbalanced parentheses:
(status:active - Invalid range syntax:
field:[1 TO] - Unescaped special characters:
url:https://example.com
Fix: Escape special characters:
// Escape colons in values
"url:https\\://example.com"
// Or use quotes
"url:\"https://example.com\""Empty Query
Error: Query returns no results or empty AST.
Solution: Check for empty or whitespace-only queries:
if (string.IsNullOrWhiteSpace(query))
{
// Handle empty query
return new MatchAllQuery();
}
var result = await parser.ParseAsync(query);Validation Errors
Field Not Allowed
Error: Field 'X' is not allowed
Cause: Field is not in the AllowedFields list.
Solution:
var options = new QueryValidationOptions();
options.AllowedFields.Add("status");
options.AllowedFields.Add("name");
options.AllowedFields.Add("created");
// Add all fields users should be able to queryField Restricted
Error: Field 'X' is restricted
Cause: Field is in the RestrictedFields list.
Solution: Remove from restricted list or use a different field:
var options = new QueryValidationOptions();
options.RestrictedFields.Remove("fieldname");Leading Wildcard Not Allowed
Error: Leading wildcards are not allowed
Cause: Query contains *value and AllowLeadingWildcards is false.
Solution:
// Option 1: Allow leading wildcards
var options = new QueryValidationOptions {
AllowLeadingWildcards = true
};
// Option 2: Rewrite query to use trailing wildcard
// Instead of: *smith
// Use: smith* (if appropriate for your use case)Query Too Deep
Error: Query exceeds maximum depth of X
Cause: Query nesting exceeds AllowedMaxNodeDepth.
Solution:
var options = new QueryValidationOptions {
AllowedMaxNodeDepth = 15 // Increase limit
};Unresolved Field
Error: Field 'X' could not be resolved
Cause: Field doesn't exist in Elasticsearch mapping and AllowUnresolvedFields is false.
Solution:
// Option 1: Allow unresolved fields
var options = new QueryValidationOptions {
AllowUnresolvedFields = true
};
// Option 2: Add field alias
var parser = new ElasticQueryParser(c => c
.UseFieldMap(new Dictionary<string, string> {
{ "user_field", "actual.field.path" }
}));
// Option 3: Force the next resolution to reload the server mapping.
parser.Configuration.MappingResolver.RefreshMapping();Elasticsearch Issues
Query Returns No Results
Possible causes:
- Analyzed vs keyword field:
// Check if field is analyzed
var resolver = parser.Configuration.MappingResolver;
bool isAnalyzed = resolver.IsPropertyAnalyzed("title");
// For exact matches on analyzed fields, use .keyword
"title.keyword:\"Exact Title\""- Case sensitivity:
// Elasticsearch is case-sensitive by default
// Use lowercase or configure analyzer
"status:Active" // May not match "active"- Filter vs query context:
// By default, queries are wrapped in filter (no scoring)
// For full-text search, enable scoring
var context = new ElasticQueryVisitorContext { UseScoring = true };
var query = await parser.BuildQueryAsync("search terms", context);Nested Query Not Working
Cause: Nested support not enabled or field not detected as nested.
Solution:
var parser = new ElasticQueryParser(c => c
.UseMappings(client, "my-index")
.UseNested()); // Enable nested support
// Verify field is nested
var resolver = parser.Configuration.MappingResolver;
bool isNested = resolver.IsNestedPropertyType("comments");For more details on how nested queries are traversed and built, see Nested Queries and Visitor Traversal.
Geo Query Fails
Cause: Geo resolver not configured or returns invalid coordinates.
Solution:
var parser = new ElasticQueryParser(c => c
.UseMappings(client, "my-index")
.UseGeo(location => {
// Ensure valid coordinates are returned
Console.WriteLine($"Resolving: {location}");
var coords = ResolveLocation(location);
Console.WriteLine($"Resolved to: {coords}");
return coords;
}));Aggregation Field Error
Error: Fielddata is disabled on text fields by default
Cause: Trying to aggregate on an analyzed text field.
Solution:
// The parser should automatically use .keyword sub-field
// If not, check your mapping has keyword sub-field:
// "title": { "type": "text", "fields": { "keyword": { "type": "keyword" } } }
// Or use field alias
var parser = new ElasticQueryParser(c => c
.UseFieldMap(new Dictionary<string, string> {
{ "title", "title.keyword" }
}));SQL/EF Core Issues
Dynamic LINQ Parse Error
Error: No property or field 'X' exists in type 'Y'
Cause: Field name doesn't match entity property.
Solution:
// Check available fields
var context = parser.GetContext(db.Products.EntityType);
foreach (var field in context.Fields)
{
Console.WriteLine($"{field.FullName} ({field.GetType().Name})");
}
// Use correct casing (EF Core is case-sensitive)
"Status:active" // Not "status:active"Navigation Property Error
Error: Query on navigation property fails.
Solution:
// Increase navigation depth
var parser = new SqlQueryParser(c => c
.SetFieldDepth(3));
// Check if navigation is included
var context = parser.GetContext(db.Products.EntityType);
var navFields = context.Fields.Where(f => f.IsNavigation);Full-Text Search Not Working
Cause: Full-text catalog not configured or fields not indexed.
Solution:
// Ensure full-text catalog exists
// CREATE FULLTEXT CATALOG ftCatalog AS DEFAULT;
// Ensure full-text index exists on table
// CREATE FULLTEXT INDEX ON Products(Name, Description) KEY INDEX PK_Products;
// Configure parser
var parser = new SqlQueryParser(c => c
.SetFullTextFields(new[] { "Name", "Description" }));Include Issues
Include Not Expanding
Cause: Include name not found in resolver.
Solution:
var parser = new ElasticQueryParser(c => c
.UseIncludes(async name => {
var include = await GetInclude(name);
if (include == null)
{
Console.WriteLine($"Include not found: {name}");
}
return include;
}));Recursive Include Detected
Error: Recursive include detected: X
Cause: Include references itself directly or indirectly.
Solution:
// Check your include definitions for cycles
var includes = new Dictionary<string, string> {
{ "a", "@include:b" },
{ "b", "@include:a" } // Cycle!
};
// Fix by removing the cycle
var includes = new Dictionary<string, string> {
{ "a", "@include:b" },
{ "b", "status:active" } // No cycle
};Performance Issues
Slow Query Parsing
Cause: Complex queries or many visitors.
Solution:
// Reuse parser instance
private readonly ElasticQueryParser _parser;
public MyService()
{
_parser = new ElasticQueryParser(c => c
.UseMappings(client, "my-index"));
}
// Don't create new parser for each requestSlow Mapping Resolution
Cause: Mapping fetched from Elasticsearch on every query.
Solution:
// Resolved fields are cached. Manual refresh is only needed after a known change to an already resolved field:
parser.Configuration.MappingResolver.RefreshMapping();
// For production, create resolver once and share
var resolver = ElasticMappingResolver.Create(client, "my-index");
var parser1 = new ElasticQueryParser(c => c.UseMappings(resolver));
var parser2 = new ElasticQueryParser(c => c.UseMappings(resolver));Cause: Queries continually reference fields that do not exist, making the resolver repeatedly eligible to reload the mapping.
Fields that cannot be resolved reload the mapping on their own short interval (UnmappedFieldRefreshInterval, default 5 seconds) because that is how dynamically created fields become visible. Concurrent reload attempts are coalesced into a single server mapping fetch per resolver. Under continuous misses, the default permits about 12 automatic attempts per minute for each resolver in each process; cold starts and explicit RefreshMapping() calls are outside that ceiling. Raise the interval if a workload legitimately queries many non-existent fields:
resolver.UnmappedFieldRefreshInterval = TimeSpan.FromSeconds(30);Reuse one resolver per concrete index and process. A resolver created for every request has its own cache, throttle, and in-flight request, so it defeats this protection. Look for the Unable to resolve mapping for field {Field} warning: it means a reload was suppressed by the throttle and the field is being treated as unmapped.
Recently Created Field Resolves As Unmapped
Cause: The field was created after the resolver loaded the index mapping — for example by a dynamic template, which only adds the field to the mapping once the first document using it has been indexed.
Symptoms:
- A
nestedfield is queried as a flat field, so the query succeeds but returns no results. - Sorting fails with
Fielddata is disabled on [field] in [index], because the resolver could not find the.keyword/.sortsub-field and fell back to the analyzed field.
Solution: the resolver becomes eligible to reload after UnmappedFieldRefreshInterval. This is a cooldown after a completed attempt, not a five-second correctness deadline. To pick up an application-controlled change immediately, call RefreshMapping() after Elasticsearch acknowledges it:
resolver.RefreshMapping();If the field stays unmapped for much longer than UnmappedFieldRefreshInterval, look for the did not complete within warning. It means a mapping reload was already running but took longer than MappingRefreshWaitTimeout (default 30 seconds), so resolutions gave up waiting for it. Raise MappingRefreshWaitTimeout to match how long your GetMapping call actually takes:
resolver.MappingRefreshWaitTimeout = TimeSpan.FromMinutes(2);The mapping callback must enforce a finite timeout and must not call the same resolver. The built-in client factories use the Elasticsearch client's configured request timeout; keep it below MappingRefreshWaitTimeout if joining resolutions must wait for the result. If treating an unresolved field as unmapped could generate an incorrect query, configure validation with AllowUnresolvedFields = false so the request fails instead.
Debugging
Enable Logging
var parser = new ElasticQueryParser(c => c
.SetLoggerFactory(loggerFactory));
// Or for LuceneQueryParser, use ILogger directly
var loggerFactory = LoggerFactory.Create(b => b.AddConsole());Inspect AST
var parser = new LuceneQueryParser();
var result = await parser.ParseAsync(query);
// Print AST structure
string debug = DebugQueryVisitor.Run(result);
Console.WriteLine(debug);Inspect Generated Query
// Regenerate query string from AST
string regenerated = GenerateQueryVisitor.Run(result);
Console.WriteLine($"Parsed as: {regenerated}");Inspect Elasticsearch Request
var response = await client.SearchAsync<MyDoc>(s => s
.Index("my-index")
.Query(_ => query));
// Get the actual request sent
string request = response.GetRequest();
Console.WriteLine(request);
// Check for errors
if (!response.IsValidResponse)
{
Console.WriteLine(response.GetErrorMessage());
}Getting Help
If you're still having issues:
Check the tests - The test files contain many examples:
tests/Foundatio.Parsers.LuceneQueries.Tests/tests/Foundatio.Parsers.ElasticQueries.Tests/tests/Foundatio.Parsers.SqlQueries.Tests/
Enable detailed logging - Use
LogLevel.Tracefor maximum detailOpen an issue - GitHub Issues
Join Discord - Foundatio Discord