Skip to content

Commit 3ded370

Browse files
committed
fix: resolve Timer.Start dueTime bugs and v3.0 hardening
Fix Timer.Start(TimeSpan) using .Milliseconds (component only) instead of .TotalMilliseconds, and both overloads not storing the dueTime parameter to the DueTime property. The int overload also used the stale DueTime value in the else branch instead of the parameter. Add Timer tests covering all Start overloads, stop, and dispose. Add test projects for MADE.Foundation and MADE.Runtime. Fix nullable annotations in MADE.Networking and add async safety to AppDiagnostics event handlers. Remove x64/x86 platform configurations from solution.
1 parent 5488d1f commit 3ded370

27 files changed

Lines changed: 957 additions & 82 deletions

MADE.NET.sln

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
1+
22
Microsoft Visual Studio Solution File, Format Version 12.00
33
# Visual Studio Version 17
44
VisualStudioVersion = 17.1.32228.430
@@ -59,6 +59,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MADE.Testing.Tests", "tests
5959
EndProject
6060
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MADE.Web.Mvc.Tests", "tests\MADE.Web.Mvc.Tests\MADE.Web.Mvc.Tests.csproj", "{F994F941-474A-4FDD-A9CB-280EB0D78407}"
6161
EndProject
62+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MADE.Foundation.Tests", "tests\MADE.Foundation.Tests\MADE.Foundation.Tests.csproj", "{EE7B8716-5B54-45EC-91AD-4764F4AC863B}"
63+
EndProject
64+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MADE.Runtime.Tests", "tests\MADE.Runtime.Tests\MADE.Runtime.Tests.csproj", "{D360BA13-4DAD-4C62-AE4A-737553F34E01}"
65+
EndProject
6266
Global
6367
GlobalSection(SolutionConfigurationPlatforms) = preSolution
6468
Debug|Any CPU = Debug|Any CPU
@@ -169,6 +173,14 @@ Global
169173
{F994F941-474A-4FDD-A9CB-280EB0D78407}.Debug|Any CPU.Build.0 = Debug|Any CPU
170174
{F994F941-474A-4FDD-A9CB-280EB0D78407}.Release|Any CPU.ActiveCfg = Release|Any CPU
171175
{F994F941-474A-4FDD-A9CB-280EB0D78407}.Release|Any CPU.Build.0 = Release|Any CPU
176+
{EE7B8716-5B54-45EC-91AD-4764F4AC863B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
177+
{EE7B8716-5B54-45EC-91AD-4764F4AC863B}.Debug|Any CPU.Build.0 = Debug|Any CPU
178+
{EE7B8716-5B54-45EC-91AD-4764F4AC863B}.Release|Any CPU.ActiveCfg = Release|Any CPU
179+
{EE7B8716-5B54-45EC-91AD-4764F4AC863B}.Release|Any CPU.Build.0 = Release|Any CPU
180+
{D360BA13-4DAD-4C62-AE4A-737553F34E01}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
181+
{D360BA13-4DAD-4C62-AE4A-737553F34E01}.Debug|Any CPU.Build.0 = Debug|Any CPU
182+
{D360BA13-4DAD-4C62-AE4A-737553F34E01}.Release|Any CPU.ActiveCfg = Release|Any CPU
183+
{D360BA13-4DAD-4C62-AE4A-737553F34E01}.Release|Any CPU.Build.0 = Release|Any CPU
172184
EndGlobalSection
173185
GlobalSection(SolutionProperties) = preSolution
174186
HideSolutionNode = FALSE
@@ -200,6 +212,8 @@ Global
200212
{865FBD49-C64B-4B36-AEFC-FD960DDC4CF8} = {69149D0F-BB09-411B-88F0-A1E845058D70}
201213
{40B5F4EB-45DD-410A-B0FB-2384C863FC33} = {69149D0F-BB09-411B-88F0-A1E845058D70}
202214
{F994F941-474A-4FDD-A9CB-280EB0D78407} = {69149D0F-BB09-411B-88F0-A1E845058D70}
215+
{EE7B8716-5B54-45EC-91AD-4764F4AC863B} = {69149D0F-BB09-411B-88F0-A1E845058D70}
216+
{D360BA13-4DAD-4C62-AE4A-737553F34E01} = {69149D0F-BB09-411B-88F0-A1E845058D70}
203217
EndGlobalSection
204218
GlobalSection(ExtensibilityGlobals) = postSolution
205219
SolutionGuid = {3921AD86-E6C0-4436-8880-2D9EDFAD6151}

src/MADE.Diagnostics/AppDiagnostics.cs

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -78,38 +78,52 @@ public void StopRecordingDiagnostics()
7878

7979
private async void OnTaskUnobservedException(object? sender, UnobservedTaskExceptionEventArgs args)
8080
{
81-
args.SetObserved();
82-
83-
var correlationId = Guid.NewGuid();
81+
try
82+
{
83+
args.SetObserved();
8484

85-
await this.EventLogger.WriteCritical(
86-
args.Exception != null
87-
? $"An unobserved task exception was thrown. Correlation ID: {correlationId}. Error: {args.Exception}."
88-
: $"An unobserved task exception was thrown. Correlation ID: {correlationId}. Error: No exception information was available.").ConfigureAwait(false);
85+
var correlationId = Guid.NewGuid();
8986

90-
if (args.Exception != null)
87+
await this.EventLogger.WriteCritical(
88+
args.Exception != null
89+
? $"An unobserved task exception was thrown. Correlation ID: {correlationId}. Error: {args.Exception}."
90+
: $"An unobserved task exception was thrown. Correlation ID: {correlationId}. Error: No exception information was available.").ConfigureAwait(false);
91+
92+
if (args.Exception != null)
93+
{
94+
this.ExceptionObserved?.Invoke(this, new ExceptionObservedEventArgs(correlationId, args.Exception));
95+
}
96+
}
97+
catch
9198
{
92-
this.ExceptionObserved?.Invoke(this, new ExceptionObservedEventArgs(correlationId, args.Exception));
99+
// Swallow exceptions in last-resort exception handlers to prevent crashing the process.
93100
}
94101
}
95102

96103
private async void OnAppUnhandledException(object sender, UnhandledExceptionEventArgs args)
97104
{
98-
if (args.IsTerminating)
105+
try
99106
{
100-
await this.EventLogger.WriteCritical(
101-
"The application is terminating due to an unhandled exception being thrown.").ConfigureAwait(false);
102-
}
107+
if (args.IsTerminating)
108+
{
109+
await this.EventLogger.WriteCritical(
110+
"The application is terminating due to an unhandled exception being thrown.").ConfigureAwait(false);
111+
}
103112

104-
if (args.ExceptionObject is not Exception ex)
105-
{
106-
return;
107-
}
113+
if (args.ExceptionObject is not Exception ex)
114+
{
115+
return;
116+
}
108117

109-
var correlationId = Guid.NewGuid();
118+
var correlationId = Guid.NewGuid();
110119

111-
await this.EventLogger.WriteCritical($"An unhandled exception was thrown. Correlation ID: {correlationId}. Error: {ex}").ConfigureAwait(false);
120+
await this.EventLogger.WriteCritical($"An unhandled exception was thrown. Correlation ID: {correlationId}. Error: {ex}").ConfigureAwait(false);
112121

113-
this.ExceptionObserved?.Invoke(this, new ExceptionObservedEventArgs(correlationId, ex));
122+
this.ExceptionObserved?.Invoke(this, new ExceptionObservedEventArgs(correlationId, ex));
123+
}
124+
catch
125+
{
126+
// Swallow exceptions in last-resort exception handlers to prevent crashing the process.
127+
}
114128
}
115129
}

