Entity Framework Integration
Foundatio.Lucene.EntityFramework converts Lucene query strings to LINQ expressions for use with Entity Framework Core.
Installation
bash
dotnet add package Foundatio.Lucene.EntityFrameworkBasic Usage
csharp
using Foundatio.Lucene.EntityFramework;
using Microsoft.EntityFrameworkCore;
var parser = new EntityFrameworkQueryParser();
// Build a LINQ expression from a Lucene query
var filter = parser.BuildFilter<Employee>("name:john AND department:engineering");
// Use with EF Core
var results = await context.Employees
.Where(filter)
.ToListAsync();API Endpoints
Enable dynamic, user-driven queries in your API:
csharp
[HttpGet("employees")]
public async Task<IActionResult> SearchEmployees([FromQuery] string query)
{
var parser = new EntityFrameworkQueryParser();
// User provides: "name:john AND salary:[50000 TO *]"
var filter = parser.BuildFilter<Employee>(query);
var results = await _context.Employees
.Where(filter)
.ToListAsync();
return Ok(results);
}Field Mapping
Map user-friendly field names to entity properties:
csharp
var parser = new EntityFrameworkQueryParser();
var fieldMap = new FieldMap
{
{ "name", "FullName" },
{ "dept", "Department.Name" },
{ "hired", "HireDate" },
{ "salary", "Compensation.BaseSalary" }
};
// User query: "name:john AND dept:engineering"
var filter = parser.BuildFilter<Employee>(query, fieldMap);Supported Query Types
Term Queries
Simple equality matching:
csharp
// Input
"status:active"
// Generates LINQ equivalent to:
employee => employee.Status == "active"Phrase Queries
Exact string matching:
csharp
// Input
"title:\"Senior Engineer\""
// Generates LINQ equivalent to:
employee => employee.Title == "Senior Engineer"Wildcard Queries
Pattern matching with wildcards:
csharp
// Input
"name:john*"
// Generates LINQ equivalent to:
employee => employee.Name.StartsWith("john")
// Input
"email:*@company.com"
// Generates LINQ equivalent to:
employee => employee.Email.EndsWith("@company.com")
// Input
"code:A*Z"
// Generates LINQ equivalent to:
employee => employee.Code.StartsWith("A") && employee.Code.EndsWith("Z")Range Queries
Comparison operators:
csharp
// Input
"salary:[50000 TO 100000]"
// Generates LINQ equivalent to:
employee => employee.Salary >= 50000 && employee.Salary <= 100000
// Input
"age:{21 TO *}"
// Generates LINQ equivalent to:
employee => employee.Age > 21Boolean Operators
Complex boolean logic:
csharp
// Input
"status:active AND department:(engineering OR sales)"
// Generates LINQ equivalent to:
employee => employee.Status == "active" &&
(employee.Department == "engineering" || employee.Department == "sales")
// Input
"active:true NOT role:admin"
// Generates LINQ equivalent to:
employee => employee.Active == true && employee.Role != "admin"Nested Properties
Access nested object properties:
csharp
// Input
"department.name:engineering"
// Generates LINQ equivalent to:
employee => employee.Department.Name == "engineering"
// With field mapping
var fieldMap = new FieldMap { { "dept", "Department.Name" } };
// Input: "dept:engineering"Exists/Missing Queries
Check for null values:
csharp
// Input
"_exists_:email"
// Generates LINQ equivalent to:
employee => employee.Email != null
// Input
"_missing_:phone"
// Generates LINQ equivalent to:
employee => employee.Phone == nullType Handling
The parser automatically handles type conversions based on entity property types:
Numeric Properties
csharp
// For int/decimal properties
"salary:50000" // Converts "50000" to numeric
"salary:[50000 TO *]" // Range comparisonBoolean Properties
csharp
// For bool properties
"active:true"
"active:false"DateTime Properties
csharp
// For DateTime properties
"hireDate:2024-01-01"
"hireDate:[2024-01-01 TO 2024-12-31]"Enum Properties
csharp
// For enum properties
"status:Active" // Matches enum value name
"status:1" // Or numeric valueConfiguration
Custom Configuration
csharp
var parser = new EntityFrameworkQueryParser();
// The parser automatically discovers entity metadata from EF Core
// when used with a DbContext-backed entity typeValidation
Add query validation to prevent expensive or dangerous queries:
csharp
var options = new QueryValidationOptions
{
AllowLeadingWildcards = false, // Prevent slow queries like "*suffix"
MaxDepth = 5 // Limit query complexity
};
options.AllowedFields.Add("name");
options.AllowedFields.Add("department");
// Validate before executing
var parseResult = LuceneQuery.Parse(userQuery);
var validation = await QueryValidator.ValidateAsync(parseResult.Document, options);
if (!validation.IsValid)
{
return BadRequest(validation.Message);
}
var filter = parser.BuildFilter<Employee>(userQuery);Error Handling
Handle parse and execution errors gracefully:
csharp
try
{
var parseResult = LuceneQuery.Parse(userQuery);
if (!parseResult.IsSuccess)
{
var errors = string.Join("; ", parseResult.Errors.Select(e => e.Message));
return BadRequest($"Invalid query: {errors}");
}
var filter = parser.BuildFilter<Employee>(userQuery);
var results = await _context.Employees.Where(filter).ToListAsync();
return Ok(results);
}
catch (InvalidOperationException ex)
{
return BadRequest($"Query error: {ex.Message}");
}Performance Considerations
Indexing
Ensure database indexes exist for commonly queried fields:
csharp
modelBuilder.Entity<Employee>()
.HasIndex(e => e.Name);
modelBuilder.Entity<Employee>()
.HasIndex(e => e.Status);
modelBuilder.Entity<Employee>()
.HasIndex(e => e.Department);Pagination
Always paginate results for user queries:
csharp
var filter = parser.BuildFilter<Employee>(query);
var results = await _context.Employees
.Where(filter)
.OrderBy(e => e.Name)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();Query Logging
Log queries for debugging and monitoring:
csharp
var queryString = QueryStringBuilder.ToQueryString(parseResult.Document);
_logger.LogInformation("Executing query: {Query}", queryString);Complete Example
csharp
[ApiController]
[Route("api/[controller]")]
public class EmployeesController : ControllerBase
{
private readonly AppDbContext _context;
private readonly EntityFrameworkQueryParser _parser;
private readonly ILogger<EmployeesController> _logger;
private static readonly FieldMap FieldMap = new()
{
{ "name", "FullName" },
{ "dept", "Department.Name" },
{ "hired", "HireDate" },
{ "salary", "Compensation.BaseSalary" },
{ "manager", "Manager.FullName" }
};
private static readonly QueryValidationOptions ValidationOptions = new()
{
AllowLeadingWildcards = false
};
public EmployeesController(AppDbContext context, ILogger<EmployeesController> logger)
{
_context = context;
_parser = new EntityFrameworkQueryParser();
_logger = logger;
ValidationOptions.AllowedFields.AddRange(FieldMap.Keys);
}
[HttpGet]
public async Task<IActionResult> Search(
[FromQuery] string q = "*:*",
[FromQuery] int page = 1,
[FromQuery] int size = 20)
{
// Parse the query
var parseResult = LuceneQuery.Parse(q);
if (!parseResult.IsSuccess)
{
return BadRequest(new {
Error = "Invalid query syntax",
Details = parseResult.Errors.Select(e => e.Message)
});
}
// Resolve field aliases
await FieldResolverQueryVisitor.RunAsync(parseResult.Document, FieldMap);
// Validate the query
var validation = await QueryValidator.ValidateAsync(
parseResult.Document,
ValidationOptions
);
if (!validation.IsValid)
{
return BadRequest(new { Error = validation.Message });
}
// Build and execute
var resolvedQuery = QueryStringBuilder.ToQueryString(parseResult.Document);
_logger.LogInformation("Searching employees with: {Query}", resolvedQuery);
var filter = _parser.BuildFilter<Employee>(resolvedQuery);
var total = await _context.Employees.CountAsync(filter);
var results = await _context.Employees
.Where(filter)
.OrderBy(e => e.FullName)
.Skip((page - 1) * size)
.Take(size)
.Select(e => new EmployeeDto(e))
.ToListAsync();
return Ok(new
{
Total = total,
Page = page,
PageSize = size,
Results = results
});
}
}Next Steps
- Query Syntax - All supported query syntax
- Field Mapping - Advanced field aliasing
- Validation - Query validation options