Skip to content

Commit 171bde2

Browse files
JusterZhuclaude
andauthored
fix(core): eliminate all AOT-incompatible patterns for Native AOT support
Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 55bdfde commit 171bde2

7 files changed

Lines changed: 83 additions & 33 deletions

File tree

src/c#/GeneralUpdate.Core/Configuration/AbstractBootstrap.cs

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,12 @@ public abstract class AbstractBootstrap<TBootstrap, TStrategy>
3636
{
3737
private readonly ConcurrentDictionary<Option, OptionValue> _options;
3838

39-
/// <summary>User-registered extension type mappings (interface type → implementation type), used for lazy instantiation.</summary>
39+
/// <summary>User-registered extension type mappings (interface type → implementation type), used for type metadata queries.</summary>
4040
private readonly Dictionary<Type, Type> _extensions = new();
4141

42+
/// <summary>User-registered extension factory delegates (interface type → factory function), used for lazy instantiation.</summary>
43+
private readonly Dictionary<Type, Func<object>> _extensionFactories = new();
44+
4245
/// <summary>Registered singleton instances (e.g., <c>BlackPolicy</c>).</summary>
4346
private readonly Dictionary<Type, object> _instances = new();
4447

@@ -95,7 +98,7 @@ protected T GetOption<T>(Option<T>? option)
9598
}
9699

97100
// ═══════════ Extension point registration ═══════════
98-
101+
99102
/// <summary>
100103
/// Registers an update status reporter for reporting update progress and results
101104
/// to a server (e.g., GeneralSpacestation).
@@ -115,6 +118,7 @@ protected T GetOption<T>(Option<T>? option)
115118
public TBootstrap UpdateReporter<T>() where T : Download.Reporting.IUpdateReporter, new()
116119
{
117120
_extensions[typeof(Download.Reporting.IUpdateReporter)] = typeof(T);
121+
_extensionFactories[typeof(Download.Reporting.IUpdateReporter)] = () => new T();
118122
return (TBootstrap)this;
119123
}
120124

@@ -134,6 +138,7 @@ protected T GetOption<T>(Option<T>? option)
134138
public TBootstrap Strategy<T>() where T : IStrategy, new()
135139
{
136140
_extensions[typeof(IStrategy)] = typeof(T);
141+
_extensionFactories[typeof(IStrategy)] = () => new T();
137142
return (TBootstrap)this;
138143
}
139144

@@ -157,6 +162,7 @@ protected T GetOption<T>(Option<T>? option)
157162
public TBootstrap Hooks<T>() where T : Hooks.IUpdateHooks, new()
158163
{
159164
_extensions[typeof(Hooks.IUpdateHooks)] = typeof(T);
165+
_extensionFactories[typeof(Hooks.IUpdateHooks)] = () => new T();
160166
return (TBootstrap)this;
161167
}
162168

@@ -176,6 +182,7 @@ protected T GetOption<T>(Option<T>? option)
176182
public TBootstrap SslPolicy<T>() where T : Security.ISslValidationPolicy, new()
177183
{
178184
_extensions[typeof(Security.ISslValidationPolicy)] = typeof(T);
185+
_extensionFactories[typeof(Security.ISslValidationPolicy)] = () => new T();
179186
return (TBootstrap)this;
180187
}
181188

@@ -196,6 +203,7 @@ protected T GetOption<T>(Option<T>? option)
196203
public TBootstrap DownloadPolicy<T>() where T : Download.Abstractions.IDownloadPolicy, new()
197204
{
198205
_extensions[typeof(Download.Abstractions.IDownloadPolicy)] = typeof(T);
206+
_extensionFactories[typeof(Download.Abstractions.IDownloadPolicy)] = () => new T();
199207
return (TBootstrap)this;
200208
}
201209

@@ -215,6 +223,7 @@ protected T GetOption<T>(Option<T>? option)
215223
public TBootstrap DownloadExecutor<T>() where T : Download.Abstractions.IDownloadExecutor, new()
216224
{
217225
_extensions[typeof(Download.Abstractions.IDownloadExecutor)] = typeof(T);
226+
_extensionFactories[typeof(Download.Abstractions.IDownloadExecutor)] = () => new T();
218227
return (TBootstrap)this;
219228
}
220229

@@ -237,6 +246,7 @@ protected T GetOption<T>(Option<T>? option)
237246
public TBootstrap DownloadSource<T>() where T : Download.Abstractions.IDownloadSource, new()
238247
{
239248
_extensions[typeof(Download.Abstractions.IDownloadSource)] = typeof(T);
249+
_extensionFactories[typeof(Download.Abstractions.IDownloadSource)] = () => new T();
240250
return (TBootstrap)this;
241251
}
242252

