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(); + } +}