A library for adding dynamic Lucene-style query capabilities to your .NET applications. Enable your users to write powerful search queries using familiar Lucene syntax, with support for Entity Framework Core and (coming soon) Elasticsearch.
This project is a modern replacement for Foundatio.Parsers.
- Dynamic User Queries - Let users write powerful search queries using Lucene syntax
- Entity Framework Integration - Convert Lucene queries directly to LINQ expressions for EF Core
- Elasticsearch Support - (Coming soon) Generate Elasticsearch queries from the same syntax
- Full Lucene Query Syntax - Terms, phrases, fields, ranges, boolean operators, wildcards, regex, and more
- Date Math Expressions - Support for Elasticsearch-style date math (
now-1d,2024-01-01||+1M/d) - Visitor Pattern - Transform, validate, or analyze queries with composable visitors
- Field Aliasing - Map user-friendly field names to your actual data model
- Query Validation - Restrict allowed fields, operators, and patterns
- Round-Trip Capable - Parse queries to AST and convert back to query strings
- Error Recovery - Resilient parser returns partial AST with detailed error information
# Core parser
dotnet add package Foundatio.LuceneQuery
# Entity Framework integration (optional)
dotnet add package Foundatio.LuceneQuery.EntityFrameworkusing Foundatio.LuceneQuery;
var result = LuceneQuery.Parse("title:hello AND status:active");
if (result.IsSuccess)
{
var document = result.Document; // QueryDocument (root AST node)
}
else
{
// Handle errors - partial AST may still be available
foreach (var error in result.Errors)
Console.WriteLine($"Error at {error.Line}:{error.Column}: {error.Message}");
}using Foundatio.LuceneQuery;
var result = LuceneQuery.Parse("title:test AND (status:active OR status:pending)");
var queryString = QueryStringBuilder.ToQueryString(result.Document);
// Returns: "title:test AND (status:active OR status:pending)"using Foundatio.LuceneQuery;
using Foundatio.LuceneQuery.Visitors;
var result = LuceneQuery.Parse("user:john AND created:[2020-01-01 TO 2020-12-31]");
var fieldMap = new FieldMap
{
{ "user", "account.username" },
{ "created", "metadata.timestamp" }
};
await FieldResolverQueryVisitor.RunAsync(result.Document, fieldMap);
var resolved = QueryStringBuilder.ToQueryString(result.Document);
// Returns: "account.username:john AND metadata.timestamp:[2020-01-01 TO 2020-12-31]"using Foundatio.LuceneQuery;
var result = LuceneQuery.Parse("*wildcard AND title:test");
var options = new QueryValidationOptions
{
AllowLeadingWildcards = false
};
options.AllowedFields.Add("title");
options.AllowedFields.Add("status");
var validationResult = await QueryValidator.ValidateAsync(result.Document, options);
if (!validationResult.IsValid)
Console.WriteLine(validationResult.Message);Enable dynamic, user-driven queries in your API endpoints:
using Foundatio.LuceneQuery.EntityFramework;
// In your API controller or service
[HttpGet("employees")]
public async Task<IActionResult> SearchEmployees([FromQuery] string query)
{
var parser = new EntityFrameworkQueryParser();
// User provides: "name:john AND salary:[50000 TO *] AND department:engineering"
Expression<Func<Employee, bool>> filter = parser.BuildFilter<Employee>(query);
var results = await _context.Employees
.Where(filter)
.ToListAsync();
return Ok(results);
}With field aliasing to protect your data model:
var parser = new EntityFrameworkQueryParser();
// Map user-friendly names to actual entity properties
var fieldMap = new FieldMap
{
{ "name", "FullName" },
{ "dept", "Department.Name" },
{ "hired", "HireDate" }
};
// User query: "name:john AND dept:engineering AND hired:[2020-01-01 TO *]"
Expression<Func<Employee, bool>> filter = parser.BuildFilter<Employee>(userQuery, fieldMap);- Search APIs - Let users filter data with powerful query syntax
- Admin Dashboards - Enable complex filtering without custom UI for each field
- Reporting - Allow dynamic report criteria using familiar search syntax
- Data Export - Let users specify exactly what data they need
- Audit/Log Search - Search through logs with date ranges, terms, and boolean logic
| Syntax | Example | Description |
|---|---|---|
| Terms | hello, hello*, hel?o |
Simple terms with optional wildcards |
| Phrases | "hello world", "hello world"~2 |
Exact phrases with optional proximity |
| Fields | title:test, user.name:john |
Field-specific queries, supports nested paths |
| Ranges | price:[100 TO 500], date:{* TO 2024-01-01} |
Inclusive [] or exclusive {} ranges |
| Boolean | AND, OR, NOT, +, - |
Boolean operators and prefix modifiers |
| Groups | (a OR b) AND c |
Parenthetical grouping |
| Exists | _exists_:field, _missing_:field |
Field existence checks |
| Match All | *:* |
Matches all documents |
| Regex | /pattern/ |
Regular expression patterns |
| Date Math | now-1d, 2024-01-01||+1M/d |
Elasticsearch date math expressions |
| Includes | @include:savedQuery |
Reference saved/named queries |
Extend QueryNodeVisitor to create custom transformations:
using Foundatio.LuceneQuery.Ast;
using Foundatio.LuceneQuery.Visitors;
public class LowercaseTermVisitor : QueryNodeVisitor
{
public override Task<QueryNode> VisitAsync(TermNode node, IQueryVisitorContext context)
{
node.Term = node.Term?.ToLowerInvariant();
return Task.FromResult<QueryNode>(node);
}
public override async Task<QueryNode> VisitAsync(FieldQueryNode node, IQueryVisitorContext context)
{
// Process this node's field
node.Field = node.Field?.ToLowerInvariant();
// Visit children
return await base.VisitAsync(node, context);
}
}
// Usage
var visitor = new LowercaseTermVisitor();
await visitor.RunAsync(result.Document);var chain = new ChainedQueryVisitor()
.AddVisitor(new FieldAliasVisitor(aliases), priority: 10)
.AddVisitor(new LowercaseTermVisitor(), priority: 20)
.AddVisitor(new ValidationVisitor(), priority: 30);
await chain.AcceptAsync(document, context);| Node Type | Description |
|---|---|
QueryDocument |
Root node containing the parsed query |
TermNode |
Simple term (e.g., hello) |
PhraseNode |
Quoted phrase (e.g., "hello world") |
FieldQueryNode |
Field:value pair (e.g., title:test) |
RangeNode |
Range query (e.g., [1 TO 10]) |
BooleanQueryNode |
Boolean combination of clauses |
GroupNode |
Parenthetical group |
NotNode |
Negation wrapper |
ExistsNode |
_exists_:field check |
MissingNode |
_missing_:field check |
MatchAllNode |
*:* match all |
RegexNode |
Regular expression |
MultiTermNode |
Multiple terms without explicit operators |
dotnet build
dotnet testApache 2.0