diff --git a/Directory.Packages.props b/Directory.Packages.props
index fa951f8d7d..6a900d2ab6 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -181,7 +181,7 @@
-
+
diff --git a/framework/src/Volo.Abp.Timing/Volo/Abp/Timing/TimeZoneHelper.cs b/framework/src/Volo.Abp.Timing/Volo/Abp/Timing/TimeZoneHelper.cs
index 6101585878..23446069a9 100644
--- a/framework/src/Volo.Abp.Timing/Volo/Abp/Timing/TimeZoneHelper.cs
+++ b/framework/src/Volo.Abp.Timing/Volo/Abp/Timing/TimeZoneHelper.cs
@@ -7,14 +7,43 @@ namespace Volo.Abp.Timing;
public static class TimeZoneHelper
{
+ ///
+ /// Returns timezone list ordered by display name, enriched with UTC offset, filtering out invalid ids.
+ ///
public static List GetTimezones(List timezones)
{
return timezones
.OrderBy(x => x.Name)
- .Select(x => new NameValue( $"{x.Name} ({GetTimezoneOffset(TZConvert.GetTimeZoneInfo(x.Name))})", x.Name))
+ .Select(TryCreateNameValueWithOffset)
+ .OfType()
.ToList();
}
+ ///
+ /// Builds a with the original timezone ID in Value and a display name that includes
+ /// the UTC offset in the Name property; returns null if the id is not found.
+ ///
+ public static NameValue? TryCreateNameValueWithOffset(NameValue timeZone)
+ {
+ try
+ {
+ var timeZoneInfo = TZConvert.GetTimeZoneInfo(timeZone.Name);
+ var name = $"{timeZone.Name} ({GetTimezoneOffset(timeZoneInfo)})";
+ return new NameValue(name, timeZone.Name);
+ }
+ catch (Exception)
+ {
+ // Invalid or unknown timezone IDs are expected here (e.g. from user input or
+ // external sources). We intentionally swallow this exception and return null
+ // so callers (like GetTimezones) can filter out invalid entries.
+ }
+
+ return null;
+ }
+
+ ///
+ /// Formats the base UTC offset as "+hh:mm" or "-hh:mm" for display purposes.
+ ///
public static string GetTimezoneOffset(TimeZoneInfo timeZoneInfo)
{
if (timeZoneInfo.BaseUtcOffset < TimeSpan.Zero)
diff --git a/framework/test/Volo.Abp.Timing.Tests/Volo/Abp/Timing/TimeZoneHelper_Tests.cs b/framework/test/Volo.Abp.Timing.Tests/Volo/Abp/Timing/TimeZoneHelper_Tests.cs
new file mode 100644
index 0000000000..54d8d37361
--- /dev/null
+++ b/framework/test/Volo.Abp.Timing.Tests/Volo/Abp/Timing/TimeZoneHelper_Tests.cs
@@ -0,0 +1,39 @@
+using System.Collections.Generic;
+using Shouldly;
+using TimeZoneConverter;
+using Volo.Abp.Testing;
+using Xunit;
+
+namespace Volo.Abp.Timing;
+
+public class TimeZoneHelper_Tests : AbpIntegratedTest
+{
+ [Fact]
+ public void GetTimezones_Should_Filter_Invalid_Timezones()
+ {
+ var validTimeZoneId = "UTC";
+ var invalidTimeZoneId = "Invalid/Zone";
+
+ var timezones = new List
+ {
+ new(invalidTimeZoneId, invalidTimeZoneId),
+ new(validTimeZoneId, validTimeZoneId)
+ };
+
+ var result = TimeZoneHelper.GetTimezones(timezones);
+
+ result.Count.ShouldBe(1);
+
+ var expectedTimeZoneInfo = TZConvert.GetTimeZoneInfo(validTimeZoneId);
+ var expectedName = $"{validTimeZoneId} ({TimeZoneHelper.GetTimezoneOffset(expectedTimeZoneInfo)})";
+
+ result[0].Name.ShouldBe(expectedName);
+ result[0].Value.ShouldBe(validTimeZoneId);
+ }
+
+ [Fact]
+ public void TryCreateNameValueWithOffset_Should_Return_Null_For_Invalid_Timezone()
+ {
+ TimeZoneHelper.TryCreateNameValueWithOffset(new NameValue("Invalid/Zone", "Invalid/Zone")).ShouldBeNull();
+ }
+}