Elastic.Clients.Esql
0.12.0
Prefix Reserved
dotnet add package Elastic.Clients.Esql --version 0.12.0
NuGet\Install-Package Elastic.Clients.Esql -Version 0.12.0
<PackageReference Include="Elastic.Clients.Esql" Version="0.12.0" />
<PackageVersion Include="Elastic.Clients.Esql" Version="0.12.0" />
<PackageReference Include="Elastic.Clients.Esql" />
paket add Elastic.Clients.Esql --version 0.12.0
#r "nuget: Elastic.Clients.Esql, 0.12.0"
#:package Elastic.Clients.Esql@0.12.0
#addin nuget:?package=Elastic.Clients.Esql&version=0.12.0
#tool nuget:?package=Elastic.Clients.Esql&version=0.12.0
Elastic.Clients.Esql
Execute ES|QL queries against Elasticsearch with LINQ. This package connects the Elastic.Esql translation engine to a real cluster via Elastic.Transport. Pair it with a source-generated JsonSerializerContext for a fully AOT-compatible query pipeline from LINQ expression to typed results.
Why?
Elastic.Esql translates LINQ to ES|QL strings. This package adds the HTTP layer to actually run them -- connection pooling, authentication, error handling, async queries, and result materialization into typed C# objects.
Quick Start
using var client = new EsqlClient(new Uri("https://my-cluster:9200"));
// LINQ query -- translates and executes in one step
var errors = await client.CreateQuery<LogEntry>()
.From("logs-*")
.Where(l => l.Level == "ERROR")
.OrderByDescending(l => l.Timestamp)
.Take(10)
.ToListAsync();
Configuration
// Simple -- just a URI
using var client = new EsqlClient(new Uri("https://localhost:9200"));
// With authentication and a source-generated JSON context
[JsonSerializable(typeof(LogEntry))]
public partial class MyContext : JsonSerializerContext;
var transport = new DistributedTransport(
new TransportConfiguration(new Uri(url), new ApiKey(apiKey)));
var settings = new EsqlClientSettings(transport)
{
JsonSerializerContext = MyContext.Default,
Defaults = new EsqlQueryDefaults
{
TimeZone = "America/New_York",
Locale = "en-US"
}
};
using var client = new EsqlClient(settings);
Query Patterns
LINQ fluent syntax
var topBrands = await client.CreateQuery<Product>()
.From("products")
.Where(p => p.InStock)
.GroupBy(p => p.Brand)
.Select(g => new { Brand = g.Key, Avg = g.Average(p => p.Price), Count = g.Count() })
.OrderByDescending(x => x.Avg)
.Take(5)
.ToListAsync();
LINQ query syntax
var results = await (
from log in client.CreateQuery<LogEntry>().From("logs-*")
where log.Level == "ERROR"
where log.Duration > 500
orderby log.Timestamp descending
select new { log.Message, log.Duration }
).ToListAsync();
Lambda expression
var results = await client.QueryAsync<LogEntry>(q =>
q.From("logs-*").Where(l => l.Level == "ERROR").Take(10));
Raw ES|QL fragments
Use RawEsql() to append expert-level ES|QL fragments directly into a LINQ pipeline:
var results = client.Query<LogEntry>(q => q
.From("logs-*")
.RawEsql("WHERE log.level == \"ERROR\"")
.RawEsql("| LIMIT 10"));
You can also switch the downstream materialization type:
var rows = client.Query<LogEntry, LogProjection>(q => q
.From("logs-*")
.RawEsql<LogEntry, LogProjection>("KEEP message, statusCode"));
For Native AOT, ensure the target materialization type (for example LogProjection) is included in your source-generated JsonSerializerContext.
Inspect the generated query
Every queryable's .ToString() returns the ES|QL without executing:
var query = client.CreateQuery<Product>()
.Where(p => p.Price > 100)
.OrderBy(p => p.Name);
Console.WriteLine(query.ToString());
// FROM products
// | WHERE price > 100
// | SORT name
Async Queries
For long-running queries, use the async query API. Results auto-delete from the cluster on dispose:
await using var asyncQuery = await client.SubmitAsyncQueryAsync<LogEntry>(
q => q.From("logs-*").Where(l => l.Level == "ERROR"),
new EsqlAsyncQueryOptions
{
WaitForCompletionTimeout = TimeSpan.FromSeconds(5),
KeepAlive = TimeSpan.FromMinutes(10)
});
if (asyncQuery.IsRunning)
Console.WriteLine($"Query {asyncQuery.QueryId} still running...");
var results = await asyncQuery.ToListAsync(); // Polls until complete
// Query automatically deleted from cluster when disposed
Raw response formats
Get the server-formatted bytes (Csv, Tsv, Txt, Json, Arrow, Smile, Cbor, Yaml) instead of materialised rows:
using var stream = await client.CreateQuery<LogEntry>()
.From("logs-*")
.Where(l => l.Level == "ERROR")
.ToStreamAsync(EsqlFormat.Csv);
await stream.CopyToAsync(File.Create("errors.csv"));
Server-side async with Apache Arrow. The query is best-effort DELETEd on disposal:
await using var q = await client.CreateQuery<LogEntry>()
.From("logs-*")
.Where(l => l.Level == "ERROR")
.ToAsyncQueryAsync(EsqlFormat.Arrow);
await q.WaitForCompletionAsync();
using var stream = q.GetResponseStream();
using var reader = new ArrowStreamReader(stream); // Apache.Arrow.Ipc — separate NuGet
while (await reader.ReadNextRecordBatchAsync() is { } batch)
Console.WriteLine($"Batch: {batch.Length} rows, {batch.ColumnCount} cols");
A ToPipeReaderAsync(format) overload is available on .NET 10+ for zero-copy consumers.
Per-query options
Configure defaults globally through EsqlClientSettings.Defaults. Override them on individual queries with .WithOptions():
var results = await client.CreateQuery<LogEntry>()
.WithOptions(new EsqlQueryOptions { TimeZone = "America/New_York", Locale = "en-US" })
.From("logs-*")
.Where(l => l.Level == "ERROR")
.ToListAsync();
Use RequestConfiguration for transport-level overrides (timeouts, authentication, headers):
var results = await client.CreateQuery<LogEntry>()
.WithOptions(new EsqlQueryOptions
{
RequestConfiguration = new RequestConfiguration { RequestTimeout = TimeSpan.FromSeconds(120) }
})
.From("logs-*")
.ToListAsync();
For async queries, combine it with EsqlAsyncQueryOptions:
await using var asyncQuery = await client.CreateQuery<LogEntry>()
.WithOptions(new EsqlQueryOptions { TimeZone = "UTC" })
.From("logs-*")
.Where(l => l.Level == "ERROR")
.ToAsyncQueryAsync(new EsqlAsyncQueryOptions
{
WaitForCompletionTimeout = TimeSpan.FromSeconds(5),
KeepAlive = TimeSpan.FromMinutes(10)
});
Testing
Generate ES|QL strings without an Elasticsearch connection:
var provider = new EsqlQueryProvider(MyContext.Default);
var esql = new EsqlQueryable<Product>(provider)
.From("products")
.Where(p => p.InStock && p.Price < 50)
.OrderBy(p => p.Name)
.ToString();
Assert.Equal("""
FROM products
| WHERE (in_stock == true AND price < 50)
| SORT name
""", esql);
AOT Compatible
The full pipeline is AOT ready when used with a source-generated JsonSerializerContext:
- Query translation (
Elastic.Esql) -- pure expression tree walking, no reflection-based serialization - Field resolution (
System.Text.Jsonmetadata) -- source-generated context aligned with your JSON contracts - HTTP transport (
Elastic.Transport) -- AOT-compatible HTTP client
Use a source-generated JsonSerializerContext so field names, JSON serialization, and ES|QL queries all derive from the same compile-time source of truth.
Architecture
Elastic.Clients.Esql -- This package: EsqlClient, transport, execution
references:
Elastic.Esql -- LINQ-to-ES|QL translation (no HTTP dependency)
Elastic.Transport -- Low-level HTTP client for Elasticsearch
If you only need ES|QL string generation (no execution), depend on Elastic.Esql directly.
| 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
- Elastic.Esql (>= 0.12.0)
- Elastic.Transport (>= 0.17.3)
- 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
- Elastic.Esql (>= 0.12.0)
- Elastic.Transport (>= 0.17.3)
-
net8.0
- Elastic.Esql (>= 0.12.0)
- Elastic.Transport (>= 0.17.3)
- System.Text.Json (>= 10.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.