Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 23 additions & 11 deletions src/c#/GeneralUpdate.Core/Configuration/AbstractBootstrap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,12 @@ public abstract class AbstractBootstrap<TBootstrap, TStrategy>
{
private readonly ConcurrentDictionary<Option, OptionValue> _options;

/// <summary>User-registered extension type mappings (interface type β†’ implementation type), used for lazy instantiation.</summary>
/// <summary>User-registered extension type mappings (interface type β†’ implementation type), used for type metadata queries.</summary>
private readonly Dictionary<Type, Type> _extensions = new();

/// <summary>User-registered extension factory delegates (interface type β†’ factory function), used for lazy instantiation.</summary>
private readonly Dictionary<Type, Func<object>> _extensionFactories = new();

/// <summary>Registered singleton instances (e.g., <c>BlackPolicy</c>).</summary>
private readonly Dictionary<Type, object> _instances = new();

Expand Down Expand Up @@ -95,7 +98,7 @@ protected T GetOption<T>(Option<T>? option)
}

// ═══════════ Extension point registration ═══════════

/// <summary>
/// Registers an update status reporter for reporting update progress and results
/// to a server (e.g., GeneralSpacestation).
Expand All @@ -115,6 +118,7 @@ protected T GetOption<T>(Option<T>? option)
public TBootstrap UpdateReporter<T>() where T : Download.Reporting.IUpdateReporter, new()
{
_extensions[typeof(Download.Reporting.IUpdateReporter)] = typeof(T);
_extensionFactories[typeof(Download.Reporting.IUpdateReporter)] = () => new T();
return (TBootstrap)this;
}

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

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

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

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

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

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

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

Expand Down Expand Up @@ -293,6 +304,7 @@ public TBootstrap HttpAuth(Security.IHttpAuthProvider provider)
public TBootstrap HttpAuth<T>() where T : Security.IHttpAuthProvider, new()
{
_extensions[typeof(Security.IHttpAuthProvider)] = typeof(T);
_extensionFactories[typeof(Security.IHttpAuthProvider)] = () => new T();
return (TBootstrap)this;
}

Expand Down Expand Up @@ -325,6 +337,7 @@ public TBootstrap HttpAuth(Security.IHttpAuthProvider provider)
public TBootstrap DownloadOrchestrator<T>() where T : Download.Abstractions.IDownloadOrchestrator, new()
{
_extensions[typeof(Download.Abstractions.IDownloadOrchestrator)] = typeof(T);
_extensionFactories[typeof(Download.Abstractions.IDownloadOrchestrator)] = () => new T();
return (TBootstrap)this;
}

Expand All @@ -338,21 +351,20 @@ public TBootstrap HttpAuth(Security.IHttpAuthProvider provider)
/// <remarks>
/// <para><b>Two-phase lookup:</b></para>
/// <para>
/// <b>Phase 1</b> β€” Looks up the registered implementation <c>Type</c> in the
/// <c>_extensions</c> dictionary by interface type. If found, a new instance is
/// created via <c>Activator.CreateInstance</c>.<br/>
/// <b>Phase 2</b> β€” If not found in <c>_extensions</c>, looks up an existing
/// singleton instance in the <c>_instances</c> dictionary.
/// <b>Phase 1</b> β€” Looks up a pre-existing singleton instance in the
/// <c>_instances</c> dictionary (registered via the instance overload
/// such as <c>HttpAuth(provider)</c>). Singleton instances take precedence.<br/>
/// <b>Phase 2</b> β€” If not found, looks up the registered factory delegate in the
/// <c>_extensionFactories</c> dictionary by interface type. If found, the factory
/// is invoked via <c>new T()</c> constraint, avoiding runtime reflection.
/// </para>
/// <para>Singleton instances in <c>_instances</c> take precedence over lazy
/// registrations in <c>_extensions</c>.</para>
/// </remarks>
protected TExtension? ResolveExtension<TExtension>() where TExtension : class
{
if (_extensions.TryGetValue(typeof(TExtension), out var t))
return Activator.CreateInstance((Type)t) as TExtension;
if (_instances.TryGetValue(typeof(TExtension), out var instance))
return instance as TExtension;
Comment on lines 362 to 365
if (_extensionFactories.TryGetValue(typeof(TExtension), out var factory))
return factory() as TExtension;
return null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,10 +138,7 @@ private static UpdateRequestBuilder LoadFromConfigFile()
}

