From 55f843e954d615ae25c90eca33b71ec4ff8dfd10 Mon Sep 17 00:00:00 2001 From: maliming Date: Mon, 29 Jun 2026 08:25:42 +0800 Subject: [PATCH] Avoid boundary overflow when converting unspecified DateTime to UTC Add an ITimezoneProvider.ConvertUnspecifiedToUtc helper that computes the UTC ticks and keeps boundary values unchanged instead of throwing, then use it in the System.Text.Json and Newtonsoft converters and the MVC model binder. Normalize the Volo.Docs commit date filters to UTC for consistency. --- .../ModelBinding/AbpDateTimeModelBinder.cs | 6 +- .../Json/Newtonsoft/AbpDateTimeConverter.cs | 6 +- .../AbpDateTimeConverterBase.cs | 15 +---- .../Abp/Timing/TimezoneProviderExtensions.cs | 37 ++++++++++++ .../AbpDateTimeConverterTimezone_Tests.cs | 11 ++-- .../TimezoneProviderExtensions_Tests.cs | 60 +++++++++++++++++++ .../GitHub/Documents/GithubDocumentSource.cs | 4 +- 7 files changed, 113 insertions(+), 26 deletions(-) create mode 100644 framework/src/Volo.Abp.Timing/Volo/Abp/Timing/TimezoneProviderExtensions.cs create mode 100644 framework/test/Volo.Abp.Timing.Tests/Volo/Abp/Timing/TimezoneProviderExtensions_Tests.cs diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ModelBinding/AbpDateTimeModelBinder.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ModelBinding/AbpDateTimeModelBinder.cs index ac6e44ed6c..35c62a626f 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ModelBinding/AbpDateTimeModelBinder.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ModelBinding/AbpDateTimeModelBinder.cs @@ -42,14 +42,14 @@ public class AbpDateTimeModelBinder : IModelBinder _clock.SupportsMultipleTimezone && !_currentTimezoneProvider.TimeZone.IsNullOrWhiteSpace()) { + var timeZone = _currentTimezoneProvider.TimeZone; try { - var timezoneInfo = _timezoneProvider.GetTimeZoneInfo(_currentTimezoneProvider.TimeZone); - dateTime = new DateTimeOffset(dateTime, timezoneInfo.GetUtcOffset(dateTime)).UtcDateTime; + dateTime = _timezoneProvider.ConvertUnspecifiedToUtc(dateTime, timeZone); } catch { - _logger.LogWarning("Could not convert DateTime with unspecified Kind using timezone '{TimeZone}'.", _currentTimezoneProvider.TimeZone); + _logger.LogWarning("Could not convert DateTime with unspecified Kind using timezone '{TimeZone}'.", timeZone); } } diff --git a/framework/src/Volo.Abp.Json.Newtonsoft/Volo/Abp/Json/Newtonsoft/AbpDateTimeConverter.cs b/framework/src/Volo.Abp.Json.Newtonsoft/Volo/Abp/Json/Newtonsoft/AbpDateTimeConverter.cs index 55c270d14e..3b278d5252 100644 --- a/framework/src/Volo.Abp.Json.Newtonsoft/Volo/Abp/Json/Newtonsoft/AbpDateTimeConverter.cs +++ b/framework/src/Volo.Abp.Json.Newtonsoft/Volo/Abp/Json/Newtonsoft/AbpDateTimeConverter.cs @@ -134,14 +134,14 @@ public class AbpDateTimeConverter : DateTimeConverterBase, ITransientDependency return _skipDateTimeNormalization ? dateTime : _clock.Normalize(dateTime); } + var timeZone = _currentTimezoneProvider.TimeZone; try { - var timezoneInfo = _timezoneProvider.GetTimeZoneInfo(_currentTimezoneProvider.TimeZone); - dateTime = new DateTimeOffset(dateTime, timezoneInfo.GetUtcOffset(dateTime)).UtcDateTime; + dateTime = _timezoneProvider.ConvertUnspecifiedToUtc(dateTime, timeZone); } catch { - Logger.LogWarning("Could not convert DateTime with unspecified Kind using timezone '{TimeZone}'.", _currentTimezoneProvider.TimeZone); + Logger.LogWarning("Could not convert DateTime with unspecified Kind using timezone '{TimeZone}'.", timeZone); } return _skipDateTimeNormalization diff --git a/framework/src/Volo.Abp.Json.SystemTextJson/Volo/Abp/Json/SystemTextJson/JsonConverters/AbpDateTimeConverterBase.cs b/framework/src/Volo.Abp.Json.SystemTextJson/Volo/Abp/Json/SystemTextJson/JsonConverters/AbpDateTimeConverterBase.cs index abf25f773e..02aaec00b2 100644 --- a/framework/src/Volo.Abp.Json.SystemTextJson/Volo/Abp/Json/SystemTextJson/JsonConverters/AbpDateTimeConverterBase.cs +++ b/framework/src/Volo.Abp.Json.SystemTextJson/Volo/Abp/Json/SystemTextJson/JsonConverters/AbpDateTimeConverterBase.cs @@ -101,23 +101,14 @@ public abstract class AbpDateTimeConverterBase : JsonConverter return IsSkipDateTimeNormalization ? dateTime : Clock.Normalize(dateTime); } + var timeZone = CurrentTimezoneProvider.TimeZone; try { - var timezoneInfo = TimezoneProvider.GetTimeZoneInfo(CurrentTimezoneProvider.TimeZone); - dateTime = new DateTimeOffset(dateTime, timezoneInfo.GetUtcOffset(dateTime)).UtcDateTime; - } - catch (ArgumentOutOfRangeException) - { - // Applying the timezone offset moved the value outside the supported DateTime range. - // This happens for values within the offset distance of DateTime.MinValue/MaxValue, - // typically placeholder values (e.g. DateTime.MinValue) that don't represent a real - // instant. Keep the value unchanged and log at Debug level instead of Warning, so it - // stays traceable without flooding production logs on every serialization. - Logger.LogDebug("Skipped timezone conversion for DateTime '{DateTime}' (kind: Unspecified) using timezone '{TimeZone}': applying the offset would move it outside the supported DateTime range.", dateTime, CurrentTimezoneProvider.TimeZone); + dateTime = TimezoneProvider.ConvertUnspecifiedToUtc(dateTime, timeZone); } catch { - Logger.LogWarning("Could not convert DateTime with unspecified Kind using timezone '{TimeZone}'.", CurrentTimezoneProvider.TimeZone); + Logger.LogWarning("Could not convert DateTime with unspecified Kind using timezone '{TimeZone}'.", timeZone); } return IsSkipDateTimeNormalization ? dateTime : Clock.Normalize(dateTime); diff --git a/framework/src/Volo.Abp.Timing/Volo/Abp/Timing/TimezoneProviderExtensions.cs b/framework/src/Volo.Abp.Timing/Volo/Abp/Timing/TimezoneProviderExtensions.cs new file mode 100644 index 0000000000..29779a08be --- /dev/null +++ b/framework/src/Volo.Abp.Timing/Volo/Abp/Timing/TimezoneProviderExtensions.cs @@ -0,0 +1,37 @@ +using System; + +namespace Volo.Abp.Timing; + +public static class TimezoneProviderExtensions +{ + /// + /// Interprets as local time in + /// and converts it to its UTC equivalent. The caller is expected to pass a + /// value; the kind is not inspected, so a value is always + /// treated as wall-clock time in the given timezone regardless of its kind. + /// + /// + /// Returns unchanged when applying the timezone offset would move it + /// outside the supported range. This happens for values within the offset + /// distance of /, typically the + /// placeholder that does not represent a real instant. Computing the + /// UTC ticks directly avoids the that + /// new DateTimeOffset(dateTime, offset) would throw for such values. + /// + public static DateTime ConvertUnspecifiedToUtc( + this ITimezoneProvider timezoneProvider, + DateTime dateTime, + string windowsOrIanaTimeZoneId) + { + Check.NotNull(timezoneProvider, nameof(timezoneProvider)); + + var timezoneInfo = timezoneProvider.GetTimeZoneInfo(windowsOrIanaTimeZoneId); + var utcTicks = dateTime.Ticks - timezoneInfo.GetUtcOffset(dateTime).Ticks; + if (utcTicks < DateTime.MinValue.Ticks || utcTicks > DateTime.MaxValue.Ticks) + { + return dateTime; + } + + return new DateTime(utcTicks, DateTimeKind.Utc); + } +} diff --git a/framework/test/Volo.Abp.Json.Tests/Volo/Abp/Json/AbpDateTimeConverterTimezone_Tests.cs b/framework/test/Volo.Abp.Json.Tests/Volo/Abp/Json/AbpDateTimeConverterTimezone_Tests.cs index 3e1a08772b..202c1d59e2 100644 --- a/framework/test/Volo.Abp.Json.Tests/Volo/Abp/Json/AbpDateTimeConverterTimezone_Tests.cs +++ b/framework/test/Volo.Abp.Json.Tests/Volo/Abp/Json/AbpDateTimeConverterTimezone_Tests.cs @@ -16,12 +16,11 @@ namespace Volo.Abp.Json; /// /// When is the converter treats /// an value as local time in the current user's timezone and -/// converts it to UTC via new DateTimeOffset(value, offset).UtcDateTime. For a placeholder -/// such as a positive offset (e.g. Asia/Shanghai = +08:00) pushes -/// the value before and throws , -/// which used to be swallowed and logged as a warning on every serialization. The converter now -/// skips the timezone conversion for the / -/// sentinel values, so no warning is emitted while the serialized output stays unchanged. +/// converts it to UTC. For a value within the offset distance of / +/// (most commonly the placeholder) a +/// non-zero offset pushes the UTC equivalent outside the supported range, which used to be swallowed +/// and logged as a warning on every serialization. The converter now detects this boundary case and +/// keeps the value unchanged, so no warning is emitted while the serialized output stays the same. /// public class AbpDateTimeConverterTimezone_Tests : AbpJsonSystemTextJsonTestBase { diff --git a/framework/test/Volo.Abp.Timing.Tests/Volo/Abp/Timing/TimezoneProviderExtensions_Tests.cs b/framework/test/Volo.Abp.Timing.Tests/Volo/Abp/Timing/TimezoneProviderExtensions_Tests.cs new file mode 100644 index 0000000000..6707eb8b82 --- /dev/null +++ b/framework/test/Volo.Abp.Timing.Tests/Volo/Abp/Timing/TimezoneProviderExtensions_Tests.cs @@ -0,0 +1,60 @@ +using System; +using Shouldly; +using Volo.Abp.Testing; +using Xunit; + +namespace Volo.Abp.Timing; + +public class TimezoneProviderExtensions_Tests : AbpIntegratedTest +{ + private readonly ITimezoneProvider _timezoneProvider; + + public TimezoneProviderExtensions_Tests() + { + _timezoneProvider = GetRequiredService(); + } + + [Theory] + [InlineData("Asia/Shanghai")] // +08:00 + [InlineData("Europe/Brussels")] // +01:00 / +02:00 + public void Should_Keep_MinValue_Unchanged_Under_Positive_Offset(string timeZoneId) + { + // A positive offset would push DateTime.MinValue below the supported range; keep it as-is. + var result = _timezoneProvider.ConvertUnspecifiedToUtc(DateTime.MinValue, timeZoneId); + + result.ShouldBe(DateTime.MinValue); + result.Kind.ShouldBe(DateTimeKind.Unspecified); + } + + [Fact] + public void Should_Keep_Value_Near_MinValue_Unchanged_Under_Positive_Offset() + { + var nearMin = DateTime.MinValue.AddHours(3); // 0001-01-01T03:00:00 - 08:00 underflows + + var result = _timezoneProvider.ConvertUnspecifiedToUtc(nearMin, "Asia/Shanghai"); + + result.ShouldBe(nearMin); + result.Kind.ShouldBe(DateTimeKind.Unspecified); + } + + [Fact] + public void Should_Keep_MaxValue_Unchanged_Under_Negative_Offset() + { + var result = _timezoneProvider.ConvertUnspecifiedToUtc(DateTime.MaxValue, "America/New_York"); + + result.ShouldBe(DateTime.MaxValue); + result.Kind.ShouldBe(DateTimeKind.Unspecified); + } + + [Fact] + public void Should_Convert_Unspecified_Value_To_Utc_Using_Offset() + { + var unspecified = new DateTime(2026, 6, 27, 18, 0, 0, DateTimeKind.Unspecified); + + var result = _timezoneProvider.ConvertUnspecifiedToUtc(unspecified, "Asia/Shanghai"); // +08:00 + + // 18:00 in +08:00 == 10:00 UTC. + result.ShouldBe(new DateTime(2026, 6, 27, 10, 0, 0, DateTimeKind.Utc)); + result.Kind.ShouldBe(DateTimeKind.Utc); + } +} diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/GithubDocumentSource.cs b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/GithubDocumentSource.cs index 8841175d54..3a770e6781 100644 --- a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/GithubDocumentSource.cs +++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/GithubDocumentSource.cs @@ -230,8 +230,8 @@ namespace Volo.Docs.GitHub.Documents var fileCommitsAfterCreation = commits.Take(commits.Count - 1); var commitsToEvaluate = (lastKnownSignificantUpdateTime != null - ? fileCommitsAfterCreation.Where(c => c.Commit.Author.Date.DateTime > lastKnownSignificantUpdateTime) - : fileCommitsAfterCreation).Where(c => c.Commit.Author.Date.DateTime > DateTime.Now.AddDays(-14)); + ? fileCommitsAfterCreation.Where(c => c.Commit.Author.Date.UtcDateTime > lastKnownSignificantUpdateTime) + : fileCommitsAfterCreation).Where(c => c.Commit.Author.Date.UtcDateTime > DateTime.UtcNow.AddDays(-14)); foreach (var gitHubCommit in commitsToEvaluate) {