@@ -257,6 +267,7 @@ protected T GetOption<T>(Option<T>? option)
257267
public TBootstrap DownloadPipeline<T>() where T : Download.Abstractions.IDownloadPipeline, new()
258268
{
259269
_extensions[typeof(Download.Abstractions.IDownloadPipeline)] = typeof(T);
270+
_extensionFactories[typeof(Download.Abstractions.IDownloadPipeline)] = () => new T();
260271
return (TBootstrap)this;
261272
}
262273

@@ -293,6 +304,7 @@ public TBootstrap HttpAuth(Security.IHttpAuthProvider provider)
293304
public TBootstrap HttpAuth<T>() where T : Security.IHttpAuthProvider, new()
294305
{
295306
_extensions[typeof(Security.IHttpAuthProvider)] = typeof(T);
307+
_extensionFactories[typeof(Security.IHttpAuthProvider)] = () => new T();
296308
return (TBootstrap)this;
297309
}
298310

@@ -325,6 +337,7 @@ public TBootstrap HttpAuth(Security.IHttpAuthProvider provider)
325337
public TBootstrap DownloadOrchestrator<T>() where T : Download.Abstractions.IDownloadOrchestrator, new()
326338
{
327339
_extensions[typeof(Download.Abstractions.IDownloadOrchestrator)] = typeof(T);
340+
_extensionFactories[typeof(Download.Abstractions.IDownloadOrchestrator)] = () => new T();
328341
return (TBootstrap)this;
329342
}
330343

@@ -338,21 +351,20 @@ public TBootstrap HttpAuth(Security.IHttpAuthProvider provider)
338351
/// <remarks>
339352
/// <para><b>Two-phase lookup:</b></para>
340353
/// <para>
341-
/// <b>Phase 1</b> — Looks up the registered implementation <c>Type</c> in the
342-
/// <c>_extensions</c> dictionary by interface type. If found, a new instance is
343-
/// created via <c>Activator.CreateInstance</c>.<br/>
344-
/// <b>Phase 2</b> — If not found in <c>_extensions</c>, looks up an existing
345-
/// singleton instance in the <c>_instances</c> dictionary.
354+
/// <b>Phase 1</b> — Looks up a pre-existing singleton instance in the
355+
/// <c>_instances</c> dictionary (registered via the instance overload
356+
/// such as <c>HttpAuth(provider)</c>). Singleton instances take precedence.<br/>
357+
/// <b>Phase 2</b> — If not found, looks up the registered factory delegate in the
358+
/// <c>_extensionFactories</c> dictionary by interface type. If found, the factory
359+
/// is invoked via <c>new T()</c> constraint, avoiding runtime reflection.
346360
/// </para>
347-
/// <para>Singleton instances in <c>_instances</c> take precedence over lazy
348-
/// registrations in <c>_extensions</c>.</para>
349361
/// </remarks>
350362
protected TExtension? ResolveExtension<TExtension>() where TExtension : class
351363
{
352-
if (_extensions.TryGetValue(typeof(TExtension), out var t))
353-
return Activator.CreateInstance((Type)t) as TExtension;
354364
if (_instances.TryGetValue(typeof(TExtension), out var instance))
355365
return instance as TExtension;
366+
if (_extensionFactories.TryGetValue(typeof(TExtension), out var factory))
367+
return factory() as TExtension;
356368
return null;
357369
}
358370

src/c#/GeneralUpdate.Core/Configuration/UpdateRequestBuilder.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -138,10 +138,7 @@ private static UpdateRequestBuilder LoadFromConfigFile()
138138
}
139139

140140
var json = File.ReadAllText(configPath);
141-
var config = JsonSerializer.Deserialize<UpdateRequest>(json, new JsonSerializerOptions
142-
{
143-
PropertyNameCaseInsensitive = true
144-
});
141+
var config = JsonSerializer.Deserialize(json, JsonContext.UpdateRequestConfigJsonContext.Default.UpdateRequest);
145142