var json = File.ReadAllText(configPath);
var config = JsonSerializer.Deserialize<UpdateRequest>(json, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
var config = JsonSerializer.Deserialize(json, JsonContext.UpdateRequestConfigJsonContext.Default.UpdateRequest);

Comment on lines 138 to 142
if (config == null)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using GeneralUpdate.Core.JsonContext;
using GeneralUpdate.Core.Network;

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

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

Expand Down
36 changes: 24 additions & 12 deletions src/c#/GeneralUpdate.Core/FileSystem/StorageManager.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
ο»Ώusing System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
Expand Down Expand Up @@ -72,7 +72,7 @@ public sealed class StorageManager
/// Example of setting: <c>StorageManager.BlackMatcher = new BlackMatcher(config);</c>
/// </remarks>
public static IBlackMatcher? BlackMatcher { get; set; }

private ComparisonResult ComparisonResult { get; set; }

#region Public Methods
Expand Down Expand Up @@ -140,15 +140,18 @@ public ComparisonResult Compare(string leftDir, string rightDir)

/// <summary>
/// Serializes an object to JSON and writes it to the specified path.
/// Uses source generator via <c>JsonTypeInfo</c> to avoid runtime reflection in AOT compilation scenarios.
/// </summary>
/// <typeparam name="T">The type of the object to serialize. Must be a reference type.</typeparam>
/// <param name="targetPath">The full path of the target JSON file.</param>
/// <param name="obj">The object instance to serialize.</param>
/// <param name="typeInfo">Optional JSON type info metadata for source generator serialization support.</param>
/// <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>
/// <exception cref="ArgumentException">Thrown when <paramref name="targetPath"/> does not contain a valid directory path.</exception>
/// <remarks>
/// If the directory of the target file does not exist, it will be created automatically.
/// Supports source generator mode via <c>JsonTypeInfo</c>, which avoids runtime reflection in AOT compilation scenarios.
/// When <paramref name="typeInfo"/> is null, this method falls back to the reflection-based serializer
/// which is not compatible with Native AOT. For AOT compilation, always pass a generated
/// <see cref="JsonTypeInfo{T}"/> instance.
/// </remarks>
public static void CreateJson<T>(string targetPath, T obj, JsonTypeInfo<T>? typeInfo = null) where T : class
{
Expand All @@ -158,31 +161,40 @@ public static void CreateJson<T>(string targetPath, T obj, JsonTypeInfo<T>? type
if (!Directory.Exists(folderPath))
Directory.CreateDirectory(folderPath);

var jsonString = typeInfo != null ? JsonSerializer.Serialize(obj, typeInfo) : JsonSerializer.Serialize(obj);
#if NET
ArgumentNullException.ThrowIfNull(typeInfo);
#else
if (typeInfo == null) throw new ArgumentNullException(nameof(typeInfo));
#endif
var jsonString = JsonSerializer.Serialize(obj, typeInfo);
File.WriteAllText(targetPath, jsonString);
}

/// <summary>
/// Reads a JSON file from the specified path and deserializes it into the specified type.
/// Uses source generator via <c>JsonTypeInfo</c> to avoid runtime reflection in AOT compilation scenarios.
/// </summary>
/// <typeparam name="T">The target type for deserialization. Must be a reference type.</typeparam>
/// <param name="path">The full path of the JSON file.</param>
/// <param name="typeInfo">Optional JSON type info metadata for source generator deserialization support.</param>
/// <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>
/// <returns>The deserialized object instance; returns <c>default</c> if the file does not exist.</returns>
/// <remarks>
/// If the file does not exist, no exception is thrown and <c>null</c> is returned.
/// Supports source generator mode via <c>JsonTypeInfo</c>.
/// When <paramref name="typeInfo"/> is null, this method falls back to the reflection-based serializer
/// which is not compatible with Native AOT. For AOT compilation, always pass a generated
/// <see cref="JsonTypeInfo{T}"/> instance.
/// </remarks>
public static T? GetJson<T>(string path, JsonTypeInfo<T>? typeInfo = null) where T : class
{
#if NET
ArgumentNullException.ThrowIfNull(typeInfo);
#else
if (typeInfo == null) throw new ArgumentNullException(nameof(typeInfo));
#endif
if (File.Exists(path))
{
var json = File.ReadAllText(path);
if (typeInfo != null)
{
return JsonSerializer.Deserialize(json, typeInfo);
}
return JsonSerializer.Deserialize<T>(json);
return JsonSerializer.Deserialize(json, typeInfo);
}
Comment on lines 178 to 198

return default;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using GeneralUpdate.Core.Download.Reporting;

namespace GeneralUpdate.Core.JsonContext;

[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
[JsonSerializable(typeof(UpdateReport))]
public partial class UpdateReportJsonContext : JsonSerializerContext;
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using GeneralUpdate.Core.Configuration;

namespace GeneralUpdate.Core.JsonContext;

/// <summary>
/// Source-generated JSON serialization context for <see cref="UpdateRequest"/> with
/// case-insensitive property matching. Used by <see cref="UpdateRequestBuilder.LoadFromConfigFile"/>
/// to support <c>update_config.json</c> files where property casing may differ from the C# model.
/// Full Native AOT compatible β€” case-insensitivity is resolved at compile time.
/// </summary>
[JsonSourceGenerationOptions(PropertyNameCaseInsensitive = true)]
[JsonSerializable(typeof(UpdateRequest))]
internal partial class UpdateRequestConfigJsonContext : JsonSerializerContext;
14 changes: 9 additions & 5 deletions src/c#/GeneralUpdate.Core/Network/VersionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -285,11 +285,11 @@ private async Task<VersionRespDTO> ValidateAsync(string url, string v, int at, s
/// <typeparam name="T">The deserialization target type for the response data.</typeparam>
/// <param name="url">The target URL for the request.</param>
/// <param name="p">The POST body parameter dictionary.</param>
/// <param name="ti">The JSON type info metadata for source generator (may be null, in which case reflection-based deserialization is used).</param>
/// <param name="ti">The JSON type info metadata for source generator. Required for Native AOT compatibility.</param>
/// <param name="t">A <see cref="CancellationToken"/> for cancelling the operation.</param>
/// <returns>The deserialized response data.</returns>
private async Task<T> PostAsync<T>(string url, Dictionary<string, object> p,
JsonTypeInfo<T>? ti, CancellationToken t)
JsonTypeInfo<T> ti, CancellationToken t)
{
for (int attempt = 0; ; attempt++)
{
Expand Down Expand Up @@ -339,11 +339,11 @@ private async Task<T> PostAsync<T>(string url, Dictionary<string, object> p,
/// <typeparam name="T">The deserialization target type for the response data.</typeparam>
/// <param name="url">The target URL for the request.</param>
/// <param name="p">The POST body parameter dictionary.</param>
/// <param name="ti">The JSON type info metadata for source generator (may be null).</param>
/// <param name="ti">The JSON type info metadata for source generator. Required for Native AOT compatibility.</param>
/// <param name="t">A <see cref="CancellationToken"/> for cancelling the operation.</param>
/// <returns>The deserialized response data.</returns>
private async Task<T> SendAsync<T>(string url, Dictionary<string, object> p,
JsonTypeInfo<T>? ti, CancellationToken t)
JsonTypeInfo<T> ti, CancellationToken t)
{
using var req = new HttpRequestMessage(HttpMethod.Post, new Uri(url));
req.Headers.Accept.ParseAdd("application/json");
Expand All @@ -356,7 +356,11 @@ private async Task<T> SendAsync<T>(string url, Dictionary<string, object> p,
var r = await _sharedClient.SendAsync(req, cts.Token).ConfigureAwait(false);
r.EnsureSuccessStatusCode();
var rj = await r.Content.ReadAsStringAsync().ConfigureAwait(false);
return ti == null ? JsonSerializer.Deserialize<T>(rj) : JsonSerializer.Deserialize(rj, ti);
var result = JsonSerializer.Deserialize(rj, ti);
if (result is null)
throw new InvalidOperationException(
$"Server returned JSON 'null' for type '{typeof(T).Name}' at URL '{url}'.");
return result;
}
Comment on lines 356 to 364

/// <summary>
Expand Down
Loading