From d9a535515ae336b544a907a6a23380a2330f30b1 Mon Sep 17 00:00:00 2001 From: gdlcf88 <47396430@qq.com> Date: Sat, 27 Jun 2026 20:50:21 +0800 Subject: [PATCH 1/2] Stop logging misleading warning when normalizing Unspecified DateTime near range boundaries AbpDateTimeConverterBase.Normalize converts an Unspecified DateTime to UTC via `new DateTimeOffset(value, offset).UtcDateTime` when the clock is UTC and a user timezone is set. For values within the timezone offset distance of DateTime.MinValue/MaxValue (most notably the DateTime.MinValue placeholder), this throws ArgumentOutOfRangeException. The exception was swallowed and logged as "Could not convert DateTime with unspecified Kind using timezone '...'." on every serialization, flooding logs for positive-offset users (e.g. Asia/Shanghai +08:00). Catch the boundary ArgumentOutOfRangeException explicitly, keep the value unchanged, and log it at Debug level instead of Warning so it stays traceable without polluting production logs. Other unexpected failures still log a warning. Also make Volo.Docs GitHubDocumentSource return a Utc-kind placeholder (DateTime.SpecifyKind(DateTime.MinValue, Utc)) and use Author.Date.UtcDateTime, so it no longer emits Unspecified placeholders in the first place. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../AbpDateTimeConverterBase.cs | 9 + .../AbpDateTimeConverterTimezone_Tests.cs | 201 ++++++++++++++++++ .../GitHub/Documents/GithubDocumentSource.cs | 12 +- 3 files changed, 216 insertions(+), 6 deletions(-) create mode 100644 framework/test/Volo.Abp.Json.Tests/Volo/Abp/Json/AbpDateTimeConverterTimezone_Tests.cs 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 ee39a66678..abf25f773e 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 @@ -106,6 +106,15 @@ public abstract class AbpDateTimeConverterBase : JsonConverter 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); + } catch { Logger.LogWarning("Could not convert DateTime with unspecified Kind using timezone '{TimeZone}'.", CurrentTimezoneProvider.TimeZone); 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 new file mode 100644 index 0000000000..3e1a08772b --- /dev/null +++ b/framework/test/Volo.Abp.Json.Tests/Volo/Abp/Json/AbpDateTimeConverterTimezone_Tests.cs @@ -0,0 +1,201 @@ +using System; +using System.Collections.Concurrent; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Shouldly; +using Volo.Abp.Timing; +using Xunit; + +namespace Volo.Abp.Json; + +/// +/// Regression tests for the warning +/// "Could not convert DateTime with unspecified Kind using timezone '...'." +/// logged by AbpDateTimeConverterBase.Normalize. +/// +/// 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. +/// +public class AbpDateTimeConverterTimezone_Tests : AbpJsonSystemTextJsonTestBase +{ + private const string WarningFragment = "Could not convert DateTime with unspecified Kind"; + + private static readonly CapturingLoggerProvider LogCapture = new(); + + private readonly IJsonSerializer _jsonSerializer; + private readonly ICurrentTimezoneProvider _currentTimezoneProvider; + + public AbpDateTimeConverterTimezone_Tests() + { + _jsonSerializer = GetRequiredService(); + _currentTimezoneProvider = GetRequiredService(); + } + + protected override void AfterAddApplication(IServiceCollection services) + { + // The warning only happens with a UTC clock, where Unspecified values get converted to UTC. + services.Configure(options => options.Kind = DateTimeKind.Utc); + + LogCapture.Clear(); + services.AddSingleton(LogCapture); + + base.AfterAddApplication(services); + } + + private sealed class FileModel + { + public DateTime DateModified { get; set; } + + public DateTime DateCreated { get; set; } + } + + [Theory] + [InlineData("Asia/Shanghai")] // +08:00 + [InlineData("Europe/Brussels")] // +01:00 / +02:00 + public void Should_Not_Warn_When_Serializing_MinValue_Under_Positive_Offset_Timezone(string timeZoneId) + { + _currentTimezoneProvider.TimeZone = timeZoneId; + LogCapture.Clear(); + + DateTime.MinValue.Kind.ShouldBe(DateTimeKind.Unspecified); + + var json = _jsonSerializer.Serialize(new FileModel + { + DateModified = DateTime.MinValue, + DateCreated = DateTime.MinValue + }); + + // The placeholder is serialized unchanged and no warning is logged. + json.ShouldContain("0001-01-01"); + LogCapture.Warnings.ShouldNotContain(m => m.Contains(WarningFragment)); + } + + [Fact] + public void Should_Not_Warn_When_Serializing_Value_Near_MinValue_Under_Positive_Offset_Timezone() + { + // Not exactly DateTime.MinValue: any value within the offset distance of the lower bound + // overflows the same way, so the converter must absorb it rather than warn. + _currentTimezoneProvider.TimeZone = "Asia/Shanghai"; // +08:00 + LogCapture.Clear(); + + var nearMin = DateTime.MinValue.AddHours(3); // 0001-01-01T03:00:00 - 08:00 underflows + + _jsonSerializer.Serialize(new FileModel + { + DateModified = nearMin, + DateCreated = nearMin + }); + + LogCapture.Warnings.ShouldNotContain(m => m.Contains(WarningFragment)); + } + + [Fact] + public void Should_Not_Warn_When_Serializing_MaxValue_Under_Negative_Offset_Timezone() + { + // The symmetric case: a negative offset would push MaxValue past DateTime.MaxValue. + _currentTimezoneProvider.TimeZone = "America/New_York"; + LogCapture.Clear(); + + var json = _jsonSerializer.Serialize(new FileModel + { + DateModified = DateTime.MaxValue, + DateCreated = DateTime.MaxValue + }); + + json.ShouldContain("9999-12-31"); + LogCapture.Warnings.ShouldNotContain(m => m.Contains(WarningFragment)); + } + + [Fact] + public void Should_Not_Warn_When_Serializing_Real_Utc_Timestamp_Under_Positive_Offset_Timezone() + { + _currentTimezoneProvider.TimeZone = "Asia/Shanghai"; + LogCapture.Clear(); + + var utc = new DateTime(2026, 6, 27, 10, 28, 7, DateTimeKind.Utc); + + var json = _jsonSerializer.Serialize(new FileModel + { + DateModified = utc, + DateCreated = utc + }); + + json.ShouldContain("2026-06-27T10:28:07Z"); + LogCapture.Warnings.ShouldNotContain(m => m.Contains(WarningFragment)); + } + + [Fact] + public void Should_Still_Convert_Real_Unspecified_Timestamp_To_Utc_Under_Positive_Offset_Timezone() + { + // A genuine (non-sentinel) Unspecified value must still be converted to UTC using the offset. + _currentTimezoneProvider.TimeZone = "Asia/Shanghai"; // +08:00 + LogCapture.Clear(); + + var unspecified = new DateTime(2026, 6, 27, 18, 0, 0, DateTimeKind.Unspecified); + + var json = _jsonSerializer.Serialize(new FileModel + { + DateModified = unspecified, + DateCreated = unspecified + }); + + // 18:00 in +08:00 == 10:00 UTC. + json.ShouldContain("2026-06-27T10:00:00Z"); + LogCapture.Warnings.ShouldNotContain(m => m.Contains(WarningFragment)); + } + + private sealed class CapturingLoggerProvider : ILoggerProvider + { + public ConcurrentQueue Warnings { get; } = new(); + + public void Clear() + { + Warnings.Clear(); + } + + public ILogger CreateLogger(string categoryName) + { + return new CapturingLogger(Warnings); + } + + public void Dispose() + { + } + + private sealed class CapturingLogger : ILogger + { + private readonly ConcurrentQueue _sink; + + public CapturingLogger(ConcurrentQueue sink) + { + _sink = sink; + } + + public IDisposable BeginScope(TState state) where TState : notnull + { + return null; + } + + public bool IsEnabled(LogLevel logLevel) + { + return logLevel >= LogLevel.Warning; + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, + Func formatter) + { + if (logLevel >= LogLevel.Warning) + { + _sink.Enqueue(formatter(state, exception)); + } + } + } + } +} 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 8d301395c0..8841175d54 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 @@ -173,7 +173,7 @@ namespace Volo.Docs.GitHub.Documents { if (commits == null) { - return DateTime.MinValue; + return DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc); } var gitHubCommit = isFirstCommit ? @@ -182,20 +182,20 @@ namespace Volo.Docs.GitHub.Documents if (gitHubCommit == null) { - return DateTime.MinValue; + return DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc); } if (gitHubCommit.Commit == null) { - return DateTime.MinValue; + return DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc); } if (gitHubCommit.Commit.Author == null) { - return DateTime.MinValue; + return DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc); } - return gitHubCommit.Commit.Author.Date.DateTime; + return gitHubCommit.Commit.Author.Date.UtcDateTime; } private async Task> GetGitHubCommitsOrNull(Project project, string documentName, string languageCode, string version) @@ -243,7 +243,7 @@ namespace Volo.Docs.GitHub.Documents if (_githubPatchAnalyzer.HasPatchSignificantChanges(fullCommit.Files.First(f => f.Filename == fileName).Patch)) { - return gitHubCommit.Commit.Author.Date.DateTime; + return gitHubCommit.Commit.Author.Date.UtcDateTime; } } From 55f843e954d615ae25c90eca33b71ec4ff8dfd10 Mon Sep 17 00:00:00 2001 From: maliming Date: Mon, 29 Jun 2026 08:25:42 +0800 Subject: [PATCH 2/2] 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) {