diff --git a/src/services/search/Elastic.Documentation.Search/Common/ElasticsearchClientAccessor.cs b/src/services/search/Elastic.Documentation.Search/Common/ElasticsearchClientAccessor.cs index fbb9edd72..b06d06166 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,18 +64,12 @@ 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, + ElasticsearchClientJsonResolver.Default, + static jsonOptions => jsonOptions.Converters.Add(RuleQueryMatchCriteriaJsonConverterFactory.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..eb34abae9 --- /dev/null +++ b/src/services/search/Elastic.Documentation.Search/Common/RuleQueryMatchCriteriaJsonConverter.cs @@ -0,0 +1,76 @@ +// 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.Reflection; +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 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(); + + 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 +{ + [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)!; + + [UnconditionalSuppressMessage( + "Trimming", + "IL2075", + Justification = "RuleQueryMatchCriteria.QueryString is preserved via DynamicDependency on RuleQueryMatchCriteriaType.")] + public static string GetQueryString(object target) => (string)QueryStringProperty.GetValue(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..a2e4291d8 --- /dev/null +++ b/src/services/search/Elastic.Documentation.Search/Common/RuleQueryMatchCriteriaTypeInfoResolver.cs @@ -0,0 +1,46 @@ +// 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) => + 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