From 9e3362a1e8ab1524790abbf0d624a67476710156 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Apr 2026 14:19:46 +0000 Subject: [PATCH 1/4] Initial plan From f3b7d6ba76d2f3b579a4d2ed19fe412b4250c543 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Apr 2026 14:26:25 +0000 Subject: [PATCH 2/4] Fix timezone mismatch in ExtensionMethods causing Delay step to get stuck with PostgreSQL Agent-Logs-Url: https://github.com/danielgerlag/workflow-core/sessions/85da3a83-cc5a-4865-8002-bac682193a86 Co-authored-by: danielgerlag <2357007+danielgerlag@users.noreply.github.com> --- .../ExtensionMethods.cs | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/providers/WorkflowCore.Persistence.EntityFramework/ExtensionMethods.cs b/src/providers/WorkflowCore.Persistence.EntityFramework/ExtensionMethods.cs index 536ccc7be..62825f727 100644 --- a/src/providers/WorkflowCore.Persistence.EntityFramework/ExtensionMethods.cs +++ b/src/providers/WorkflowCore.Persistence.EntityFramework/ExtensionMethods.cs @@ -137,6 +137,13 @@ internal static PersistedScheduledCommand ToPersistable(this ScheduledCommand in return result; } + private static DateTime EnsureUtc(DateTime dateTime) + { + if (dateTime.Kind == DateTimeKind.Local) + return dateTime.ToUniversalTime(); + return DateTime.SpecifyKind(dateTime, DateTimeKind.Utc); + } + internal static WorkflowInstance ToWorkflowInstance(this PersistedWorkflow instance) { WorkflowInstance result = new WorkflowInstance(); @@ -148,9 +155,9 @@ internal static WorkflowInstance ToWorkflowInstance(this PersistedWorkflow insta result.Version = instance.Version; result.WorkflowDefinitionId = instance.WorkflowDefinitionId; result.Status = instance.Status; - result.CreateTime = DateTime.SpecifyKind(instance.CreateTime, DateTimeKind.Utc); + result.CreateTime = EnsureUtc(instance.CreateTime); if (instance.CompleteTime.HasValue) - result.CompleteTime = DateTime.SpecifyKind(instance.CompleteTime.Value, DateTimeKind.Utc); + result.CompleteTime = EnsureUtc(instance.CompleteTime.Value); result.ExecutionPointers = new ExecutionPointerCollection(instance.ExecutionPointers.Count + 8); @@ -163,15 +170,15 @@ internal static WorkflowInstance ToWorkflowInstance(this PersistedWorkflow insta pointer.Active = ep.Active; if (ep.SleepUntil.HasValue) - pointer.SleepUntil = DateTime.SpecifyKind(ep.SleepUntil.Value, DateTimeKind.Utc); + pointer.SleepUntil = EnsureUtc(ep.SleepUntil.Value); pointer.PersistenceData = JsonConvert.DeserializeObject(ep.PersistenceData ?? string.Empty, SerializerSettings); if (ep.StartTime.HasValue) - pointer.StartTime = DateTime.SpecifyKind(ep.StartTime.Value, DateTimeKind.Utc); + pointer.StartTime = EnsureUtc(ep.StartTime.Value); if (ep.EndTime.HasValue) - pointer.EndTime = DateTime.SpecifyKind(ep.EndTime.Value, DateTimeKind.Utc); + pointer.EndTime = EnsureUtc(ep.EndTime.Value); pointer.StepName = ep.StepName; @@ -212,7 +219,7 @@ internal static EventSubscription ToEventSubscription(this PersistedSubscription result.StepId = instance.StepId; result.ExecutionPointerId = instance.ExecutionPointerId; result.WorkflowId = instance.WorkflowId; - result.SubscribeAsOf = DateTime.SpecifyKind(instance.SubscribeAsOf, DateTimeKind.Utc); + result.SubscribeAsOf = EnsureUtc(instance.SubscribeAsOf); result.SubscriptionData = JsonConvert.DeserializeObject(instance.SubscriptionData, SerializerSettings); result.ExternalToken = instance.ExternalToken; result.ExternalTokenExpiry = instance.ExternalTokenExpiry; @@ -227,7 +234,7 @@ internal static Event ToEvent(this PersistedEvent instance) result.Id = instance.EventId.ToString(); result.EventKey = instance.EventKey; result.EventName = instance.EventName; - result.EventTime = DateTime.SpecifyKind(instance.EventTime, DateTimeKind.Utc); + result.EventTime = EnsureUtc(instance.EventTime); result.IsProcessed = instance.IsProcessed; result.EventData = JsonConvert.DeserializeObject(instance.EventData, SerializerSettings); From 6da36df13d85f57d887a02cf109b58ec107ce53a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Apr 2026 22:16:02 +0000 Subject: [PATCH 3/4] Address reviewer feedback: optimize EnsureUtc and add unit tests for Local/Utc/Unspecified DateTime conversion Agent-Logs-Url: https://github.com/danielgerlag/workflow-core/sessions/ffb23322-90eb-473d-a351-30db6c1ef619 Co-authored-by: danielgerlag <2357007+danielgerlag@users.noreply.github.com> --- .../ExtensionMethods.cs | 2 + .../Properties/AssemblyInfo.cs | 3 + .../ExtensionMethodsFixture.cs | 93 +++++++++++++++++++ 3 files changed, 98 insertions(+) create mode 100644 test/WorkflowCore.Tests.Sqlite/ExtensionMethodsFixture.cs diff --git a/src/providers/WorkflowCore.Persistence.EntityFramework/ExtensionMethods.cs b/src/providers/WorkflowCore.Persistence.EntityFramework/ExtensionMethods.cs index 62825f727..c31a7f6d5 100644 --- a/src/providers/WorkflowCore.Persistence.EntityFramework/ExtensionMethods.cs +++ b/src/providers/WorkflowCore.Persistence.EntityFramework/ExtensionMethods.cs @@ -141,6 +141,8 @@ private static DateTime EnsureUtc(DateTime dateTime) { if (dateTime.Kind == DateTimeKind.Local) return dateTime.ToUniversalTime(); + if (dateTime.Kind == DateTimeKind.Utc) + return dateTime; return DateTime.SpecifyKind(dateTime, DateTimeKind.Utc); } diff --git a/src/providers/WorkflowCore.Persistence.EntityFramework/Properties/AssemblyInfo.cs b/src/providers/WorkflowCore.Persistence.EntityFramework/Properties/AssemblyInfo.cs index d147caeeb..2eb3c7bf4 100644 --- a/src/providers/WorkflowCore.Persistence.EntityFramework/Properties/AssemblyInfo.cs +++ b/src/providers/WorkflowCore.Persistence.EntityFramework/Properties/AssemblyInfo.cs @@ -1,4 +1,5 @@ using System.Reflection; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following @@ -16,3 +17,5 @@ // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("fe54ad67-817a-4cc6-a9ef-c9f7a5122ca4")] + +[assembly: InternalsVisibleTo("WorkflowCore.Tests.Sqlite")] diff --git a/test/WorkflowCore.Tests.Sqlite/ExtensionMethodsFixture.cs b/test/WorkflowCore.Tests.Sqlite/ExtensionMethodsFixture.cs new file mode 100644 index 000000000..37123ad67 --- /dev/null +++ b/test/WorkflowCore.Tests.Sqlite/ExtensionMethodsFixture.cs @@ -0,0 +1,93 @@ +using System; +using FluentAssertions; +using WorkflowCore.Models; +using WorkflowCore.Persistence.EntityFramework; +using WorkflowCore.Persistence.EntityFramework.Models; +using Xunit; + +namespace WorkflowCore.Tests.Sqlite +{ + public class ExtensionMethodsFixture + { + [Fact] + public void ToWorkflowInstance_CreateTime_Local_Kind_Should_Convert_To_Utc() + { + var localTime = new DateTime(2026, 1, 1, 12, 0, 0, DateTimeKind.Local); + var expectedUtc = localTime.ToUniversalTime(); + + var persisted = BuildPersistedWorkflow(createTime: localTime); + + var instance = persisted.ToWorkflowInstance(); + + instance.CreateTime.Should().Be(expectedUtc); + instance.CreateTime.Kind.Should().Be(DateTimeKind.Utc); + } + + [Fact] + public void ToWorkflowInstance_CreateTime_Utc_Kind_Should_Return_Same_Value() + { + var utcTime = new DateTime(2026, 1, 1, 12, 0, 0, DateTimeKind.Utc); + + var persisted = BuildPersistedWorkflow(createTime: utcTime); + + var instance = persisted.ToWorkflowInstance(); + + instance.CreateTime.Should().Be(utcTime); + instance.CreateTime.Kind.Should().Be(DateTimeKind.Utc); + } + + [Fact] + public void ToWorkflowInstance_CreateTime_Unspecified_Kind_Should_Treat_As_Utc() + { + var unspecifiedTime = new DateTime(2026, 1, 1, 12, 0, 0, DateTimeKind.Unspecified); + + var persisted = BuildPersistedWorkflow(createTime: unspecifiedTime); + + var instance = persisted.ToWorkflowInstance(); + + instance.CreateTime.Should().Be(DateTime.SpecifyKind(unspecifiedTime, DateTimeKind.Utc)); + instance.CreateTime.Kind.Should().Be(DateTimeKind.Utc); + } + + [Fact] + public void ToWorkflowInstance_SleepUntil_Local_Kind_Should_Convert_To_Utc() + { + var localTime = new DateTime(2026, 3, 26, 12, 30, 31, DateTimeKind.Local); + var expectedUtc = localTime.ToUniversalTime(); + + var persisted = BuildPersistedWorkflow(); + persisted.ExecutionPointers.Add(new PersistedExecutionPointer + { + Id = "ep1", + StepId = 0, + Active = true, + SleepUntil = localTime, + PersistenceData = "null", + ContextItem = "null", + Scope = "", + Children = "", + EventData = "null", + Outcome = "null" + }); + + var instance = persisted.ToWorkflowInstance(); + + var pointer = instance.ExecutionPointers.FindById("ep1"); + pointer.SleepUntil.Should().Be(expectedUtc); + pointer.SleepUntil.Value.Kind.Should().Be(DateTimeKind.Utc); + } + + private static PersistedWorkflow BuildPersistedWorkflow(DateTime? createTime = null) + { + return new PersistedWorkflow + { + InstanceId = Guid.NewGuid(), + WorkflowDefinitionId = "test", + Version = 1, + Status = WorkflowStatus.Runnable, + Data = "{}", + CreateTime = createTime ?? new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc) + }; + } + } +} From 3e217a59333a337133b60e449aca87b155f5be44 Mon Sep 17 00:00:00 2001 From: Daniel Gerlag Date: Sun, 19 Apr 2026 17:43:57 -0700 Subject: [PATCH 4/4] fix: apply EnsureUtc to ExternalTokenExpiry in ToEventSubscription The ExternalTokenExpiry DateTime? field was missed in the initial fix and would still suffer from incorrect UTC handling when PostgreSQL returns DateTime values with Kind=Local. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ExtensionMethods.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/providers/WorkflowCore.Persistence.EntityFramework/ExtensionMethods.cs b/src/providers/WorkflowCore.Persistence.EntityFramework/ExtensionMethods.cs index c31a7f6d5..6e88bae09 100644 --- a/src/providers/WorkflowCore.Persistence.EntityFramework/ExtensionMethods.cs +++ b/src/providers/WorkflowCore.Persistence.EntityFramework/ExtensionMethods.cs @@ -224,7 +224,8 @@ internal static EventSubscription ToEventSubscription(this PersistedSubscription result.SubscribeAsOf = EnsureUtc(instance.SubscribeAsOf); result.SubscriptionData = JsonConvert.DeserializeObject(instance.SubscriptionData, SerializerSettings); result.ExternalToken = instance.ExternalToken; - result.ExternalTokenExpiry = instance.ExternalTokenExpiry; + if (instance.ExternalTokenExpiry.HasValue) + result.ExternalTokenExpiry = EnsureUtc(instance.ExternalTokenExpiry.Value); result.ExternalWorkerId = instance.ExternalWorkerId; return result;