Skip to content

Migrating to Elastic.Clients.Elasticsearch

This guide covers breaking changes when upgrading Foundatio.Parsers.ElasticQueries from NEST (7.x) to Elastic.Clients.Elasticsearch (8.x). This is a major version bump to Foundatio.Parsers 8.0.

Note: Foundatio.Parsers 8.0 uses the Elastic.Clients.Elasticsearch 8.19.x package and is validated against Elasticsearch 9.x servers. The new .NET client package (8.x) is the official successor to NEST and supports both Elasticsearch 8.x and 9.x clusters.

Package Changes

Before:

xml
<PackageReference Include="NEST" Version="7.17.5" />

After:

xml
<PackageReference Include="Foundatio.Parsers.ElasticQueries" Version="8.x" />
<!-- Transitively brings in Elastic.Clients.Elasticsearch -->

Namespace Changes

Remove old NEST namespaces and add new ones:

csharp
// Remove:
using Nest;

// Add (as needed):
using Elastic.Clients.Elasticsearch;
using Elastic.Clients.Elasticsearch.QueryDsl;
using Elastic.Clients.Elasticsearch.Mapping;
using Elastic.Clients.Elasticsearch.Aggregations;
using Elastic.Clients.Elasticsearch.IndexManagement;

Client Type Changes

Before (NEST)After (new client)
IElasticClientElasticsearchClient
ElasticClientElasticsearchClient
ConnectionSettingsElasticsearchClientSettings

Before:

csharp
var client = new ElasticClient(new ConnectionSettings(new Uri("http://localhost:9200")));

After:

csharp
var client = new ElasticsearchClient(new ElasticsearchClientSettings(new Uri("http://localhost:9200")));

ElasticQueryParser API Changes

BuildQueryAsync

Returns Query instead of QueryContainer:

Before:

csharp
QueryContainer query = await parser.BuildQueryAsync("status:active");

After:

csharp
Query query = await parser.BuildQueryAsync("status:active");

BuildAggregationsAsync

Returns AggregationMap instead of AggregationContainer. Use .ToDictionary() to get the Elasticsearch-native dictionary or the Aggregations() extension method on the search descriptor:

Before:

csharp
AggregationContainer aggs = await parser.BuildAggregationsAsync("terms:status");

var response = await client.SearchAsync<MyDoc>(s => s
    .Aggregations(aggs));

After:

csharp
using Foundatio.Parsers.ElasticQueries.Extensions;

AggregationMap aggs = await parser.BuildAggregationsAsync("terms:status");

var response = await client.SearchAsync<MyDoc>(s => s
    .Aggregations(aggs));

BuildSortAsync

Returns ICollection<SortOptions> instead of IEnumerable<IFieldSort>:

Before:

csharp
IEnumerable<IFieldSort> sorts = await parser.BuildSortAsync("created:-1");

var response = await client.SearchAsync<MyDoc>(s => s
    .Sort(sorts));

After:

csharp
ICollection<SortOptions> sorts = await parser.BuildSortAsync("created:-1");

var response = await client.SearchAsync<MyDoc>(s => s
    .Sort(sorts));

TIP

The deleted SearchDescriptorExtensions.Sort() extension is no longer needed. The new client's SearchRequestDescriptor<T>.Sort() accepts ICollection<SortOptions> natively.

UseMappings

The mapping builder parameter changed from Func (return-based) to Action (mutation-based), and the client type changed:

Before:

csharp
var parser = new ElasticQueryParser(c => c
    .UseMappings<MyDoc>(
        m => m.Properties(p => p
            .Text(t => t.Name(n => n.Title))
            .Keyword(k => k.Name(n => n.Status))),
        client));

After:

csharp
var parser = new ElasticQueryParser(c => c
    .UseMappings<MyDoc>(
        m => m.Properties(p => p
            .Text(n => n.Title)
            .Keyword(n => n.Status)),
        client));

Other UseMappings overload changes:

BeforeAfter
UseMappings<T>(Func<..., ...> builder, IElasticClient)UseMappings<T>(Action<...> builder, ElasticsearchClient)
UseMappings<T>(Func<..., ...> builder, Inferrer, Func<ITypeMapping>)UseMappings<T>(Action<...> builder, Inferrer, Func<TypeMapping>)
UseMappings<T>(IElasticClient)UseMappings<T>(ElasticsearchClient)
UseMappings(IElasticClient, string)UseMappings(ElasticsearchClient, string)
UseMappings(Func<ITypeMapping>, Inferrer)UseMappings(Func<TypeMapping>, Inferrer)

Query Type Changes

All query types changed from object-initializer style to constructor-based:

Before (NEST)After (new client)
new MatchQuery { Field = "f", Query = "v" }new MatchQuery("f", "v")
new MatchPhraseQuery { Field = "f", Query = "v" }new MatchPhraseQuery("f", "v")
new TermQuery { Field = "f", Value = "v" }new TermQuery("f", "v")
new PrefixQuery { Field = "f", Value = "v" }new PrefixQuery("f", "v")
new ExistsQuery { Field = "f" }new ExistsQuery("f")
new NestedQuery { Path = "p", Query = q }new NestedQuery("p", q)
new GeoDistanceQuery { Field = "f", Location = loc, Distance = d }new GeoDistanceQuery(distance, field, location)
new DateRangeQuery { Field = "f", GreaterThanOrEqualTo = v }new DateRangeQuery("f") { Gte = v }
new DateRangeQuery { Field = "f", LessThan = v }new DateRangeQuery("f") { Lt = v }
new TermRangeQuery { Field = "f", GreaterThan = v }new TermRangeQuery("f") { Gt = v }
new TermRangeQuery { Field = "f", LessThanOrEqualTo = v }new TermRangeQuery("f") { Lte = v }
new QueryStringQuery { Fields = f, Query = q }new QueryStringQuery(q) { Fields = f }
new MultiMatchQuery { Fields = f, Query = q }new MultiMatchQuery(q) { Fields = f }
QueryBaseQuery
QueryContainerQuery

Bool Query Operators

The & and | operators still work on Query, but note that | (OR) no longer implicitly sets minimum_should_match: 1. The parser handles this automatically, but if you build bool queries manually in custom visitors, you must set it explicitly:

csharp
var boolQuery = new BoolQuery
{
    Should = new List<Query> { queryA, queryB },
    MinimumShouldMatch = 1
};

Aggregation Changes

AggregationBase and AggregationContainer are replaced by AggregationMap, a custom intermediate type that bridges the gap between the parser's tree-building phase and the new client's discriminated-union Aggregation type.

Before:

csharp
AggregationContainer aggs = await parser.BuildAggregationsAsync("terms:status");

After:

csharp
AggregationMap aggs = await parser.BuildAggregationsAsync("terms:status");

// Convert to ES client dictionary when needed:
IDictionary<string, Aggregation> dict = aggs.ToDictionary();

Sort Changes

Before (NEST)After (new client)
IFieldSortSortOptions
ISortSortOptions
SortOrder.AscendingSortOrder.Asc
SortOrder.DescendingSortOrder.Desc

The SearchDescriptorExtensions.Sort() extension method has been removed. Use the native SearchRequestDescriptor<T>.Sort() method directly.

Property Mapping Changes

The new client uses a simpler expression-based syntax:

Before (NEST)After (new client)
.Keyword(f => f.Name(e => e.Id)).Keyword(e => e.Id)
.Text(f => f.Name(e => e.Name)).Text(e => e.Name)
.Number(f => f.Name(e => e.Age).Type(NumberType.Integer)).IntegerNumber(e => e.Age)
.Date(f => f.Name(e => e.Created)).Date(e => e.Created)
.Boolean(f => f.Name(e => e.Active)).Boolean(e => e.Active)
.Object<T>(f => f.Name(e => e.Address).Properties(...)).Object(e => e.Address, o => o.Properties(...))
.Nested<T>(f => f.Name(e => e.Items).Properties(...)).Nested(e => e.Items, n => n.Properties(...))
.Dynamic(false).Dynamic(DynamicMapping.False)
m.AutoMap()(removed — define all properties explicitly)

