From 51fcb8f8b7582602def0c230f09586a12b75a458 Mon Sep 17 00:00:00 2001 From: Florian Bernd Date: Fri, 6 Jun 2025 11:50:06 +0200 Subject: [PATCH] Add AOT example --- Elasticsearch.sln | 9 ++ examples/aot/Program.cs | 55 ++++++++++ examples/aot/aot.csproj | 17 +++ .../Elastic.Clients.Elasticsearch.csproj | 2 +- .../Core/Infer/DynamicPropertyAccessor.cs | 4 +- .../_Shared/Core/Infer/Id/IdResolver.cs | 2 +- .../_Shared/Core/Infer/RoutingResolver.cs | 2 +- src/Playground/Person.cs | 3 + src/Playground/Program.cs | 100 ++++++++++++++++-- 9 files changed, 183 insertions(+), 11 deletions(-) create mode 100644 examples/aot/Program.cs create mode 100644 examples/aot/aot.csproj diff --git a/Elasticsearch.sln b/Elasticsearch.sln index 242fd14cf6b..cecd196bedb 100644 --- a/Elasticsearch.sln +++ b/Elasticsearch.sln @@ -57,6 +57,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests.ClusterLauncher", "te EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "tests\Tests\Tests.csproj", "{6FD804B2-CE80-41CB-A411-2023F34C18FE}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "aot", "examples\aot\aot.csproj", "{3FA9C99A-7DA0-4DF2-89C0-BDDFC97E2CB7}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "examples", "examples", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -103,6 +107,10 @@ Global {6FD804B2-CE80-41CB-A411-2023F34C18FE}.Debug|Any CPU.Build.0 = Debug|Any CPU {6FD804B2-CE80-41CB-A411-2023F34C18FE}.Release|Any CPU.ActiveCfg = Release|Any CPU {6FD804B2-CE80-41CB-A411-2023F34C18FE}.Release|Any CPU.Build.0 = Release|Any CPU + {3FA9C99A-7DA0-4DF2-89C0-BDDFC97E2CB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3FA9C99A-7DA0-4DF2-89C0-BDDFC97E2CB7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3FA9C99A-7DA0-4DF2-89C0-BDDFC97E2CB7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3FA9C99A-7DA0-4DF2-89C0-BDDFC97E2CB7}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -118,6 +126,7 @@ Global {68D1BFDC-F447-4D2C-AF81-537807636610} = {1FE49D14-216A-41EE-A177-E42BFF53E0DC} {F6162603-D134-4121-8106-2BA4DAD7350B} = {362B2776-4B29-46AB-B237-56776B5372B6} {6FD804B2-CE80-41CB-A411-2023F34C18FE} = {362B2776-4B29-46AB-B237-56776B5372B6} + {3FA9C99A-7DA0-4DF2-89C0-BDDFC97E2CB7} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {CE74F821-B001-4C69-A58D-CF81F8B0B632} diff --git a/examples/aot/Program.cs b/examples/aot/Program.cs new file mode 100644 index 00000000000..7532c87693f --- /dev/null +++ b/examples/aot/Program.cs @@ -0,0 +1,55 @@ +using System; +using System.Diagnostics; +using System.Text.Json.Serialization; + +using Elastic.Clients.Elasticsearch; +using Elastic.Clients.Elasticsearch.Serialization; +using Elastic.Transport; +using Elastic.Transport.Extensions; + +namespace AOT; + +public static class Program +{ + public static void Main(string[] args) + { + var nodePool = new SingleNodePool(new Uri("http://localhost:9200")); + var settings = new ElasticsearchClientSettings( + nodePool, + sourceSerializer: (_, settings) => + new DefaultSourceSerializer(settings, UserTypeSerializerContext.Default) + ) + .DefaultMappingFor(x => x.IndexName("index")); + + var client = new ElasticsearchClient(settings); + + var person = new Person + { + Id = 1234, + FirstName = "Florian", + LastName = "Bernd" + }; + + Trace.Assert(client.Infer.Id(person) == "1234"); + + var indexRequest = new IndexRequest(person); + var indexRequestBody = client.ElasticsearchClientSettings.RequestResponseSerializer.SerializeToString(indexRequest); + var indexRequest2 = client.ElasticsearchClientSettings.RequestResponseSerializer.Deserialize>(indexRequestBody)!; + + Trace.Assert(indexRequest.Document == indexRequest2.Document); + } +} + +internal sealed record Person +{ + public long? Id { get; init; } + public required string FirstName { get; init; } + public required string LastName { get; init; } + public DateTimeOffset? BirthDate { get; init; } +} + +[JsonSerializable(typeof(Person), GenerationMode = JsonSourceGenerationMode.Default)] +internal sealed partial class UserTypeSerializerContext : + JsonSerializerContext +{ +} diff --git a/examples/aot/aot.csproj b/examples/aot/aot.csproj new file mode 100644 index 00000000000..cec1671aa11 --- /dev/null +++ b/examples/aot/aot.csproj @@ -0,0 +1,17 @@ + + + + Exe + net9.0 + + enable + true + true + false + + + + + + + diff --git a/src/Elastic.Clients.Elasticsearch/Elastic.Clients.Elasticsearch.csproj b/src/Elastic.Clients.Elasticsearch/Elastic.Clients.Elasticsearch.csproj index bb9197f0bc9..13992fe296f 100644 --- a/src/Elastic.Clients.Elasticsearch/Elastic.Clients.Elasticsearch.csproj +++ b/src/Elastic.Clients.Elasticsearch/Elastic.Clients.Elasticsearch.csproj @@ -14,7 +14,7 @@ true true - netstandard2.0;net462;netstandard2.1;net8.0 + netstandard2.0;net462;netstandard2.1;net8.0;net9.0 true true annotations diff --git a/src/Elastic.Clients.Elasticsearch/_Shared/Core/Infer/DynamicPropertyAccessor.cs b/src/Elastic.Clients.Elasticsearch/_Shared/Core/Infer/DynamicPropertyAccessor.cs index 74e8aaf3bea..deb5e175874 100644 --- a/src/Elastic.Clients.Elasticsearch/_Shared/Core/Infer/DynamicPropertyAccessor.cs +++ b/src/Elastic.Clients.Elasticsearch/_Shared/Core/Infer/DynamicPropertyAccessor.cs @@ -50,9 +50,9 @@ internal static class DynamicPropertyAccessor // Build compiled getter delegate. -#pragma warning disable IL3050 +#pragma warning disable IL3050, IL2060 var getterDelegateFactory = MakeDelegateMethodInfo.MakeGenericMethod(type, getterMethod.ReturnType); -#pragma warning restore IL3050 +#pragma warning restore IL3050, IL2060 var genericGetterDelegate = (Func)getterDelegateFactory.Invoke(null, [getterMethod])!; return instance => retrieverFunc(genericGetterDelegate, instance); diff --git a/src/Elastic.Clients.Elasticsearch/_Shared/Core/Infer/Id/IdResolver.cs b/src/Elastic.Clients.Elasticsearch/_Shared/Core/Infer/Id/IdResolver.cs index 59ad5ebc601..27ac07a6558 100644 --- a/src/Elastic.Clients.Elasticsearch/_Shared/Core/Infer/Id/IdResolver.cs +++ b/src/Elastic.Clients.Elasticsearch/_Shared/Core/Infer/Id/IdResolver.cs @@ -28,7 +28,7 @@ public IdResolver(IElasticsearchClientSettings settings) return null; } - return Resolve(instance.GetType(), instance); + return Resolve(typeof(T), instance); } public string? Resolve([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] Type type, object instance) diff --git a/src/Elastic.Clients.Elasticsearch/_Shared/Core/Infer/RoutingResolver.cs b/src/Elastic.Clients.Elasticsearch/_Shared/Core/Infer/RoutingResolver.cs index b950e5c082e..78a3139f95c 100644 --- a/src/Elastic.Clients.Elasticsearch/_Shared/Core/Infer/RoutingResolver.cs +++ b/src/Elastic.Clients.Elasticsearch/_Shared/Core/Infer/RoutingResolver.cs @@ -30,7 +30,7 @@ public RoutingResolver(IElasticsearchClientSettings settings, IdResolver idResol return null; } - return Resolve(instance.GetType(), instance); + return Resolve(typeof(T), instance); } public string? Resolve([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] Type type, object instance) diff --git a/src/Playground/Person.cs b/src/Playground/Person.cs index c1107bcfba3..62f758eab7d 100644 --- a/src/Playground/Person.cs +++ b/src/Playground/Person.cs @@ -4,6 +4,7 @@ using System.Runtime.Serialization; using Elastic.Clients.Elasticsearch; +using Elastic.Clients.Elasticsearch.QueryDsl; namespace Playground { @@ -30,6 +31,8 @@ public class Person public string Data { get; init; } = "NOTHING"; public DateTimeKind Enum { get; init; } + + public Query? Q { get; init; } } public class PersonV3 diff --git a/src/Playground/Program.cs b/src/Playground/Program.cs index ad854df1d61..77200187fe7 100644 --- a/src/Playground/Program.cs +++ b/src/Playground/Program.cs @@ -2,19 +2,107 @@ // 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.Runtime.CompilerServices; +using System.Text.Json; +using System.Text.Json.Serialization; using Elastic.Clients.Elasticsearch; +using Elastic.Clients.Elasticsearch.Aggregations; +using Elastic.Clients.Elasticsearch.Nodes; +using Elastic.Clients.Elasticsearch.QueryDsl; +using Elastic.Clients.Elasticsearch.Requests; +using Elastic.Clients.Elasticsearch.Serialization; using Elastic.Transport; - +using Elastic.Transport.Extensions; using Playground; +RequestConfiguration? globalRequestConfiguration = null; +ConditionalWeakTable? globalRequestConfigurations = null; + var settings = new ElasticsearchClientSettings(new Uri("https://primary.es.europe-west3.gcp.cloud.es.io")) .Authentication(new BasicAuthentication("elastic", "Oov35Wtxj5DzpZNzYAzFb0KZ")) - .DisableDirectStreaming() - .EnableDebugMode(cd => + .OnBeforeRequest(OnBeforeRequest) + .EnableDebugMode(cd => Console.WriteLine(cd.DebugInformation)); +void OnBeforeRequest(ElasticsearchClient client, Request request, EndpointPath endpointPath, ref PostData? postData, ref IRequestConfiguration? requestConfiguration) +{ + // Each time a request is made, the transport creates a new `BoundConfiguration` for every `IRequestConfiguration` + // that is not in the cache (based on reference equality). + + // To prevent frequent allocations of our mutated request configurations (and the secondary allocations for + // `BoundConfiguration`), we have to maintain a custom cache that maps every original request configuration to the + // mutated one. + + if (requestConfiguration is null) + { + globalRequestConfiguration = Interlocked.CompareExchange( + ref globalRequestConfiguration, + new RequestConfiguration + { + UserAgent = UserAgent.Create("my-custom-user-agent") + }, + null) ?? globalRequestConfiguration; + + requestConfiguration = globalRequestConfiguration; + return; + } + + if (requestConfiguration is not RequestConfiguration rc) + { + // Only `RequestConfiguration` (not all implementations of `IRequestConfiguration`) gets cached in the + // internal cache. + requestConfiguration = MutateRequestConfiguration(requestConfiguration); + return; + } + + // ReSharper disable InconsistentlySynchronizedField + + var cache = (Interlocked.CompareExchange( + ref globalRequestConfigurations, + new ConditionalWeakTable(), + null + ) ?? globalRequestConfigurations); + + if (cache.TryGetValue(rc, out var mutatedRequestConfiguration)) { - //var request = System.Text.Encoding.Default.GetString(cd.RequestBodyInBytes); - Console.WriteLine(cd.DebugInformation); - }); + requestConfiguration = mutatedRequestConfiguration; + return; + } + + mutatedRequestConfiguration = MutateRequestConfiguration(rc); + +#if NET8_0_OR_GREATER + cache.TryAdd(rc, mutatedRequestConfiguration); +#else + lock (cache) + { + cache.Add(rc, mutatedRequestConfiguration); + } +#endif + + // ReSharper restore InconsistentlySynchronizedField + + return; + + RequestConfiguration MutateRequestConfiguration(IRequestConfiguration requestConfiguration) + { + return new RequestConfiguration(requestConfiguration) + { + UserAgent = UserAgent.Create("my-custom-user-agent") + }; + } +} var client = new ElasticsearchClient(settings); +var s = settings.SourceSerializer.SerializeToString(new Person +{ + Q = new Query + { + MatchAll = new MatchAllQuery() + } +}); + +await client.ExplainAsync("my-tweet-index", 1); +await client.DeleteAsync("my-tweet-index", 1); + +BulkRequestDescriptor descriptor = new(); +descriptor.Index("indexName");