146143
if (config == null)
147144
{

src/c#/GeneralUpdate.Core/Download/Reporting/IUpdateReporter.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Text.Json;
55
using System.Threading;
66
using System.Threading.Tasks;
7+
using GeneralUpdate.Core.JsonContext;
78
using GeneralUpdate.Core.Network;
89

910
namespace GeneralUpdate.Core.Download.Reporting;
@@ -141,7 +142,7 @@ public async Task ReportAsync(UpdateReport report, CancellationToken token = def
141142
if(string.IsNullOrWhiteSpace(_reportUrl))
142143
return;
143144

144-
var json = JsonSerializer.Serialize(report, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
145+
var json = JsonSerializer.Serialize(report, UpdateReportJsonContext.Default.UpdateReport);
145146
using var request = new HttpRequestMessage(HttpMethod.Post, _reportUrl);
146147
request.Content = new StringContent(json, Encoding.UTF8, "application/json");
147148

src/c#/GeneralUpdate.Core/FileSystem/StorageManager.cs

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System;
1+
using System;
22
using System.Collections.Generic;
33
using System.IO;
44
using System.Linq;
@@ -72,7 +72,7 @@ public sealed class StorageManager
7272
/// Example of setting: <c>StorageManager.BlackMatcher = new BlackMatcher(config);</c>
7373
/// </remarks>
7474
public static IBlackMatcher? BlackMatcher { get; set; }
75-
75+
7676
private ComparisonResult ComparisonResult { get; set; }
7777

7878
#region Public Methods
@@ -140,15 +140,18 @@ public ComparisonResult Compare(string leftDir, string rightDir)
140140

141141
/// <summary>
142142
/// Serializes an object to JSON and writes it to the specified path.
143+
/// Uses source generator via <c>JsonTypeInfo</c> to avoid runtime reflection in AOT compilation scenarios.
143144
/// </summary>
144145
/// <typeparam name="T">The type of the object to serialize. Must be a reference type.</typeparam>
145146
/// <param name="targetPath">The full path of the target JSON file.</param>
146147
/// <param name="obj">The object instance to serialize.</param>
147-
/// <param name="typeInfo">Optional JSON type info metadata for source generator serialization support.</param>
148+
/// <param name="typeInfo">JSON type info metadata for source generator serialization support. When <c>null</c>, the reflection-based serializer is used (not AOT-compatible); pass a generated context for Native AOT support.</param>
148149
/// <exception cref="ArgumentException">Thrown when <paramref name="targetPath"/> does not contain a valid directory path.</exception>
149150
/// <remarks>
150151
/// If the directory of the target file does not exist, it will be created automatically.
151-
/// Supports source generator mode via <c>JsonTypeInfo</c>, which avoids runtime reflection in AOT compilation scenarios.
152+
/// When <paramref name="typeInfo"/> is null, this method falls back to the reflection-based serializer
153+
/// which is not compatible with Native AOT. For AOT compilation, always pass a generated
154+
/// <see cref="JsonTypeInfo{T}"/> instance.
152155
/// </remarks>
153156
public static void CreateJson<T>(string targetPath, T obj, JsonTypeInfo<T>? typeInfo = null) where T : class
154157
{
@@ -158,31 +161,40 @@ public static void CreateJson<T>(string targetPath, T obj, JsonTypeInfo<T>? type
158161
if (!Directory.Exists(folderPath))
159162
Directory.CreateDirectory(folderPath);
160163

161-
var jsonString = typeInfo != null ? JsonSerializer.Serialize(obj, typeInfo) : JsonSerializer.Serialize(obj);
164+
#if NET
165+
ArgumentNullException.ThrowIfNull(typeInfo);
166+
#else
167+
if (typeInfo == null) throw new ArgumentNullException(nameof(typeInfo));
168+
#endif
169+
var jsonString = JsonSerializer.Serialize(obj, typeInfo);
162170
File.WriteAllText(targetPath, jsonString);
163171
}
164172

165173
/// <summary>
166174
/// Reads a JSON file from the specified path and deserializes it into the specified type.
175+
/// Uses source generator via <c>JsonTypeInfo</c> to avoid runtime reflection in AOT compilation scenarios.
167176
/// </summary>
168177
/// <typeparam name="T">The target type for deserialization. Must be a reference type.</typeparam>
169178
/// <param name="path">The full path of the JSON file.</param>
170-
/// <param name="typeInfo">Optional JSON type info metadata for source generator deserialization support.</param>
179+
/// <param name="typeInfo">JSON type info metadata for source generator deserialization support. When <c>null</c>, the reflection-based serializer is used (not AOT-compatible); pass a generated context for Native AOT support.</param>
171180
/// <returns>The deserialized object instance; returns <c>default</c> if the file does not exist.</returns>
172181
/// <remarks>
173182
/// If the file does not exist, no exception is thrown and <c>null</c> is returned.
174-
/// Supports source generator mode via <c>JsonTypeInfo</c>.
183+
/// When <paramref name="typeInfo"/> is null, this method falls back to the reflection-based serializer
184+
/// which is not compatible with Native AOT. For AOT compilation, always pass a generated
185+
/// <see cref="JsonTypeInfo{T}"/> instance.
175186
/// </remarks>
176187
public static T? GetJson<T>(string path, JsonTypeInfo<T>? typeInfo = null) where T : class
177188
{
189+
#if NET
190+
ArgumentNullException.ThrowIfNull(typeInfo);
191+
#else
192+
if (typeInfo == null) throw new ArgumentNullException(nameof(typeInfo));
193+
#endif
178194
if (File.Exists(path))
179195
{
180196
var json = File.ReadAllText(path);
181-
if (typeInfo != null)
182-
{
183-
return JsonSerializer.Deserialize(json, typeInfo);
184-
}
185-
return JsonSerializer.Deserialize<T>(json);
197+
return JsonSerializer.Deserialize(json, typeInfo);
186198
}
187199

188200
return default;
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
using System.Text.Json;
2+
using System.Text.Json.Serialization;
3+
using GeneralUpdate.Core.Download.Reporting;
4+
5+
namespace GeneralUpdate.Core.JsonContext;
6+
7+
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
8+
[JsonSerializable(typeof(UpdateReport))]
9+
public partial class UpdateReportJsonContext : JsonSerializerContext;
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using System.Text.Json;
2+
using System.Text.Json.Serialization;
3+
using GeneralUpdate.Core.Configuration;
4+
5+
namespace GeneralUpdate.Core.JsonContext;
6+
7+
/// <summary>
8+
/// Source-generated JSON serialization context for <see cref="UpdateRequest"/> with
9+
/// case-insensitive property matching. Used by <see cref="UpdateRequestBuilder.LoadFromConfigFile"/>
10+
/// to support <c>update_config.json</c> files where property casing may differ from the C# model.
11+
/// Full Native AOT compatible — case-insensitivity is resolved at compile time.
12+
/// </summary>
13+
[JsonSourceGenerationOptions(PropertyNameCaseInsensitive = true)]
14+
[JsonSerializable(typeof(UpdateRequest))]
15+
internal partial class UpdateRequestConfigJsonContext : JsonSerializerContext;

src/c#/GeneralUpdate.Core/Network/VersionService.cs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -285,11 +285,11 @@ private async Task<VersionRespDTO> ValidateAsync(string url, string v, int at, s
285285
/// <typeparam name="T">The deserialization target type for the response data.</typeparam>
286286
/// <param name="url">The target URL for the request.</param>
287287
/// <param name="p">The POST body parameter dictionary.</param>
288-
/// <param name="ti">The JSON type info metadata for source generator (may be null, in which case reflection-based deserialization is used).</param>
288+
/// <param name="ti">The JSON type info metadata for source generator. Required for Native AOT compatibility.</param>
289289
/// <param name="t">A <see cref="CancellationToken"/> for cancelling the operation.</param>
290290
/// <returns>The deserialized response data.</returns>
291291
private async Task<T> PostAsync<T>(string url, Dictionary<string, object> p,
292-
JsonTypeInfo<T>? ti, CancellationToken t)
292+
JsonTypeInfo<T> ti, CancellationToken t)
293293
{
294294
for (int attempt = 0; ; attempt++)
295295
{
@@ -339,11 +339,11 @@ private async Task<T> PostAsync<T>(string url, Dictionary<string, object> p,
339339
/// <typeparam name="T">The deserialization target type for the response data.</typeparam>
340340
/// <param name="url">The target URL for the request.</param>
341341
/// <param name="p">The POST body parameter dictionary.</param>
342-
/// <param name="ti">The JSON type info metadata for source generator (may be null).</param>
342+
/// <param name="ti">The JSON type info metadata for source generator. Required for Native AOT compatibility.</param>
343343
/// <param name="t">A <see cref="CancellationToken"/> for cancelling the operation.</param>
344344
/// <returns>The deserialized response data.</returns>
345345
private async Task<T> SendAsync<T>(string url, Dictionary<string, object> p,
346-
JsonTypeInfo<T>? ti, CancellationToken t)
346+
JsonTypeInfo<T> ti, CancellationToken t)
347347
{
348348
using var req = new HttpRequestMessage(HttpMethod.Post, new Uri(url));
349349
req.Headers.Accept.ParseAdd("application/json");
@@ -356,7 +356,11 @@ private async Task<T> SendAsync<T>(string url, Dictionary<string, object> p,
356356
var r = await _sharedClient.SendAsync(req, cts.Token).ConfigureAwait(false);
357357
r.EnsureSuccessStatusCode();
358358
var rj = await r.Content.ReadAsStringAsync().ConfigureAwait(false);
359-
return ti == null ? JsonSerializer.Deserialize<T>(rj) : JsonSerializer.Deserialize(rj, ti);
359+
var result = JsonSerializer.Deserialize(rj, ti);
360+
if (result is null)
361+
throw new InvalidOperationException(
362+
$"Server returned JSON 'null' for type '{typeof(T).Name}' at URL '{url}'.");
363+
return result;
360364
}
361365

362366
/// <summary>

0 commit comments

Comments
 (0)