Custom Visitor Migration

If you have custom visitors that set queries, aggregations, or sorts on nodes, update the types:

Before:

csharp
public class MyVisitor : ChainableQueryVisitor
{
    public override async Task VisitAsync(GroupNode node, IQueryVisitorContext context)
    {
        QueryBase query = new TermQuery { Field = "status", Value = "active" };
        node.SetQuery(query);
    }
}

After:

csharp
public class MyVisitor : ChainableQueryVisitor
{
    public override async Task VisitAsync(GroupNode node, IQueryVisitorContext context)
    {
        Query query = new TermQuery("status", "active");
        node.SetQuery(query);
    }
}

Key type changes in the visitor API:

Method / PropertyBeforeAfter
SetQuery()QueryBaseQuery
GetQueryAsync()Task<QueryBase>Task<Query>
SetAggregation()AggregationBaseAggregationMap
GetAggregationAsync()Task<AggregationBase>Task<AggregationMap>
SetSort()IFieldSortSortOptions
GetSort()IFieldSortSortOptions

IElasticQueryVisitorContext Changes

A new property was added to the IElasticQueryVisitorContext interface:

csharp
Func<string, Task<string>> GeoLocationResolver { get; set; }

This enables per-request geo location resolution (e.g., converting zip codes to lat/lon). If you implement IElasticQueryVisitorContext directly (rather than inheriting from ElasticQueryVisitorContext), you must add this property.

Response Validation

Before (NEST)After (new client)
response.IsValidresponse.IsValidResponse

Behavioral Changes

These are not breaking API changes but produce different (improved) Elasticsearch JSON:

Filter-mode queries are flatter

Non-scoring queries now produce flat bool { filter: [a, b, c] } instead of nested bool { filter: [bool { must: [a, b, c] }] }. Functionally equivalent but cleaner.

Explicit minimum_should_match on OR queries

NEST's | operator implicitly set minimum_should_match: 1. The new Elastic.Clients.Elasticsearch client does not, so the parser now explicitly sets it on root-level OR queries and parenthesized OR groups inside AND context.

Nested query negation

Negated nested terms (e.g., NOT nested.field:value) now negate the entire NestedQuery rather than the inner query. This is a correctness fix — the new behavior correctly excludes documents that have any matching nested object.

Integer fields map to long

The new client's auto-mapping maps C# int properties to long instead of integer. This means @field_type metadata on aggregations will report "long" instead of "integer" for int-typed fields.

Migration Checklist

  • [ ] Replace using Nest; with using Elastic.Clients.Elasticsearch; (and sub-namespaces)
  • [ ] Replace IElasticClient / ElasticClient with ElasticsearchClient
  • [ ] Replace ConnectionSettings with ElasticsearchClientSettings
  • [ ] Update BuildQueryAsync return type from QueryContainer to Query
  • [ ] Update BuildAggregationsAsync return type from AggregationContainer to AggregationMap
  • [ ] Update BuildSortAsync return type from IEnumerable<IFieldSort> to ICollection<SortOptions>
  • [ ] Update UseMappings calls (change Func<..., ...> to Action<...>, change client type)
  • [ ] Update property mapping syntax (remove .Name(e => e.Prop) wrapper)
  • [ ] Replace AutoMap() calls with explicit property mappings
  • [ ] Replace .Dynamic(false) with .Dynamic(DynamicMapping.False)
  • [ ] Replace response.IsValid with response.IsValidResponse
  • [ ] Update custom visitors to use Query instead of QueryBase/QueryContainer
  • [ ] Update custom visitors to use AggregationMap instead of AggregationBase
  • [ ] Update custom visitors to use SortOptions instead of IFieldSort
  • [ ] Add GeoLocationResolver property if directly implementing IElasticQueryVisitorContext
  • [ ] Remove SearchDescriptorExtensions.Sort() usage (use native .Sort())
  • [ ] Remove NEST package reference
  • [ ] Update Elasticsearch/Kibana Docker images to 9.x

Released under the MIT License.