src/MADE.Networking/Extensions/UriExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public static class UriExtensions
1717
/// <param name="uri">The <see cref="Uri"/> to extract a query value from.</param>
1818
/// <param name="queryParam">The key of the parameter in the query to extract the value for.</param>
1919
/// <returns>The value for the query parameter.</returns>
20-
public static string GetQueryValue(this Uri uri, string queryParam)
20+
public static string? GetQueryValue(this Uri uri, string queryParam)
2121
{
2222
NameValueCollection queryDictionary = System.Web.HttpUtility.ParseQueryString(uri.Query);
2323
return queryDictionary.Get(queryParam);

src/MADE.Networking/Http/INetworkRequestManager.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// See the LICENSE file in the project root for more information.
33

44
using System.Collections.Concurrent;
5+
using System.Threading.Tasks;
56
using MADE.Networking.Http.Requests;
67

78
namespace MADE.Networking.Http;
@@ -100,5 +101,6 @@ void AddOrUpdate<TRequest, TResponse, TErrorResponse>(
100101
/// <summary>
101102
/// Processes the current queue of network requests.
102103
/// </summary>
103-
void ProcessCurrentQueue();
104+
/// <returns>An asynchronous operation.</returns>
105+
Task ProcessCurrentQueueAsync();
104106
}

src/MADE.Networking/Http/NetworkRequestFactory.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,43 +33,43 @@ private NetworkRequestFactory(IHttpClientFactory httpClientFactory, string? clie
3333
/// <inheritdoc/>
3434
public JsonGetNetworkRequest Get(string url, Dictionary<string, string>? headers = null)
3535
{
36-
return new JsonGetNetworkRequest(this.CreateClient(), url, headers!);
36+
return new JsonGetNetworkRequest(this.CreateClient(), url, headers);
3737
}
3838

3939
/// <inheritdoc/>
4040
public JsonPostNetworkRequest Post(string url, string? jsonData = null, Dictionary<string, string>? headers = null)
4141
{
42-
return new JsonPostNetworkRequest(this.CreateClient(), url, jsonData!, headers!);
42+
return new JsonPostNetworkRequest(this.CreateClient(), url, jsonData, headers);
4343
}
4444

4545
/// <inheritdoc/>
4646
public JsonPutNetworkRequest Put(string url, string? jsonData = null, Dictionary<string, string>? headers = null)
4747
{
48-
return new JsonPutNetworkRequest(this.CreateClient(), url, jsonData!, headers!);
48+
return new JsonPutNetworkRequest(this.CreateClient(), url, jsonData, headers);
4949
}
5050

5151
/// <inheritdoc/>
5252
public JsonPatchNetworkRequest Patch(string url, string? jsonData = null, Dictionary<string, string>? headers = null)
5353
{
54-
return new JsonPatchNetworkRequest(this.CreateClient(), url, jsonData!, headers!);
54+
return new JsonPatchNetworkRequest(this.CreateClient(), url, jsonData, headers);
5555
}
5656

5757
/// <inheritdoc/>
5858
public JsonDeleteNetworkRequest Delete(string url, Dictionary<string, string>? headers = null)
5959
{
60-
return new JsonDeleteNetworkRequest(this.CreateClient(), url, headers!);
60+
return new JsonDeleteNetworkRequest(this.CreateClient(), url, headers);
6161
}
6262

6363
/// <inheritdoc/>
6464
public StreamGetNetworkRequest GetStream(string url, Dictionary<string, string>? headers = null)
6565
{
66-
return new StreamGetNetworkRequest(this.CreateClient(), url, headers!);
66+
return new StreamGetNetworkRequest(this.CreateClient(), url, headers);
6767
}
6868

6969
/// <inheritdoc/>
7070
public MultipartFormDataPostNetworkRequest PostMultipart(string url, Dictionary<string, string>? headers = null)
7171
{
72-
return new MultipartFormDataPostNetworkRequest(this.CreateClient(), url, headers!);
72+
return new MultipartFormDataPostNetworkRequest(this.CreateClient(), url, headers);
7373
}
7474

7575
/// <inheritdoc/>

src/MADE.Networking/Http/NetworkRequestManager.cs

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@ public void Dispose()
8484
/// <summary>
8585
/// Processes the current queue of network requests.
8686
/// </summary>
87-
public void ProcessCurrentQueue()
87+
/// <returns>An asynchronous operation.</returns>
88+
public async Task ProcessCurrentQueueAsync()
8889
{
8990
if (this.CurrentQueue.Count == 0 || this.isProcessingRequests)
9091
{
@@ -102,13 +103,13 @@ public void ProcessCurrentQueue()
102103
{
103104
if (this.CurrentQueue.TryRemove(
104105
this.CurrentQueue.FirstOrDefault().Key,
105-
out NetworkRequestCallback request))
106+
out NetworkRequestCallback? request))
106107
{
107108
requestTasks.Add(ExecuteRequestsAsync(this.CurrentQueue, request, cts.Token));
108109
}
109110
}
110111

111-
Task.WhenAll(requestTasks).GetAwaiter().GetResult();
112+
await Task.WhenAll(requestTasks).ConfigureAwait(false);
112113
}
113114
finally
114115
{
@@ -220,23 +221,39 @@ private static async Task ExecuteRequestsAsync(
220221
}
221222

222223
NetworkRequest request = requestCallback.Request;
223-
WeakReferenceCallback successCallback = requestCallback.SuccessCallback;
224-
WeakReferenceCallback errorCallback = requestCallback.ErrorCallback;
224+
WeakReferenceCallback? successCallback = requestCallback.SuccessCallback;
225+
WeakReferenceCallback? errorCallback = requestCallback.ErrorCallback;
225226

226227
try
227228
{
229+
if (successCallback is null)
230+
{
231+
return;
232+
}
233+
228234
object response = await request.ExecuteAsync(successCallback.Type, cancellationToken).ConfigureAwait(false);
229235
successCallback.Invoke(response);
230236
}
231237
catch (Exception ex)
232238
{
233-
successCallback.Invoke(Activator.CreateInstance(successCallback.Type));
239+
if (successCallback is not null)
240+
{
241+
successCallback.Invoke(Activator.CreateInstance(successCallback.Type)!);
242+
}
243+
234244
errorCallback?.Invoke(ex);
235245
}
236246
}
237247

238-
private void OnProcessTimerTick(object sender, object e)
248+
private async void OnProcessTimerTick(object sender, object e)
239249
{
240-
this.ProcessCurrentQueue();
250+
try
251+
{
252+
await this.ProcessCurrentQueueAsync().ConfigureAwait(false);
253+
}
254+
catch
255+
{
256+
// Swallow exceptions in timer callback to prevent unobserved task exceptions.
257+
}
241258
}
242259
}

