From ff91803ff9acbb743123da035d01561705767124 Mon Sep 17 00:00:00 2001 From: John Simons Date: Mon, 28 Sep 2026 17:34:28 +1000 Subject: [PATCH] Fix monthly throughput grouping to use Gregorian calendar regardless of system culture Date formatting with interpolation respects the current thread culture, which can produce incorrect month keys on systems using non-Gregorian calendars (e.g., Thai Buddhist calendar). Using CultureInfo.InvariantCulture ensures yyyy-MM keys are always Gregorian. --- ...hroughputCollector_ThroughputSummary_Tests.cs | 16 ++++++++++++++++ .../ThroughputDataExtensions.cs | 3 ++- .../WebApi/LicensingController.cs | 3 ++- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_ThroughputSummary_Tests.cs b/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_ThroughputSummary_Tests.cs index ddb3725bec..a8fae5c875 100644 --- a/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_ThroughputSummary_Tests.cs +++ b/src/Particular.LicensingComponent.UnitTests/ThroughputCollector/ThroughputCollector_ThroughputSummary_Tests.cs @@ -176,6 +176,22 @@ await DataStore.CreateBuilder() } } + [Test] + [SetCulture("th-TH")] + public async Task Should_key_monthly_throughput_by_gregorian_month_when_the_culture_uses_another_calendar() + { + await DataStore.CreateBuilder() + .AddEndpoint("Endpoint1", sources: [ThroughputSource.Broker]) + .WithThroughput(new ThroughputData([ + new EndpointDailyThroughput(new DateOnly(2025, 1, 10), 50), + new EndpointDailyThroughput(new DateOnly(2025, 2, 20), 60)])) + .Build(); + + var summary = await ThroughputCollector.GetThroughputSummary(CancellationToken.None); + + Assert.That(summary.Single().MonthlyThroughput, Is.EquivalentTo(new[] { new MonthlyThroughput("2025-01", 50), new MonthlyThroughput("2025-02", 60) }), "Monthly throughput must be keyed by Gregorian yyyy-MM months"); + } + [Test] public async Task Should_return_correct_max_daily_throughput_in_summary_when_endpoint_has_zero_throughput() { diff --git a/src/Particular.LicensingComponent/ThroughputDataExtensions.cs b/src/Particular.LicensingComponent/ThroughputDataExtensions.cs index 18a11ed2f6..db47a84a0d 100644 --- a/src/Particular.LicensingComponent/ThroughputDataExtensions.cs +++ b/src/Particular.LicensingComponent/ThroughputDataExtensions.cs @@ -1,5 +1,6 @@ namespace Particular.LicensingComponent; +using System.Globalization; using Contracts; static class ThroughputDataExtensions @@ -29,7 +30,7 @@ public static MonthlyThroughput[] MonthlyThroughput(this List th .Where(x => x.Value >= 0) .GroupBy(x => x.Key, x => x.Value) .ToLookup(x => x.Key, x => x.Max()) - .GroupBy(kvp => $"{kvp.Key:yyyy-MM}", x => x.Sum()) + .GroupBy(kvp => kvp.Key.ToString("yyyy-MM", CultureInfo.InvariantCulture), x => x.Sum()) .Select(group => new MonthlyThroughput(group.Key, group.Sum()))]; public static long AverageMonthlyThroughput(this List throughputs) diff --git a/src/Particular.LicensingComponent/WebApi/LicensingController.cs b/src/Particular.LicensingComponent/WebApi/LicensingController.cs index 99019ae60c..ad011a2aa5 100644 --- a/src/Particular.LicensingComponent/WebApi/LicensingController.cs +++ b/src/Particular.LicensingComponent/WebApi/LicensingController.cs @@ -1,5 +1,6 @@ namespace Particular.LicensingComponent.WebApi { + using System.Globalization; using System.IO.Compression; using System.Text; using System.Text.Json; @@ -66,7 +67,7 @@ public async Task GetThroughputReportFile([FromQuery(Name = "spVersion")] string null, cancellationToken); - var fileName = $"{report.ReportData.CustomerName}.throughput-report-{report.ReportData.EndTime:yyyyMMdd-HHmmss}"; + var fileName = $"{report.ReportData.CustomerName}.throughput-report-{report.ReportData.EndTime.ToString("yyyyMMdd-HHmmss", CultureInfo.InvariantCulture)}"; HttpContext.Response.ContentType = "application/zip"; HttpContext.Response.Headers[HeaderNames.ContentDisposition] = new ContentDispositionHeaderValue("attachment")