ES|QL LINQ for .NET
Write C# LINQ expressions, get Elasticsearch ES|QL query strings. Type-safe, AOT compatible, with full IntelliSense and compile-time checking.
var results = await client.CreateQuery<LogEntry>()
.From("logs-*")
.Where(l => l.Level == "ERROR" && l.Duration > 1000)
.OrderByDescending(l => l.Timestamp)
.Take(50)
.ToListAsync();
Produces:
FROM logs-*
| WHERE (log.level == "ERROR" AND duration > 1000)
| SORT @timestamp DESC
| LIMIT 50
The ES|QL LINQ support is split into two NuGet packages so you can choose the right level of dependency for your project.
Most projects should install Elastic.Clients.Esql -- it includes everything you need to build and execute ES|QL queries against an Elasticsearch cluster:
dotnet add package Elastic.Clients.Esql
This pulls in Elastic.Esql and Elastic.Transport automatically.
If you only need query string generation without any HTTP or transport dependency, install the translation library directly:
dotnet add package Elastic.Esql
| Package | What it does | When to use it |
|---|---|---|
| Elastic.Clients.Esql | LINQ translation + query execution via Elastic.Transport |
You want to run queries against a cluster and get results back |
| Elastic.Esql | LINQ-to-ES|QL translation only, zero dependencies | You need string generation, a custom transport, or query inspection |
Field names are resolved automatically from your C# types using System.Text.Json conventions:
[JsonPropertyName]attributes -- if a property has[JsonPropertyName("log.level")], that exact name is used in the generated ES|QLJsonNamingPolicy-- if aJsonNamingPolicyis configured (e.g.,JsonNamingPolicy.CamelCase), property names are transformed accordingly- Default convention -- without explicit configuration, property names are used as-is with camelCase conversion
public class LogEntry
{
[JsonPropertyName("@timestamp")]
public DateTime Timestamp { get; set; }
[JsonPropertyName("log.level")]
public string Level { get; set; }
public string Message { get; set; }
public long Duration { get; set; }
}
- → @timestamp
- → log.level
- → message (camelCase)
- → duration (camelCase)
For Native AOT compatibility, pass a source-generated JsonSerializerContext to the query provider. This ensures field names are resolved at compile time with zero reflection:
[JsonSerializable(typeof(LogEntry))]
public partial class MyJsonContext : JsonSerializerContext;
var provider = new EsqlQueryProvider(MyJsonContext.Default);
var query = new EsqlQueryable<LogEntry>(provider)
.From("logs-*")
.Where(l => l.Level == "ERROR")
.ToString();