Elastic.Esql 0.10.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>()
    .From("logs-*")
    .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>()
    .From("orders");

// Or with a source-generated JsonSerializerContext -- AOT safe
var provider = new EsqlQueryProvider(MyContext.Default);
query = new EsqlQueryable<Order>(provider)
    .From("orders");

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>().From("orders")
    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)
    .From("orders")
    .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 provider = new EsqlQueryProvider(MyContext.Default);
var query = new EsqlQueryable<Product>(provider)
    .From("products")
    .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.

Showing the top 20 packages that depend on Elastic.Esql.

Packages Downloads
Elastic.Clients.Elasticsearch
This strongly-typed, client library enables working with Elasticsearch. It is the official client maintained and supported by Elastic.
2
Elastic.Clients.Elasticsearch
This strongly-typed, client library enables working with Elasticsearch. It is the official client maintained and supported by Elastic.
4

https://github.com/elastic/esql-dotnet/releases

.NET 8.0

.NET 10.0

  • No dependencies.

.NET Standard 2.0

Version Downloads Last updated
0.11.0 0 2026/4/20
0.10.0 3 2026/4/13
0.9.0 4 2026/4/13
0.8.0 2 2026/4/13
0.7.0 3 2026/4/13
0.6.0 2 2026/4/13
0.5.0 2 2026/4/13
0.4.1 3 2026/4/13
0.4.0 2 2026/4/13
0.3.0 3 2026/4/13
0.2.0 3 2026/4/13