Skip to content

Commit 42b43e9

Browse files
reakaleekclaude
andauthored
EsSitemapReader: Fix AOT serialization of ES search body (#2969)
* EsSitemapReader: Fix AOT serialization of ES search body Anonymous types in BuildSearchBody and ClosePitAsync cannot be serialized by System.Text.Json in AOT/trimmed mode (no reflection), causing malformed request bodies and 400 errors from ES in CI. Replace anonymous types with JsonObject/JsonArray (AOT-safe) and use PostData.String() instead of PostData.Serializable(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix CA2241 warning in ApplicabilitySwitchTests The "because" string contained `{8 hex digits}` which the CA2241 analyzer interprets as a format placeholder. Remove the redundant reason string since the regex pattern is self-documenting. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix misleading test name: first page has PIT, just no search_after Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix remaining AOT trim analysis errors in EsSitemapReader - JsonArray.Add<JsonValue>() has RequiresUnreferencedCodeAttribute; use implicit JsonNode conversion instead - JsonSerializer.Serialize<Dictionary>() requires reflection; use JsonObject.ToJsonString() instead Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 34f9bef commit 42b43e9

3 files changed

Lines changed: 37 additions & 18 deletions

File tree

src/services/Elastic.Documentation.Assembler/Building/EsSitemapReader.cs

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public async IAsyncEnumerable<SitemapEntry> ReadAllAsync([EnumeratorCancellation
3131
do
3232
{
3333
var body = BuildSearchBody(pitId, lastSortValues);
34-
var response = await transport.PostAsync<JsonResponse>("/_search", PostData.Serializable(body), ct);
34+
var response = await transport.PostAsync<JsonResponse>("/_search", PostData.String(body), ct);
3535

3636
if (!response.ApiCallDetails.HasSuccessfulStatusCode)
3737
throw new InvalidOperationException(
@@ -109,7 +109,8 @@ private async Task ClosePitAsync(string pitId, Cancel ct)
109109
{
110110
try
111111
{
112-
_ = await transport.DeleteAsync<VoidResponse>("/_pit", new DefaultRequestParameters(), PostData.Serializable(new { id = pitId }), ct);
112+
var body = new JsonObject { ["id"] = pitId }.ToJsonString();
113+
_ = await transport.DeleteAsync<VoidResponse>("/_pit", new DefaultRequestParameters(), PostData.String(body), ct);
113114
logger.LogInformation("Closed PIT");
114115
}
115116
catch (OperationCanceledException)
@@ -122,20 +123,41 @@ private async Task ClosePitAsync(string pitId, Cancel ct)
122123
}
123124
}
124125

125-
internal static object BuildSearchBody(string pitId, string[]? searchAfter)
126+
// Returns pre-built JSON string because this assembly is published with AOT trimming.
127+
// System.Text.Json cannot serialize anonymous types or unregistered objects via reflection in AOT mode,
128+
// so we build the JSON with JsonObject/JsonArray and send it via PostData.String() instead of PostData.Serializable().
129+
internal static string BuildSearchBody(string pitId, string[]? searchAfter)
126130
{
127-
var body = new Dictionary<string, object>
131+
var body = new JsonObject
128132
{
129133
["size"] = PageSize,
130-
["_source"] = new[] { "url", "last_updated" },
131-
["query"] = new { @bool = new { must_not = new[] { new { term = new { hidden = true } } } } },
132-
["pit"] = new { id = pitId, keep_alive = PitKeepAlive },
133-
["sort"] = new[] { new { url = "asc" } }
134+
["_source"] = new JsonArray("url", "last_updated"),
135+
["query"] = new JsonObject
136+
{
137+
["bool"] = new JsonObject
138+
{
139+
["must_not"] = new JsonArray(new JsonObject
140+
{
141+
["term"] = new JsonObject { ["hidden"] = true }
142+
})
143+
}
144+
},
145+
["pit"] = new JsonObject
146+
{
147+
["id"] = pitId,
148+
["keep_alive"] = PitKeepAlive
149+
},
150+
["sort"] = new JsonArray(new JsonObject { ["url"] = "asc" })
134151
};
135152

136153
if (searchAfter is { Length: > 0 })
137-
body["search_after"] = searchAfter;
154+
{
155+
var sortArray = new JsonArray();
156+
foreach (var value in searchAfter)
157+
sortArray.Add((JsonNode)value);
158+
body["search_after"] = sortArray;
159+
}
138160

139-
return body;
161+
return body.ToJsonString();
140162
}
141163
}

tests/Elastic.Documentation.Build.Tests/SitemapTests.cs

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,10 @@ public void Generate_OrdersUrlsAlphabetically()
9595
}
9696

9797
[Fact]
98-
public void BuildSearchBody_FirstPage_HasNoPitAndNoSearchAfter()
98+
public void BuildSearchBody_FirstPage_HasPitButNoSearchAfter()
9999
{
100100
// Act
101-
var body = EsSitemapReader.BuildSearchBody("test-pit-id", null);
102-
var json = JsonSerializer.Serialize(body);
101+
var json = EsSitemapReader.BuildSearchBody("test-pit-id", null);
103102

104103
// Assert
105104
json.Should().Contain("\"pit\"");
@@ -113,8 +112,7 @@ public void BuildSearchBody_FirstPage_HasNoPitAndNoSearchAfter()
113112
public void BuildSearchBody_SubsequentPage_IncludesSearchAfter()
114113
{
115114
// Act
116-
var body = EsSitemapReader.BuildSearchBody("test-pit-id", ["/docs/last-url"]);
117-
var json = JsonSerializer.Serialize(body);
115+
var json = EsSitemapReader.BuildSearchBody("test-pit-id", ["/docs/last-url"]);
118116

119117
// Assert
120118
json.Should().Contain("\"search_after\"");
@@ -125,8 +123,7 @@ public void BuildSearchBody_SubsequentPage_IncludesSearchAfter()
125123
public void BuildSearchBody_EscapesSpecialCharactersInPitId()
126124
{
127125
// Act
128-
var body = EsSitemapReader.BuildSearchBody("pit-with-\"quotes\"", null);
129-
var json = JsonSerializer.Serialize(body);
126+
var json = EsSitemapReader.BuildSearchBody("pit-with-\"quotes\"", null);
130127
var doc = JsonDocument.Parse(json);
131128

132129
// Assert — verify the value round-trips correctly through serialization

tests/Elastic.Markdown.Tests/Directives/ApplicabilitySwitchTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ public void GeneratesConsistentSyncKeysForYamlObjects()
217217

218218
// Also verify the key has the expected format
219219
key1.Should().StartWith("applies-", "Sync key should start with 'applies-' prefix");
220-
key1.Should().MatchRegex(@"^applies-[0-9A-F]{8}$", "Sync key should be in format 'applies-{8 hex digits}'");
220+
key1.Should().MatchRegex(@"^applies-[0-9A-F]{8}$");
221221
}
222222
}
223223

0 commit comments

Comments
 (0)