src/MADE.Networking/Http/Requests/Json/JsonDeleteNetworkRequest.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ namespace MADE.Networking.Http.Requests.Json;
1515
/// </summary>
1616
public sealed class JsonDeleteNetworkRequest : NetworkRequest
1717
{
18+
private static readonly JsonSerializerOptions DefaultJsonOptions = new() { PropertyNameCaseInsensitive = true };
19+
1820
private readonly HttpClient client;
1921

2022
/// <summary>
@@ -43,7 +45,7 @@ public JsonDeleteNetworkRequest(HttpClient client, string url)
4345
/// <param name="headers">
4446
/// The additional headers.
4547
/// </param>
46-
public JsonDeleteNetworkRequest(HttpClient client, string url, Dictionary<string, string> headers)
48+
public JsonDeleteNetworkRequest(HttpClient client, string url, Dictionary<string, string>? headers)
4749
: base(url, headers)
4850
{
4951
this.client = client ?? throw new ArgumentNullException(nameof(client));
@@ -64,7 +66,7 @@ public JsonDeleteNetworkRequest(HttpClient client, string url, Dictionary<string
6466
public override async Task<TResponse> ExecuteAsync<TResponse>(CancellationToken cancellationToken = default)
6567
{
6668
string json = await this.GetJsonResponseAsync(cancellationToken).ConfigureAwait(false);
67-
return JsonSerializer.Deserialize<TResponse>(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
69+
return JsonSerializer.Deserialize<TResponse>(json, DefaultJsonOptions);
6870
}
6971

7072
/// <summary>
@@ -84,7 +86,7 @@ public override async Task<object> ExecuteAsync(
8486
CancellationToken cancellationToken = default)
8587
{
8688
string json = await this.GetJsonResponseAsync(cancellationToken).ConfigureAwait(false);
87-
return JsonSerializer.Deserialize(json, expectedResponse, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
89+
return JsonSerializer.Deserialize(json, expectedResponse, DefaultJsonOptions);
8890
}
8991

9092
private async Task<string> GetJsonResponseAsync(CancellationToken cancellationToken = default)
@@ -118,6 +120,6 @@ private async Task<string> GetJsonResponseAsync(CancellationToken cancellationTo
118120

119121
response.EnsureSuccessStatusCode();
120122

121-
return await response.Content.ReadAsStringAsync().ConfigureAwait(false);
123+
return await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
122124
}
123125
}

src/MADE.Networking/Http/Requests/Json/JsonGetNetworkRequest.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ namespace MADE.Networking.Http.Requests.Json;
1515
/// </summary>
1616
public sealed class JsonGetNetworkRequest : NetworkRequest
1717
{
18+
private static readonly JsonSerializerOptions DefaultJsonOptions = new() { PropertyNameCaseInsensitive = true };
19+
1820
private readonly HttpClient client;
1921

2022
/// <summary>
@@ -43,7 +45,7 @@ public JsonGetNetworkRequest(HttpClient client, string url)
4345
/// <param name="headers">
4446
/// The additional headers.
4547
/// </param>
46-
public JsonGetNetworkRequest(HttpClient client, string url, Dictionary<string, string> headers)
48+
public JsonGetNetworkRequest(HttpClient client, string url, Dictionary<string, string>? headers)
4749
: base(url, headers)
4850
{
4951
this.client = client ?? throw new ArgumentNullException(nameof(client));
@@ -64,7 +66,7 @@ public JsonGetNetworkRequest(HttpClient client, string url, Dictionary<string, s
6466
public override async Task<TResponse> ExecuteAsync<TResponse>(CancellationToken cancellationToken = default)
6567
{
6668
string json = await this.GetJsonResponseAsync(cancellationToken).ConfigureAwait(false);
67-
return JsonSerializer.Deserialize<TResponse>(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
69+
return JsonSerializer.Deserialize<TResponse>(json, DefaultJsonOptions);
6870
}
6971

7072
/// <summary>
@@ -84,7 +86,7 @@ public override async Task<object> ExecuteAsync(
8486
CancellationToken cancellationToken = default)
8587
{
8688
string json = await this.GetJsonResponseAsync(cancellationToken).ConfigureAwait(false);
87-
return JsonSerializer.Deserialize(json, expectedResponse, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
89+
return JsonSerializer.Deserialize(json, expectedResponse, DefaultJsonOptions);
8890
}
8991

9092
private async Task<string> GetJsonResponseAsync(CancellationToken cancellationToken = default)
@@ -118,6 +120,6 @@ private async Task<string> GetJsonResponseAsync(CancellationToken cancellationTo
118120

119121
response.EnsureSuccessStatusCode();
120122

121-
return await response.Content.ReadAsStringAsync().ConfigureAwait(false);
123+
return await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
122124
}
123125
}

0 commit comments

Comments
 (0)