Skip to content

Commit dd4ac22

Browse files
committed
chore(core): upgrade to .NET 9 and apply various fixes and improvements
1 parent ba724ec commit dd4ac22

File tree

9 files changed

+245
-74
lines changed

9 files changed

+245
-74
lines changed

src/WART-Core/Authentication/JWT/JwtServiceCollectionExtension.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ public static IServiceCollection AddJwtMiddleware(this IServiceCollection servic
6868
ValidateIssuer = false,
6969
ValidateActor = false,
7070
ValidateLifetime = true,
71+
ValidateIssuerSigningKey = true,
7172
IssuerSigningKey = securityKey
7273
};
7374
options.Events = new JwtBearerEvents

src/WART-Core/Hubs/WartHub.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// (c) 2019 Francesco Del Re <francesco.delre.87@gmail.com>
22
// This code is licensed under MIT license (see LICENSE.txt for details)
3-
using Microsoft.Extensions.Logging;
4-
3+
using Microsoft.Extensions.Logging;
4+
55
namespace WART_Core.Hubs
66
{
77
/// <summary>

src/WART-Core/Middleware/WartApplicationBuilderExtension.cs

Lines changed: 39 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@ public static class WartApplicationBuilderExtension
1818
private static string NormalizeHubPath(string name)
1919
=> "/" + (name ?? string.Empty).Trim().Trim('/');
2020

21+
private static IReadOnlyList<string> GetDistinctPaths(IEnumerable<string> hubNameList)
22+
=> (hubNameList ?? [])
23+
.Where(s => !string.IsNullOrWhiteSpace(s))
24+
.Select(NormalizeHubPath)
25+
.Distinct(StringComparer.Ordinal)
26+
.ToList();
27+
2128
/// <summary>
2229
/// Configures and adds the WART middleware to the IApplicationBuilder.
2330
/// This method sets up the default SignalR hub (warthub) without authentication.
@@ -219,45 +226,43 @@ public static IApplicationBuilder UseWartMiddleware(this IApplicationBuilder app
219226
{
220227
ArgumentNullException.ThrowIfNull(hubNameList);
221228

229+
var paths = GetDistinctPaths(hubNameList);
230+
222231
app.UseForwardedHeaders();
223232
app.UseResponseCompression();
224233
app.UseRouting();
225234

226-
foreach (var hubName in hubNameList.Distinct())
235+
switch (hubType)
227236
{
228-
switch (hubType)
229-
{
230-
default:
231-
case HubType.NoAuthentication:
232-
{
233-
app.UseEndpoints(endpoints =>
234-
{
235-
endpoints.MapControllers();
236-
endpoints.MapHub<WartHub>(NormalizeHubPath(hubName));
237-
});
238-
break;
239-
}
240-
case HubType.JwtAuthentication:
241-
{
242-
app.UseJwtMiddleware();
243-
app.UseEndpoints(endpoints =>
244-
{
245-
endpoints.MapControllers();
246-
endpoints.MapHub<WartHubJwt>(NormalizeHubPath(hubName));
247-
});
248-
break;
249-
}
250-
case HubType.CookieAuthentication:
251-
{
252-
app.UseCookieMiddleware();
253-
app.UseEndpoints(endpoints =>
254-
{
255-
endpoints.MapControllers();
256-
endpoints.MapHub<WartHubCookie>(NormalizeHubPath(hubName));
257-
});
258-
break;
259-
}
260-
}
237+
default:
238+
case HubType.NoAuthentication:
239+
app.UseEndpoints(endpoints =>
240+
{
241+
endpoints.MapControllers();
242+
foreach (var path in paths)
243+
endpoints.MapHub<WartHub>(path);
244+
});
245+
break;
246+
247+
case HubType.JwtAuthentication:
248+
app.UseJwtMiddleware();
249+
app.UseEndpoints(endpoints =>
250+
{
251+
endpoints.MapControllers();
252+
foreach (var path in paths)
253+
endpoints.MapHub<WartHubJwt>(path);
254+
});
255+
break;
256+
257+
case HubType.CookieAuthentication:
258+
app.UseCookieMiddleware();
259+
app.UseEndpoints(endpoints =>
260+
{
261+
endpoints.MapControllers();
262+
foreach (var path in paths)
263+
endpoints.MapHub<WartHubCookie>(path);
264+
});
265+
break;
261266
}
262267

263268
return app;

src/WART-Core/Serialization/JsonArrayOrObjectStringConverter.cs

Lines changed: 30 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,45 +6,50 @@
66

77
namespace WART_Core.Serialization
88
{
9+
/// <summary>
10+
/// A custom JSON converter that allows a string property to accept JSON objects or arrays
11+
/// as raw JSON text. This is useful when the schema is flexible, and a value could be a
12+
/// primitive string or an entire JSON structure (array or object).
13+
/// </summary>
914
public class JsonArrayOrObjectStringConverter : JsonConverter<string>
1015
{
16+
/// <summary>
17+
/// Reads JSON tokens and returns them as a raw JSON string.
18+
/// If the token is a string, returns its value.
19+
/// If it's an object or array, serializes it to a JSON string.
20+
/// </summary>
1121
public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
1222
{
13-
return reader.GetString();
23+
return reader.TokenType switch
24+
{
25+
JsonTokenType.String => reader.GetString(),
26+
JsonTokenType.StartObject or JsonTokenType.StartArray => JsonDocument.ParseValue(ref reader).RootElement.GetRawText(),
27+
JsonTokenType.Null => null,
28+
_ => reader.GetString()
29+
};
1430
}
1531

32+
/// <summary>
33+
/// Writes a string to the JSON output.
34+
/// If the string contains valid JSON (object or array), it writes it as raw JSON.
35+
/// Otherwise, it writes it as a simple string value.
36+
/// </summary>
1637
public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options)
1738
{
18-
if (string.IsNullOrEmpty(value))
39+
if (string.IsNullOrWhiteSpace(value))
1940
{
2041
writer.WriteStringValue(value);
2142
return;
2243
}
2344

24-
using (JsonDocument doc = JsonDocument.Parse(value))
45+
try
46+
{
47+
using var doc = JsonDocument.Parse(value);
48+
doc.RootElement.WriteTo(writer);
49+
}
50+
catch
2551
{
26-
if (doc.RootElement.ValueKind == JsonValueKind.Array)
27-
{
28-
writer.WriteStartArray();
29-
foreach (var element in doc.RootElement.EnumerateArray())
30-
{
31-
element.WriteTo(writer);
32-
}
33-
writer.WriteEndArray();
34-
}
35-
else if (doc.RootElement.ValueKind == JsonValueKind.Object)
36-
{
37-
writer.WriteStartObject();
38-
foreach (var property in doc.RootElement.EnumerateObject())
39-
{
40-
property.WriteTo(writer);
41-
}
42-
writer.WriteEndObject();
43-
}
44-
else
45-
{
46-
writer.WriteStringValue(value);
47-
}
52+
writer.WriteStringValue(value);
4853
}
4954
}
5055
}

src/WART-Core/Services/WartEventWorker.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ await _hubContext.Clients
151151

152152
// Log the event sent to the group.
153153
_logger?.LogInformation("Group: {group}, WartEvent: {wartEvent}",
154-
group, wartEvent);
154+
group, wartEvent.ToString());
155155
}
156156

157157
/// <summary>

src/WART-Core/WART-Core.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
<PackageLicenseExpression></PackageLicenseExpression>
1515
<AssemblyVersion>4.0.0.0</AssemblyVersion>
1616
<FileVersion>4.0.0.0</FileVersion>
17-
<Version>5.4.0</Version>
17+
<Version>6.0.0</Version>
1818
<PackageIcon>icon.png</PackageIcon>
1919
<PackageReadmeFile>README.md</PackageReadmeFile>
2020
</PropertyGroup>
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// (c) 2025 Francesco Del Re <francesco.delre.87@gmail.com>
2+
// This code is licensed under MIT license (see LICENSE.txt for details)
3+
using Microsoft.AspNetCore.Http;
4+
using Microsoft.AspNetCore.Mvc;
5+
using Microsoft.AspNetCore.Mvc.Controllers;
6+
using Microsoft.AspNetCore.Mvc.Filters;
7+
using Microsoft.AspNetCore.SignalR;
8+
using Microsoft.Extensions.DependencyInjection;
9+
using Microsoft.Extensions.Logging;
10+
using Moq;
11+
using WART_Core.Controllers;
12+
using WART_Core.Filters;
13+
using WART_Core.Services;
14+
15+
namespace WART_Tests.Controllers
16+
{
17+
public class WartBaseControllerTests
18+
{
19+
public class TestHub : Hub { }
20+
21+
private class SutController : WartBaseController<TestHub>
22+
{
23+
public SutController(IHubContext<TestHub> hubContext, ILogger logger) : base(hubContext, logger) { }
24+
public IActionResult OkResult() => new OkObjectResult(new { ok = true });
25+
}
26+
27+
private (SutController ctrl, ActionExecutingContext aex, ActionExecutedContext aed, WartEventQueueService queue) Arrange(ObjectResult result, IList<IFilterMetadata> filters)
28+
{
29+
var services = new ServiceCollection();
30+
var queue = new WartEventQueueService();
31+
services.AddSingleton(queue);
32+
var sp = services.BuildServiceProvider();
33+
34+
var http = new DefaultHttpContext { RequestServices = sp };
35+
http.Request.Method = "GET";
36+
http.Request.Path = "/api/test";
37+
38+
var actionCtx = new ActionContext(http, new Microsoft.AspNetCore.Routing.RouteData(),
39+
new ControllerActionDescriptor(), modelState: new Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary());
40+
41+
var aex = new ActionExecutingContext(actionCtx, new List<IFilterMetadata>(), new Dictionary<string, object> { { "id", 1 } }, controller: null);
42+
var aed = new ActionExecutedContext(actionCtx, filters, controller: null) { Result = result };
43+
44+
var hubCtx = new Mock<IHubContext<TestHub>>().Object;
45+
var logger = new Mock<ILogger>().Object;
46+
var ctrl = new SutController(hubCtx, logger);
47+
48+
return (ctrl, aex, aed, queue);
49+
}
50+
51+
[Fact]
52+
public void Enqueues_On_ObjectResult_Without_Exclude()
53+
{
54+
var filters = new List<IFilterMetadata>();
55+
var (ctrl, aex, aed, queue) = Arrange(new OkObjectResult(new { x = 1 }), filters);
56+
57+
ctrl.OnActionExecuting(aex);
58+
ctrl.OnActionExecuted(aed);
59+
60+
Assert.Equal(1, queue.Count);
61+
Assert.True(queue.TryPeek(out var item));
62+
Assert.NotNull(item.WartEvent);
63+
Assert.Equal("GET", item.WartEvent.HttpMethod);
64+
Assert.Equal("/api/test", item.WartEvent.HttpPath);
65+
}
66+
67+
[Fact]
68+
public void DoesNotEnqueue_When_Excluded()
69+
{
70+
var filters = new List<IFilterMetadata> { new ExcludeWartAttribute() };
71+
var (ctrl, aex, aed, queue) = Arrange(new OkObjectResult(new { x = 1 }), filters);
72+
73+
ctrl.OnActionExecuting(aex);
74+
ctrl.OnActionExecuted(aed);
75+
76+
Assert.True(queue.IsEmpty);
77+
}
78+
79+
[Fact]
80+
public void Preserves_Filters_For_Worker_GroupUsage()
81+
{
82+
var filters = new List<IFilterMetadata> { new GroupWartAttribute("g1", "g2") };
83+
var (ctrl, aex, aed, queue) = Arrange(new OkObjectResult(new { x = 1 }), filters);
84+
85+
ctrl.OnActionExecuting(aex);
86+
ctrl.OnActionExecuted(aed);
87+
88+
Assert.True(queue.TryPeek(out var item));
89+
Assert.Contains(item.Filters, f => f is GroupWartAttribute);
90+
}
91+
}
92+
}

src/WART-Tests/Entity/WartEventTests.cs

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// (c) 2024 Francesco Del Re <francesco.delre.87@gmail.com>
22
// This code is licensed under MIT license (see LICENSE.txt for details)
3-
namespace WART_Core.Entity.Tests
3+
using WART_Core.Entity;
4+
5+
namespace WART_Tests.Entity
46
{
57
public class WartEventTests
68
{
@@ -17,14 +19,14 @@ public void WartEvent_ConstructorWithParameters_ShouldSetProperties()
1719

1820
// Assert
1921
Assert.NotEqual(Guid.Empty, wartEvent.EventId);
20-
Assert.NotEqual(default(DateTime), wartEvent.TimeStamp);
21-
Assert.NotEqual(default(DateTime), wartEvent.UtcTimeStamp);
22+
Assert.NotEqual(default, wartEvent.TimeStamp);
23+
Assert.NotEqual(default, wartEvent.UtcTimeStamp);
2224
Assert.Equal(httpMethod, wartEvent.HttpMethod);
2325
Assert.Equal(httpPath, wartEvent.HttpPath);
2426
Assert.Equal(remoteAddress, wartEvent.RemoteAddress);
25-
Assert.Null(wartEvent.JsonRequestPayload);
26-
Assert.Null(wartEvent.JsonResponsePayload);
27-
Assert.Null(wartEvent.ExtraInfo);
27+
Assert.True(string.IsNullOrEmpty(wartEvent.JsonRequestPayload));
28+
Assert.True(string.IsNullOrEmpty(wartEvent.JsonResponsePayload));
29+
Assert.True(string.IsNullOrEmpty(wartEvent.ExtraInfo));
2830
}
2931

3032
[Fact]
@@ -42,14 +44,14 @@ public void WartEvent_ConstructorWithRequestAndResponse_ShouldSetProperties()
4244

4345
// Assert
4446
Assert.NotEqual(Guid.Empty, wartEvent.EventId);
45-
Assert.NotEqual(default(DateTime), wartEvent.TimeStamp);
46-
Assert.NotEqual(default(DateTime), wartEvent.UtcTimeStamp);
47+
Assert.NotEqual(default, wartEvent.TimeStamp);
48+
Assert.NotEqual(default, wartEvent.UtcTimeStamp);
4749
Assert.Equal(httpMethod, wartEvent.HttpMethod);
4850
Assert.Equal(httpPath, wartEvent.HttpPath);
4951
Assert.Equal(remoteAddress, wartEvent.RemoteAddress);
50-
Assert.NotNull(wartEvent.JsonRequestPayload);
51-
Assert.NotNull(wartEvent.JsonResponsePayload);
52-
Assert.Null(wartEvent.ExtraInfo);
52+
Assert.False(string.IsNullOrEmpty(wartEvent.JsonRequestPayload));
53+
Assert.False(string.IsNullOrEmpty(wartEvent.JsonResponsePayload));
54+
Assert.True(string.IsNullOrEmpty(wartEvent.ExtraInfo));
5355
}
5456

5557
[Fact]

0 commit comments

Comments
 (0)