Browse Source

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.
pull/25703/head
maliming 1 day ago
parent
commit
55f843e954
No known key found for this signature in database GPG Key ID: A646B9CB645ECEA4
  1. 6
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ModelBinding/AbpDateTimeModelBinder.cs
  2. 6
      framework/src/Volo.Abp.Json.Newtonsoft/Volo/Abp/Json/Newtonsoft/AbpDateTimeConverter.cs
  3. 15
      framework/src/Volo.Abp.Json.SystemTextJson/Volo/Abp/Json/SystemTextJson/JsonConverters/AbpDateTimeConverterBase.cs
  4. 37
      framework/src/Volo.Abp.Timing/Volo/Abp/Timing/TimezoneProviderExtensions.cs
  5. 11
      framework/test/Volo.Abp.Json.Tests/Volo/Abp/Json/AbpDateTimeConverterTimezone_Tests.cs
  6. 60
      framework/test/Volo.Abp.Timing.Tests/Volo/Abp/Timing/TimezoneProviderExtensions_Tests.cs
  7. 4
      modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/GithubDocumentSource.cs

6
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);
}
}

6
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

15
framework/src/Volo.Abp.Json.SystemTextJson/Volo/Abp/Json/SystemTextJson/JsonConverters/AbpDateTimeConverterBase.cs

@ -101,23 +101,14 @@ public abstract class AbpDateTimeConverterBase<T> : JsonConverter<T>
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);

37
framework/src/Volo.Abp.Timing/Volo/Abp/Timing/TimezoneProviderExtensions.cs

@ -0,0 +1,37 @@
using System;
namespace Volo.Abp.Timing;
public static class TimezoneProviderExtensions
{
/// <summary>
/// Interprets <paramref name="dateTime"/> as local time in <paramref name="windowsOrIanaTimeZoneId"/>
/// and converts it to its UTC equivalent. The caller is expected to pass a
/// <see cref="DateTimeKind.Unspecified"/> value; the kind is not inspected, so a value is always
/// treated as wall-clock time in the given timezone regardless of its kind.
/// </summary>
/// <remarks>
/// Returns <paramref name="dateTime"/> unchanged when applying the timezone offset would move it
/// outside the supported <see cref="DateTime"/> range. This happens for values within the offset
/// distance of <see cref="DateTime.MinValue"/>/<see cref="DateTime.MaxValue"/>, typically the
/// <see cref="DateTime.MinValue"/> placeholder that does not represent a real instant. Computing the
/// UTC ticks directly avoids the <see cref="ArgumentOutOfRangeException"/> that
/// <c>new DateTimeOffset(dateTime, offset)</c> would throw for such values.
/// </remarks>
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);
}
}

11
framework/test/Volo.Abp.Json.Tests/Volo/Abp/Json/AbpDateTimeConverterTimezone_Tests.cs

@ -16,12 +16,11 @@ namespace Volo.Abp.Json;
///
/// When <see cref="AbpClockOptions.Kind"/> is <see cref="DateTimeKind.Utc"/> the converter treats
/// an <see cref="DateTimeKind.Unspecified"/> value as local time in the current user's timezone and
/// converts it to UTC via <c>new DateTimeOffset(value, offset).UtcDateTime</c>. For a placeholder
/// such as <see cref="DateTime.MinValue"/> a positive offset (e.g. Asia/Shanghai = +08:00) pushes
/// the value before <see cref="DateTime.MinValue"/> and throws <see cref="ArgumentOutOfRangeException"/>,
/// which used to be swallowed and logged as a warning on every serialization. The converter now
/// skips the timezone conversion for the <see cref="DateTime.MinValue"/>/<see cref="DateTime.MaxValue"/>
/// 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 <see cref="DateTime.MinValue"/>/
/// <see cref="DateTime.MaxValue"/> (most commonly the <see cref="DateTime.MinValue"/> 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.
/// </summary>
public class AbpDateTimeConverterTimezone_Tests : AbpJsonSystemTextJsonTestBase
{

60
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<AbpTimingTestModule>
{
private readonly ITimezoneProvider _timezoneProvider;
public TimezoneProviderExtensions_Tests()
{
_timezoneProvider = GetRequiredService<ITimezoneProvider>();
}
[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);
}
}

4
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)
{

Loading…
Cancel
Save