Elastic.Esql
0.5.0
Prefix Reserved
See the version list below for details.
dotnet add package Elastic.Esql --version 0.5.0
NuGet\Install-Package Elastic.Esql -Version 0.5.0
<PackageReference Include="Elastic.Esql" Version="0.5.0" />
<PackageVersion Include="Elastic.Esql" Version="0.5.0" />
<PackageReference Include="Elastic.Esql" />
paket add Elastic.Esql --version 0.5.0
#r "nuget: Elastic.Esql, 0.5.0"
#:package Elastic.Esql@0.5.0
#addin nuget:?package=Elastic.Esql&version=0.5.0
#tool nuget:?package=Elastic.Esql&version=0.5.0
Elastic.Esql
Write LINQ, get ES|QL. A pure translation library that converts C# LINQ expressions into Elasticsearch ES|QL query strings. No HTTP dependencies, no transport layer, AOT compatible -- just query generation.
Why?
ES|QL is powerful but building query strings by hand is error-prone. Elastic.Esql lets you write idiomatic C# and get correct, optimized ES|QL -- with full IntelliSense, compile-time checking, and refactoring support.
var esql = new EsqlQueryable<LogEntry>()
.Where(l => l.Level == "ERROR" && l.Duration > 1000)
.OrderByDescending(l => l.Timestamp)
.Take(50)
.ToString();
Produces:
FROM logs-*
| WHERE (log.level == "ERROR" AND duration > 1000)
| SORT @timestamp DESC
| LIMIT 50
Quick Start
Translation-only (no Elasticsearch connection needed)
// Reflection-based field resolution
var query = new EsqlQueryable<Order>();
// Or with a source-generated mapping context (from Elastic.Mapping) -- AOT safe
var query = new EsqlQueryable<Order>(MyContext.Instance);
var esql = query
.Where(o => o.Status == "shipped" && o.Total > 100)
.OrderByDescending(o => o.CreatedAt)
.Take(25)
.ToString();
LINQ query syntax works too
var esql = (
from o in new EsqlQueryable<Order>()
where o.Status == "shipped"
where o.Total > 100
orderby o.CreatedAt descending
select new { o.Id, o.Total, o.CreatedAt }
).ToString();
FROM orders
| WHERE status == "shipped"
| WHERE total > 100
| SORT created_at DESC
| KEEP id, total, created_at
What Translates?
Filtering
.Where(l => l.StatusCode >= 500) // WHERE statusCode >= 500
.Where(l => l.Level == "ERROR" || l.Level == "FATAL") // WHERE (log.level == "ERROR" OR log.level == "FATAL")
.Where(l => !l.IsResolved) // WHERE NOT isResolved
.Where(l => tags.Contains(l.Tag)) // WHERE tag IN ("a", "b", "c")
Sorting
.OrderBy(l => l.Level).ThenByDescending(l => l.Timestamp) // SORT log.level, @timestamp DESC
Projection
.Select(l => new { l.Message, Secs = l.Duration / 1000 }) // KEEP message | EVAL secs = (duration / 1000)
Aggregation
.GroupBy(l => l.Level)
.Select(g => new {
Level = g.Key,
Count = g.Count(),
AvgDuration = g.Average(l => l.Duration)
})
// STATS count = COUNT(*), avgDuration = AVG(duration) BY level = log.level
String methods
.Where(l => l.Message.Contains("timeout")) // WHERE message LIKE "*timeout*"
.Where(l => l.Host.StartsWith("prod-")) // WHERE host LIKE "prod-*"
.Where(l => string.IsNullOrEmpty(l.Tag)) // WHERE (tag IS NULL OR tag == "")
DateTime -- properties, arithmetic, and static members all translate
.Where(l => l.Timestamp.Year == 2025) // WHERE DATE_EXTRACT("year", @timestamp) == 2025
.Where(l => l.Timestamp > DateTime.UtcNow.AddHours(-1)) // WHERE @timestamp > DATE_ADD("hours", -1, NOW())
.Select(l => new { Hour = l.Timestamp.Hour }) // EVAL hour = DATE_EXTRACT("hour", @timestamp)
Math
.Where(l => Math.Abs(l.Delta) > 0.5) // WHERE ABS(delta) > 0.5
.Select(l => new { Root = Math.Sqrt(l.Value) }) // EVAL root = SQRT(value)
ES|QL-specific functions
using static Elastic.Esql.Functions.EsqlFunctions;
.Where(l => Match(l.Message, "connection error")) // WHERE MATCH(message, "connection error")
.Where(l => CidrMatch(l.ClientIp, "10.0.0.0/8")) // WHERE CIDR_MATCH(client_ip, "10.0.0.0/8")
.Where(l => Like(l.Path, "/api/v?/users")) // WHERE path LIKE "/api/v?/users"
AOT Compatible
Elastic.Esql has no dependency on Elastic.Transport or any HTTP library. The entire translation pipeline -- expression visitors, query model, ES|QL generation -- is pure computation with no reflection-based serialization, no dynamic code generation, and no runtime type emission.
When paired with Elastic.Mapping's source-generated field resolution, the full path from LINQ expression to ES|QL string is AOT safe.
Execution
Elastic.Esql is a pure translation library -- it generates ES|QL strings but does not execute them. Use Elastic.Clients.Esql for the official Elastic.Transport-based execution layer, or subclass EsqlQueryProvider to plug in your own transport:
var provider = new MyCustomQueryProvider(fieldResolver);
var results = await new EsqlQueryable<Order>(provider)
.Where(o => o.Total > 100)
.ToListAsync();
Without an execution-capable provider, queries translate to strings only -- calling ToListAsync() throws. This is by design.
Works With Elastic.Mapping
When paired with the Elastic.Mapping source generator, field names resolve from your generated mapping context instead of reflection -- fully AOT compatible:
// Field names come from [JsonPropertyName], [Text], [Keyword], etc.
// Aligned with your System.Text.Json source-generated serialization context
var query = new EsqlQueryable<Product>(MyContext.Instance)
.Where(p => p.Name.Contains("laptop")) // Uses generated field name
.ToString();
Without Elastic.Mapping, field names are resolved via reflection using JsonPropertyName attributes or camelCase convention.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- Microsoft.Bcl.AsyncInterfaces (>= 10.0.0)
- Microsoft.Bcl.HashCode (>= 6.0.0)
- System.Memory (>= 4.6.3)
- System.Text.Json (>= 10.0.0)
-
net10.0
- No dependencies.
-
net8.0
- System.Text.Json (>= 10.0.0)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on Elastic.Esql:
| Package | Downloads |
|---|---|
|
Elastic.Clients.Esql
Elasticsearch ES|QL client with LINQ support and HTTP transport |
GitHub repositories
This package is not used by any popular GitHub repositories.