From c51356c8c773795d399b546e89aff7ffde75a3e8 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Mon, 1 Jun 2026 14:26:09 +0200 Subject: [PATCH 1/4] Fix AOT build failure in ElasticsearchClientAccessor after search refactor PR #3364 introduced DefaultJsonTypeInfoResolver for ES client serialization, which breaks native AOT publish for Elastic.Documentation.Mcp.Remote. Restore source-generated EsJsonContext for Elastic.Internal.Search document types. Co-Authored-By: Claude Sonnet 4.6 (1M context) Co-authored-by: Cursor --- .../Common/ElasticsearchClientAccessor.cs | 11 +---------- .../Elastic.Documentation.Search/EsJsonContext.cs | 12 ++++++++++++ 2 files changed, 13 insertions(+), 10 deletions(-) create mode 100644 src/services/search/Elastic.Documentation.Search/EsJsonContext.cs diff --git a/src/services/search/Elastic.Documentation.Search/Common/ElasticsearchClientAccessor.cs b/src/services/search/Elastic.Documentation.Search/Common/ElasticsearchClientAccessor.cs index fbb9edd72..cf5f08961 100644 --- a/src/services/search/Elastic.Documentation.Search/Common/ElasticsearchClientAccessor.cs +++ b/src/services/search/Elastic.Documentation.Search/Common/ElasticsearchClientAccessor.cs @@ -2,7 +2,6 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -using System.Text.Json.Serialization.Metadata; using Elastic.Clients.Elasticsearch; using Elastic.Clients.Elasticsearch.Serialization; using Elastic.Documentation.Configuration; @@ -65,17 +64,9 @@ SearchConfiguration searchConfiguration ? new BasicAuthentication(username, password) : null!; - // Combine the contract's source-gen context with a reflection fallback so that - // internal package types (e.g. RuleQueryMatchCriteria from the Elasticsearch impl - // package) are still serializable when the ES client delegates to the source serializer. - var resolver = JsonTypeInfoResolver.Combine( - SourceGenerationContext.Default, - new DefaultJsonTypeInfoResolver() - ); - _clientSettings = new ElasticsearchClientSettings( _nodePool, - sourceSerializer: (_, settings) => new DefaultSourceSerializer(settings, resolver, null) + sourceSerializer: (_, settings) => new DefaultSourceSerializer(settings, EsJsonContext.Default) ) .DefaultIndex(SearchIndex) .Authentication(auth); diff --git a/src/services/search/Elastic.Documentation.Search/EsJsonContext.cs b/src/services/search/Elastic.Documentation.Search/EsJsonContext.cs new file mode 100644 index 000000000..e57186feb --- /dev/null +++ b/src/services/search/Elastic.Documentation.Search/EsJsonContext.cs @@ -0,0 +1,12 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.Text.Json.Serialization; +using Elastic.Internal.Search; + +namespace Elastic.Documentation.Search; + +[JsonSerializable(typeof(DocumentationDocument))] +[JsonSerializable(typeof(ParentDocument))] +internal sealed partial class EsJsonContext : JsonSerializerContext; From 3042451540ce28fdd0ffeff5ea78e4ad241dec5e Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Mon, 1 Jun 2026 14:42:44 +0200 Subject: [PATCH 2/4] Combine search JSON resolvers and fix RuleQueryMatchCriteria serialization Wire Elastic.Internal.Search.SourceGenerationContext together with docs-builder SourceGenerationContext, and add an AOT-safe converter for the internal RuleQueryMatchCriteria type used by query rules. Co-Authored-By: Claude Sonnet 4.6 (1M context) Co-authored-by: Cursor --- .../Common/ElasticsearchClientAccessor.cs | 6 ++- .../Common/ElasticsearchClientJsonResolver.cs | 27 ++++++++++ .../RuleQueryMatchCriteriaJsonConverter.cs | 44 +++++++++++++++++ .../RuleQueryMatchCriteriaTypeInfoResolver.cs | 49 +++++++++++++++++++ .../Elastic.Documentation.Search.csproj | 1 + .../EsJsonContext.cs | 12 ----- 6 files changed, 125 insertions(+), 14 deletions(-) create mode 100644 src/services/search/Elastic.Documentation.Search/Common/ElasticsearchClientJsonResolver.cs create mode 100644 src/services/search/Elastic.Documentation.Search/Common/RuleQueryMatchCriteriaJsonConverter.cs create mode 100644 src/services/search/Elastic.Documentation.Search/Common/RuleQueryMatchCriteriaTypeInfoResolver.cs delete mode 100644 src/services/search/Elastic.Documentation.Search/EsJsonContext.cs diff --git a/src/services/search/Elastic.Documentation.Search/Common/ElasticsearchClientAccessor.cs b/src/services/search/Elastic.Documentation.Search/Common/ElasticsearchClientAccessor.cs index cf5f08961..f1c98004a 100644 --- a/src/services/search/Elastic.Documentation.Search/Common/ElasticsearchClientAccessor.cs +++ b/src/services/search/Elastic.Documentation.Search/Common/ElasticsearchClientAccessor.cs @@ -66,8 +66,10 @@ SearchConfiguration searchConfiguration _clientSettings = new ElasticsearchClientSettings( _nodePool, - sourceSerializer: (_, settings) => new DefaultSourceSerializer(settings, EsJsonContext.Default) - ) + sourceSerializer: (_, settings) => new DefaultSourceSerializer( + settings, + ElasticsearchClientJsonResolver.Default, + static jsonOptions => jsonOptions.Converters.Add(RuleQueryMatchCriteriaJsonConverter.Instance))) .DefaultIndex(SearchIndex) .Authentication(auth); diff --git a/src/services/search/Elastic.Documentation.Search/Common/ElasticsearchClientJsonResolver.cs b/src/services/search/Elastic.Documentation.Search/Common/ElasticsearchClientJsonResolver.cs new file mode 100644 index 000000000..95fb70d1c --- /dev/null +++ b/src/services/search/Elastic.Documentation.Search/Common/ElasticsearchClientJsonResolver.cs @@ -0,0 +1,27 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.Text.Json.Serialization.Metadata; +using Elastic.Documentation.Serialization; +using InternalSearch = Elastic.Internal.Search; + +namespace Elastic.Documentation.Search.Common; + +/// +/// Combined JSON type info resolver for the shared Elasticsearch client: external search contract types, +/// docs-builder document metadata, and internal query-rule criteria from the Elasticsearch search package. +/// +internal static class ElasticsearchClientJsonResolver +{ + public static IJsonTypeInfoResolver Default { get; } = Create(); + + private static IJsonTypeInfoResolver Create() + { + var combined = JsonTypeInfoResolver.Combine( + InternalSearch.SourceGenerationContext.Default, + SourceGenerationContext.Default); + + return new RuleQueryMatchCriteriaTypeInfoResolver(combined); + } +} diff --git a/src/services/search/Elastic.Documentation.Search/Common/RuleQueryMatchCriteriaJsonConverter.cs b/src/services/search/Elastic.Documentation.Search/Common/RuleQueryMatchCriteriaJsonConverter.cs new file mode 100644 index 000000000..86d328781 --- /dev/null +++ b/src/services/search/Elastic.Documentation.Search/Common/RuleQueryMatchCriteriaJsonConverter.cs @@ -0,0 +1,44 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Elastic.Documentation.Search.Common; + +/// +/// Serializes Elastic.Internal.Search.Elasticsearch.RuleQueryMatchCriteria, which is internal to the +/// Elasticsearch search package and therefore not covered by public types. +/// +internal sealed class RuleQueryMatchCriteriaJsonConverter : JsonConverter +{ + public static readonly RuleQueryMatchCriteriaJsonConverter Instance = new(); + + public override object? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => + throw new NotSupportedException(); + + public override void Write(Utf8JsonWriter writer, object? value, JsonSerializerOptions options) + { + if (value is null) + { + writer.WriteNullValue(); + return; + } + + var queryString = RuleQueryMatchCriteriaAccessors.GetQueryString(value); + writer.WriteStartObject(); + writer.WriteString("query_string", queryString); + writer.WriteEndObject(); + } +} + +internal static class RuleQueryMatchCriteriaAccessors +{ + [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "get_QueryString")] + private static extern string GetQueryStringInternal(object target); + + public static string GetQueryString(object target) => GetQueryStringInternal(target); +} diff --git a/src/services/search/Elastic.Documentation.Search/Common/RuleQueryMatchCriteriaTypeInfoResolver.cs b/src/services/search/Elastic.Documentation.Search/Common/RuleQueryMatchCriteriaTypeInfoResolver.cs new file mode 100644 index 000000000..bbce88ae4 --- /dev/null +++ b/src/services/search/Elastic.Documentation.Search/Common/RuleQueryMatchCriteriaTypeInfoResolver.cs @@ -0,0 +1,49 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; + +namespace Elastic.Documentation.Search.Common; + +/// +/// Supplies for internal Elasticsearch search query types not declared on public contexts. +/// +internal sealed class RuleQueryMatchCriteriaTypeInfoResolver(IJsonTypeInfoResolver inner) : IJsonTypeInfoResolver +{ + [DynamicDependency( + DynamicallyAccessedMemberTypes.All, + "Elastic.Internal.Search.Elasticsearch.RuleQueryMatchCriteria", + "Elastic.Internal.Search.Elasticsearch")] + private static readonly Type RuleQueryMatchCriteriaType = Type.GetType( + "Elastic.Internal.Search.Elasticsearch.RuleQueryMatchCriteria, Elastic.Internal.Search.Elasticsearch", + throwOnError: true)!; + + private JsonTypeInfo? _ruleQueryMatchCriteriaTypeInfo; + + public JsonTypeInfo? GetTypeInfo(Type type, JsonSerializerOptions options) + { + if (type == RuleQueryMatchCriteriaType) + { + return _ruleQueryMatchCriteriaTypeInfo ??= CreateRuleQueryMatchCriteriaTypeInfo(options); + } + + return inner.GetTypeInfo(type, options); + } + + [UnconditionalSuppressMessage( + "Trimming", + "IL2026", + Justification = "RuleQueryMatchCriteria is internal to Elastic.Internal.Search.Elasticsearch; serialization uses an UnsafeAccessor-based converter.")] + [UnconditionalSuppressMessage( + "AOT", + "IL3050", + Justification = "RuleQueryMatchCriteria is internal to Elastic.Internal.Search.Elasticsearch; serialization uses an UnsafeAccessor-based converter.")] + private static JsonTypeInfo CreateRuleQueryMatchCriteriaTypeInfo(JsonSerializerOptions options) + { + options.Converters.Add(RuleQueryMatchCriteriaJsonConverter.Instance); + return JsonTypeInfo.CreateJsonTypeInfo(RuleQueryMatchCriteriaType, options); + } +} diff --git a/src/services/search/Elastic.Documentation.Search/Elastic.Documentation.Search.csproj b/src/services/search/Elastic.Documentation.Search/Elastic.Documentation.Search.csproj index 20108904c..d94155819 100644 --- a/src/services/search/Elastic.Documentation.Search/Elastic.Documentation.Search.csproj +++ b/src/services/search/Elastic.Documentation.Search/Elastic.Documentation.Search.csproj @@ -6,6 +6,7 @@ enable Elastic.Documentation.Search Elastic.Documentation.Search + true diff --git a/src/services/search/Elastic.Documentation.Search/EsJsonContext.cs b/src/services/search/Elastic.Documentation.Search/EsJsonContext.cs deleted file mode 100644 index e57186feb..000000000 --- a/src/services/search/Elastic.Documentation.Search/EsJsonContext.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Licensed to Elasticsearch B.V under one or more agreements. -// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information - -using System.Text.Json.Serialization; -using Elastic.Internal.Search; - -namespace Elastic.Documentation.Search; - -[JsonSerializable(typeof(DocumentationDocument))] -[JsonSerializable(typeof(ParentDocument))] -internal sealed partial class EsJsonContext : JsonSerializerContext; From bbe775a5f793781db0688c3702caaaa7c4e2f5b7 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Mon, 1 Jun 2026 15:13:08 +0200 Subject: [PATCH 3/4] Fix read-only JsonSerializerOptions mutation in search resolver Register RuleQueryMatchCriteria converter via JsonConverterFactory during DefaultSourceSerializer setup only. Do not modify options when lazily creating JsonTypeInfo after they are frozen. Co-Authored-By: Claude Sonnet 4.6 (1M context) Co-authored-by: Cursor --- .../Common/ElasticsearchClientAccessor.cs | 2 +- .../RuleQueryMatchCriteriaJsonConverter.cs | 18 ++++++++++++++++++ .../RuleQueryMatchCriteriaTypeInfoResolver.cs | 7 ++----- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/services/search/Elastic.Documentation.Search/Common/ElasticsearchClientAccessor.cs b/src/services/search/Elastic.Documentation.Search/Common/ElasticsearchClientAccessor.cs index f1c98004a..b06d06166 100644 --- a/src/services/search/Elastic.Documentation.Search/Common/ElasticsearchClientAccessor.cs +++ b/src/services/search/Elastic.Documentation.Search/Common/ElasticsearchClientAccessor.cs @@ -69,7 +69,7 @@ SearchConfiguration searchConfiguration sourceSerializer: (_, settings) => new DefaultSourceSerializer( settings, ElasticsearchClientJsonResolver.Default, - static jsonOptions => jsonOptions.Converters.Add(RuleQueryMatchCriteriaJsonConverter.Instance))) + static jsonOptions => jsonOptions.Converters.Add(RuleQueryMatchCriteriaJsonConverterFactory.Instance))) .DefaultIndex(SearchIndex) .Authentication(auth); diff --git a/src/services/search/Elastic.Documentation.Search/Common/RuleQueryMatchCriteriaJsonConverter.cs b/src/services/search/Elastic.Documentation.Search/Common/RuleQueryMatchCriteriaJsonConverter.cs index 86d328781..1fab6e5b9 100644 --- a/src/services/search/Elastic.Documentation.Search/Common/RuleQueryMatchCriteriaJsonConverter.cs +++ b/src/services/search/Elastic.Documentation.Search/Common/RuleQueryMatchCriteriaJsonConverter.cs @@ -13,6 +13,24 @@ namespace Elastic.Documentation.Search.Common; /// Serializes Elastic.Internal.Search.Elasticsearch.RuleQueryMatchCriteria, which is internal to the /// Elasticsearch search package and therefore not covered by public types. /// +internal sealed class RuleQueryMatchCriteriaJsonConverterFactory : JsonConverterFactory +{ + public static readonly RuleQueryMatchCriteriaJsonConverterFactory Instance = new(); + + [DynamicDependency( + DynamicallyAccessedMemberTypes.All, + "Elastic.Internal.Search.Elasticsearch.RuleQueryMatchCriteria", + "Elastic.Internal.Search.Elasticsearch")] + private static readonly Type RuleQueryMatchCriteriaType = Type.GetType( + "Elastic.Internal.Search.Elasticsearch.RuleQueryMatchCriteria, Elastic.Internal.Search.Elasticsearch", + throwOnError: true)!; + + public override bool CanConvert(Type typeToConvert) => typeToConvert == RuleQueryMatchCriteriaType; + + public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) => + RuleQueryMatchCriteriaJsonConverter.Instance; +} + internal sealed class RuleQueryMatchCriteriaJsonConverter : JsonConverter { public static readonly RuleQueryMatchCriteriaJsonConverter Instance = new(); diff --git a/src/services/search/Elastic.Documentation.Search/Common/RuleQueryMatchCriteriaTypeInfoResolver.cs b/src/services/search/Elastic.Documentation.Search/Common/RuleQueryMatchCriteriaTypeInfoResolver.cs index bbce88ae4..a2e4291d8 100644 --- a/src/services/search/Elastic.Documentation.Search/Common/RuleQueryMatchCriteriaTypeInfoResolver.cs +++ b/src/services/search/Elastic.Documentation.Search/Common/RuleQueryMatchCriteriaTypeInfoResolver.cs @@ -41,9 +41,6 @@ internal sealed class RuleQueryMatchCriteriaTypeInfoResolver(IJsonTypeInfoResolv "AOT", "IL3050", Justification = "RuleQueryMatchCriteria is internal to Elastic.Internal.Search.Elasticsearch; serialization uses an UnsafeAccessor-based converter.")] - private static JsonTypeInfo CreateRuleQueryMatchCriteriaTypeInfo(JsonSerializerOptions options) - { - options.Converters.Add(RuleQueryMatchCriteriaJsonConverter.Instance); - return JsonTypeInfo.CreateJsonTypeInfo(RuleQueryMatchCriteriaType, options); - } + private static JsonTypeInfo CreateRuleQueryMatchCriteriaTypeInfo(JsonSerializerOptions options) => + JsonTypeInfo.CreateJsonTypeInfo(RuleQueryMatchCriteriaType, options); } From c788806944f66486299e49de4c779093389329c8 Mon Sep 17 00:00:00 2001 From: Martijn Laarman Date: Mon, 1 Jun 2026 15:30:21 +0200 Subject: [PATCH 4/4] Fix RuleQueryMatchCriteria accessor for cross-assembly internal type UnsafeAccessor on object resolved get_QueryString on System.Object. Read QueryString via reflection with trim annotations instead. Co-Authored-By: Claude Sonnet 4.6 (1M context) Co-authored-by: Cursor --- .../RuleQueryMatchCriteriaJsonConverter.cs | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/services/search/Elastic.Documentation.Search/Common/RuleQueryMatchCriteriaJsonConverter.cs b/src/services/search/Elastic.Documentation.Search/Common/RuleQueryMatchCriteriaJsonConverter.cs index 1fab6e5b9..eb34abae9 100644 --- a/src/services/search/Elastic.Documentation.Search/Common/RuleQueryMatchCriteriaJsonConverter.cs +++ b/src/services/search/Elastic.Documentation.Search/Common/RuleQueryMatchCriteriaJsonConverter.cs @@ -3,7 +3,7 @@ // See the LICENSE file in the project root for more information using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; +using System.Reflection; using System.Text.Json; using System.Text.Json.Serialization; @@ -55,8 +55,22 @@ public override void Write(Utf8JsonWriter writer, object? value, JsonSerializerO internal static class RuleQueryMatchCriteriaAccessors { - [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "get_QueryString")] - private static extern string GetQueryStringInternal(object target); + [DynamicDependency( + DynamicallyAccessedMemberTypes.PublicProperties, + "Elastic.Internal.Search.Elasticsearch.RuleQueryMatchCriteria", + "Elastic.Internal.Search.Elasticsearch")] + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] + private static readonly Type RuleQueryMatchCriteriaType = Type.GetType( + "Elastic.Internal.Search.Elasticsearch.RuleQueryMatchCriteria, Elastic.Internal.Search.Elasticsearch", + throwOnError: true)!; + + private static readonly PropertyInfo QueryStringProperty = RuleQueryMatchCriteriaType.GetProperty( + "QueryString", + BindingFlags.Public | BindingFlags.Instance)!; - public static string GetQueryString(object target) => GetQueryStringInternal(target); + [UnconditionalSuppressMessage( + "Trimming", + "IL2075", + Justification = "RuleQueryMatchCriteria.QueryString is preserved via DynamicDependency on RuleQueryMatchCriteriaType.")] + public static string GetQueryString(object target) => (string)QueryStringProperty.GetValue(target)!; }