Skip to content

Commit ea99326

Browse files
committed
fix: make ReportUrl optional and fix IUpdateReporter consumption
- HttpUpdateReporter: add parameterless constructor to satisfy new() constraint, add ReportUrl/Client properties for post-construction configuration - GeneralUpdateBootstrap.LaunchWithStrategy: resolve IUpdateReporter and inject into roleStrategy. When ReportUrl is null/empty, force NoOpUpdateReporter - ClientStrategy/UpdateStrategy.Create: propagate Reporter to OS strategy - AbstractStrategy.ExecuteAsync: use Reporter.ReportAsync() instead of VersionService.Report(), unifying the reporting path - VersionService: remove dead Report() and ReportAsync() methods - ProcessInfo: make ReportUrl optional (remove ArgumentNullException)
1 parent 07b2cfb commit ea99326

7 files changed

Lines changed: 79 additions & 80 deletions

File tree

src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,20 @@ private async Task<GeneralUpdateBootstrap> LaunchWithStrategy(IStrategy roleStra
139139
var hooks = ResolveExtension<Hooks.IUpdateHooks>() ?? new Hooks.NoOpUpdateHooks();
140140
roleStrategy.Hooks = hooks;
141141

142+
// Resolve reporter from extensions. If ReportUrl is not configured,
143+
// force NoOpUpdateReporter regardless of what the user registered.
144+
Download.Reporting.IUpdateReporter reporter;
145+
if (string.IsNullOrWhiteSpace(_configInfo.ReportUrl))
146+
{
147+
reporter = new Download.Reporting.NoOpUpdateReporter();
148+
}
149+
else
150+
{
151+
reporter = ResolveExtension<Download.Reporting.IUpdateReporter>() ??
152+
new Download.Reporting.NoOpUpdateReporter();
153+
}
154+
roleStrategy.Reporter = reporter;
155+
142156
// ── Download components ──
143157
var downloadOrchestrator = ResolveExtension<Download.Abstractions.IDownloadOrchestrator>();
144158
var downloadPolicy = ResolveExtension<Download.Abstractions.IDownloadPolicy>();

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

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,21 @@ public enum UpdateStatus { Updating = 1, Success = 2, Failure = 3 }
6464
/// </remarks>
6565
public record UpdateReport(int RecordId, int Status = 1, int Type = 1);
6666

67+
/// <summary>
68+
/// A no-op (null object) update status reporter that performs no actual work.
69+
/// Used when no ReportUrl is configured.
70+
/// </summary>
71+
/// <remarks>
72+
/// <para>
73+
/// This class implements the Null Object Pattern, eliminating the need for null checks
74+
/// in consumer code. Every report operation returns a completed task immediately without
75+
/// performing any actual data transmission.
76+
/// </para>
77+
/// <para>
78+
/// Use this implementation when no remote status reporting is needed,
79+
/// such as during local testing or when the report endpoint is not configured.
80+
/// </para>
81+
/// </remarks>
6782
/// <summary>
6883
/// An HTTP POST-based update status reporter that serializes <see cref="UpdateReport"/> to JSON
6984
/// and sends it to a configured remote endpoint. Compatible with the GeneralSpacestation ReportDTO format.
@@ -86,26 +101,59 @@ public record UpdateReport(int RecordId, int Status = 1, int Type = 1);
86101
/// </remarks>
87102
public class HttpUpdateReporter : IUpdateReporter
88103
{
89-
private readonly HttpClient _client;
90-
private readonly string _reportUrl;
104+
private HttpClient _client;
105+
private string _reportUrl;
91106

92107
/// <summary>
93-
/// Initializes a new instance of the <see cref="HttpUpdateReporter"/> class.
108+
/// Gets or sets the report URL for update status reporting.
109+
/// When null or empty, <see cref="ReportAsync"/> is a no-op.
110+
/// </summary>
111+
public string ReportUrl
112+
{
113+
get => _reportUrl;
114+
set => _reportUrl = value ?? string.Empty;
115+
}
116+
117+
/// <summary>
118+
/// Gets or sets the <see cref="HttpClient"/> used for HTTP requests.
119+
/// If not set, a default instance is created in the parameterless constructor.
120+
/// </summary>
121+
public HttpClient Client
122+
{
123+
get => _client;
124+
set => _client = value ?? throw new ArgumentNullException(nameof(value));
125+
}
126+
127+
/// <summary>
128+
/// Parameterless constructor required by the extension resolution mechanism.
129+
/// Uses a default HttpClient and empty ReportUrl (no-op until configured).
130+
/// </summary>
131+
public HttpUpdateReporter()
132+
{
133+
_client = new HttpClient();
134+
_reportUrl = string.Empty;
135+
}
136+
137+
/// <summary>
138+
/// Initializes a new instance of the <see cref="HttpUpdateReporter"/> class
139+
/// with a specific client and report URL.
94140
/// </summary>
95141
/// <param name="client">The <see cref="HttpClient"/> instance used to send HTTP requests.</param>
96142
/// <param name="reportUrl">The remote URL that receives the update status reports.</param>
97143
public HttpUpdateReporter(HttpClient client, string reportUrl)
98144
{
99-
_client = client;
100-
_reportUrl = reportUrl;
145+
_client = client ?? throw new ArgumentNullException(nameof(client));
146+
_reportUrl = reportUrl ?? string.Empty;
101147
}
102148

103149
public async Task ReportAsync(UpdateReport report, CancellationToken token = default)
104150
{
105151
try
106152
{
153+
if(string.IsNullOrWhiteSpace(_reportUrl))
154+
return;
155+
107156
var json = JsonSerializer.Serialize(report, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
108-
109157
using var request = new HttpRequestMessage(HttpMethod.Post, _reportUrl);
110158
request.Content = new StringContent(json, Encoding.UTF8, "application/json");
111159

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

Lines changed: 3 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ namespace GeneralUpdate.Core.Network
5757
/// <list type="bullet">
5858
/// <item><description>At startup, call <see cref="Validate(string, string, AppType, string, PlatformType, string, string, string, CancellationToken)"/>
5959
/// to check whether the server has a new version.</description></item>
60-
/// <item><description>After download completes, call <see cref="Report(string, int, int, int?, string, string, CancellationToken)"/>
60+
/// <item><description>After download completes, use <see cref="Download.Reporting.IUpdateReporter.ReportAsync"/>
6161
/// to report the update status.</description></item>
6262
/// </list>
6363
/// </para>
@@ -146,43 +146,6 @@ public VersionService(IHttpAuthProvider? auth = null, TimeSpan? timeout = null,
146146
_maxRetries = maxRetries;
147147
}
148148

149-
/// <summary>
150-
/// Reports the update status to the server for a specified record.
151-
/// </summary>
152-
/// <remarks>
153-
/// <para>
154-
/// This is a backward-compatible static convenience method that internally creates
155-
/// a <see cref="VersionService"/> instance and calls <see cref="ReportAsync"/>.
156-
/// </para>
157-
/// <para>
158-
/// Execution flow:
159-
/// <list type="number">
160-
/// <item><description>Resolves the authentication provider: uses the global provider
161-
/// (<see cref="SetDefaultAuthProvider"/>) first; otherwise creates one via
162-
/// <see cref="HttpAuthProviderFactory.Create"/>.</description></item>
163-
/// <item><description>Creates a temporary <see cref="VersionService"/> instance.</description></item>
164-
/// <item><description>Calls <see cref="ReportAsync"/> to perform the report.</description></item>
165-
/// </list>
166-
/// </para>
167-
/// </remarks>
168-
/// <param name="url">The server API URL.</param>
169-
/// <param name="recordId">The update record identifier.</param>
170-
/// <param name="status">The current status code.</param>
171-
/// <param name="type">The update type (may be null).</param>
172-
/// <param name="scheme">The authentication scheme (e.g., "bearer", "apikey", "hmac"), used to create the auth provider. Ignored when a global auth provider is set.</param>
173-
/// <param name="token">The authentication token or key, used together with <paramref name="scheme"/>.</param>
174-
/// <param name="ct">A <see cref="CancellationToken"/> for cancelling the operation.</param>
175-
/// <returns>A task representing the asynchronous operation.</returns>
176-
public static Task Report(string url, int recordId, int status, int? type,
177-
string scheme = null, string token = null, CancellationToken ct = default)
178-
{
179-
if (string.IsNullOrWhiteSpace(url))
180-
return Task.CompletedTask;
181-
182-
var a = _globalAuthProvider ?? HttpAuthProviderFactory.Create(scheme, token, null);
183-
return new VersionService(a).ReportAsync(url, recordId, status, type, ct);
184-
}
185-
186149
/// <summary>
187150
/// Validates the current version against the server to check for available updates.
188151
/// </summary>
@@ -219,8 +182,8 @@ public static Task<VersionRespDTO> Validate(string url, string version,
219182
AppType appType, string appKey, PlatformType platform, string productId,
220183
string scheme = null, string token = null, CancellationToken ct = default)
221184
{
222-
var a = _globalAuthProvider ?? HttpAuthProviderFactory.Create(scheme, token, appKey);
223-
return new VersionService(a).ValidateAsync(url, version, (int)appType, appKey, (int)platform, productId, ct);
185+
var auth = _globalAuthProvider ?? HttpAuthProviderFactory.Create(scheme, token, appKey);
186+
return new VersionService(auth).ValidateAsync(url, version, (int)appType, appKey, (int)platform, productId, ct);
224187
}
225188

226189
/// <summary>
@@ -249,31 +212,6 @@ public static Task<VersionRespDTO> Validate(string url, string version,
249212
string scheme = null, string token = null, CancellationToken ct = default)
250213
=> Validate(url, version, (AppType)appType, appKey, (PlatformType)platform, productId, scheme, token, ct);
251214

252-
/// <summary>
253-
/// Asynchronously reports the update record status to the server.
254-
/// </summary>
255-
/// <remarks>
256-
/// <para>
257-
/// Execution flow:
258-
/// <list type="number">
259-
/// <item><description>Constructs a parameter dictionary with the record ID, status, and type.</description></item>
260-
/// <item><description>Sends the parameters via a POST request using <see cref="PostAsync{T}"/>.</description></item>
261-
/// <item><description>Deserializes the response into a <see cref="BaseResponseDTO{T}"/> of type <see cref="bool"/>.</description></item>
262-
/// </list>
263-
/// </para>
264-
/// </remarks>
265-
/// <param name="url">The server API URL.</param>
266-
/// <param name="recordId">The update record identifier.</param>
267-
/// <param name="status">The current status code.</param>
268-
/// <param name="type">The update type (may be null).</param>
269-
/// <param name="t">A <see cref="CancellationToken"/> for cancelling the operation.</param>
270-
/// <returns>A task representing the asynchronous operation.</returns>
271-
private async Task ReportAsync(string url, int recordId, int status, int? type, CancellationToken t = default)
272-
{
273-
var p = new Dictionary<string, object> { ["recordId"] = recordId, ["status"] = status, ["type"] = type };
274-
await PostAsync<BaseResponseDTO<bool>>(url, p, ReportRespJsonContext.Default.BaseResponseDTOBoolean, t);
275-
}
276-
277215
/// <summary>
278216
/// Asynchronously validates the version by querying the server for available updates.
279217
/// </summary>

src/c#/GeneralUpdate.Core/Strategy/AbstractStrategy.cs

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@
66
using GeneralUpdate.Core.Event;
77
using GeneralUpdate.Core.Pipeline;
88
using GeneralUpdate.Core.Configuration;
9-
using GeneralUpdate.Core.Network;
9+
1010
using GeneralUpdate.Core.Hooks;
1111
using IUpdateReporter = GeneralUpdate.Core.Download.Reporting.IUpdateReporter;
12+
using UpdateReport = GeneralUpdate.Core.Download.Reporting.UpdateReport;
1213

1314
namespace GeneralUpdate.Core.Strategy
1415
{
@@ -30,7 +31,7 @@ namespace GeneralUpdate.Core.Strategy
3031
/// <item><description>Calls <see cref="BuildPipeline"/> (abstract method, implemented by subclasses) to obtain the middleware builder.</description></item>
3132
/// <item><description>Executes <c>PipelineBuilder.Build()</c> to run the registered middleware in FIFO order:
3233
/// <c>Hash</c> (integrity verification) → <c>Decompress</c> (extract update package) → <c>Patch</c> (apply incremental patches).</description></item>
33-
/// <item><description>Reports the update result for the current version to the server via <see cref="VersionService.Report"/>.</description></item>
34+
/// <item><description>Reports the update result for the current version via <see cref="Reporter"/>.</description></item>
3435
/// <item><description>Deletes the processed archive file.</description></item>
3536
/// </list>
3637
/// </para>
@@ -58,7 +59,7 @@ public abstract class AbstractStrategy : IStrategy
5859
/// <summary>
5960
/// Gets or sets the update status reporter. Responsible for reporting the processing progress and final result of each version to the server.
6061
/// </summary>
61-
public IUpdateReporter Reporter { get; set; } = new Download.Reporting.NoOpUpdateReporter();
62+
public IUpdateReporter Reporter { get; set; } = new Download.Reporting.HttpUpdateReporter();
6263

6364
/// <summary>
6465
/// Gets or sets the differential patch pipeline. Supports parallel application of incremental patches and progress reporting.
@@ -161,12 +162,7 @@ public virtual async Task ExecuteAsync()
161162
}
162163
finally
163164
{
164-
await VersionService.Report(_configinfo.ReportUrl
165-
, version.RecordId
166-
, status
167-
, version.AppType
168-
, _configinfo.Scheme
169-
, _configinfo.Token);
165+
await Reporter.ReportAsync(new UpdateReport(version.RecordId, status, version.AppType ?? 1));
170166

171167
// Delete only this version's zip file — other AppType packages
172168
// in TempPath may still be needed by a downstream process.

src/c#/GeneralUpdate.Core/Strategy/ClientStrategy.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ public void Create(GlobalConfigInfo parameter)
224224
if (_osStrategy is AbstractStrategy abs)
225225
{
226226
if (_pendingDiffPipeline != null) abs.DiffPipeline = _pendingDiffPipeline;
227+
abs.Reporter = this.Reporter;
227228
}
228229
}
229230

src/c#/GeneralUpdate.Core/Strategy/UpdateStrategy.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ public void Create(GlobalConfigInfo parameter)
8484
if (_osStrategy is AbstractStrategy abs)
8585
{
8686
if (_pendingDiffPipeline != null) abs.DiffPipeline = _pendingDiffPipeline;
87+
abs.Reporter = this.Reporter;
8788
}
8889
}
8990

tests/ClientTest/Program.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using GeneralUpdate.Core;
22
using GeneralUpdate.Core.Configuration;
33
using GeneralUpdate.Core.Download;
4+
using GeneralUpdate.Core.Download.Reporting;
45
using GeneralUpdate.Core.Event;
56
using GeneralUpdate.Core.Hooks;
67

0 commit comments

Comments
 (0)