@@ -110,6 +110,74 @@ public async Task Workflow_ShouldUseDeterministicGuidGeneration()
110110 Assert . All ( guids , g => Assert . NotEqual ( Guid . Empty , g ) ) ;
111111 }
112112
113+ [ Fact ]
114+ public async Task NewGuid_ShouldRemainStableAcrossReplays ( )
115+ {
116+ var componentsDir = TestDirectoryManager . CreateTestDirectory ( "workflow-components" ) ;
117+ var workflowInstanceId = Guid . NewGuid ( ) . ToString ( ) ;
118+
119+ await using var environment = await DaprTestEnvironment . CreateWithPooledNetworkAsync ( needsActorState : true ) ;
120+ await environment . StartAsync ( ) ;
121+
122+ var harness = new DaprHarnessBuilder ( componentsDir )
123+ . WithEnvironment ( environment )
124+ . BuildWorkflow ( ) ;
125+ await using var testApp = await DaprHarnessBuilder . ForHarness ( harness )
126+ . ConfigureServices ( builder =>
127+ {
128+ builder . Services . AddDaprWorkflowBuilder (
129+ configureRuntime : opt =>
130+ {
131+ opt . RegisterWorkflow < ReplayGuidWorkflow > ( ) ;
132+ } ,
133+ configureClient : ( sp , clientBuilder ) =>
134+ {
135+ var config = sp . GetRequiredService < IConfiguration > ( ) ;
136+ var grpcEndpoint = config [ "DAPR_GRPC_ENDPOINT" ] ;
137+ if ( ! string . IsNullOrEmpty ( grpcEndpoint ) )
138+ clientBuilder . UseGrpcEndpoint ( grpcEndpoint ) ;
139+ } ) ;
140+ } )
141+ . BuildAndStartAsync ( ) ;
142+
143+ using var scope = testApp . CreateScope ( ) ;
144+ var daprWorkflowClient = scope . ServiceProvider . GetRequiredService < DaprWorkflowClient > ( ) ;
145+
146+ await daprWorkflowClient . ScheduleNewWorkflowAsync ( nameof ( ReplayGuidWorkflow ) , workflowInstanceId ) ;
147+
148+ using var statusCts = new CancellationTokenSource ( TimeSpan . FromMinutes ( 1 ) ) ;
149+ var initialStatus = await WaitForGuidStatusAsync ( daprWorkflowClient , workflowInstanceId , phase : 0 , statusCts . Token ) ;
150+ var expectedGuid = initialStatus . Guid ;
151+
152+ await daprWorkflowClient . RaiseEventAsync ( workflowInstanceId , ReplayGuidWorkflow . Event1Name , "go" , statusCts . Token ) ;
153+ var replayStatus = await WaitForGuidStatusAsync ( daprWorkflowClient , workflowInstanceId , phase : 1 , statusCts . Token ) ;
154+ Assert . Equal ( expectedGuid , replayStatus . Guid ) ;
155+
156+ await daprWorkflowClient . RaiseEventAsync ( workflowInstanceId , ReplayGuidWorkflow . Event2Name , "go" , statusCts . Token ) ;
157+
158+ var result = await daprWorkflowClient . WaitForWorkflowCompletionAsync ( workflowInstanceId , cancellation : statusCts . Token ) ;
159+ Assert . Equal ( WorkflowRuntimeStatus . Completed , result . RuntimeStatus ) ;
160+ Assert . Equal ( expectedGuid , result . ReadOutputAs < string > ( ) ) ;
161+ }
162+
163+ private static async Task < ReplayGuidStatus > WaitForGuidStatusAsync (
164+ DaprWorkflowClient client ,
165+ string instanceId ,
166+ int phase ,
167+ CancellationToken cancellationToken )
168+ {
169+ while ( true )
170+ {
171+ cancellationToken . ThrowIfCancellationRequested ( ) ;
172+ var state = await client . GetWorkflowStateAsync ( instanceId , getInputsAndOutputs : true , cancellation : cancellationToken ) ;
173+ var status = state ? . ReadCustomStatusAs < ReplayGuidStatus > ( ) ;
174+ if ( status is not null && status . Phase == phase && ! string . IsNullOrWhiteSpace ( status . Guid ) )
175+ return status ;
176+
177+ await Task . Delay ( TimeSpan . FromMilliseconds ( 200 ) , cancellationToken ) ;
178+ }
179+ }
180+
113181 private sealed partial class LoggingWorkflow : Workflow < string , string >
114182 {
115183 public override async Task < string > RunAsync ( WorkflowContext context , string input )
@@ -147,6 +215,28 @@ public override Task<Guid[]> RunAsync(WorkflowContext context, object? input)
147215 }
148216 }
149217
218+ private sealed class ReplayGuidWorkflow : Workflow < object ? , string >
219+ {
220+ public const string Event1Name = "ReplayGuidStep1" ;
221+ public const string Event2Name = "ReplayGuidStep2" ;
222+
223+ public override async Task < string > RunAsync ( WorkflowContext context , object ? input )
224+ {
225+ var guid = context . NewGuid ( ) . ToString ( ) ;
226+ context . SetCustomStatus ( new ReplayGuidStatus ( 0 , guid ) ) ;
227+
228+ await context . WaitForExternalEventAsync < string > ( Event1Name ) ;
229+ context . SetCustomStatus ( new ReplayGuidStatus ( 1 , guid ) ) ;
230+
231+ await context . WaitForExternalEventAsync < string > ( Event2Name ) ;
232+ context . SetCustomStatus ( new ReplayGuidStatus ( 2 , guid ) ) ;
233+
234+ return guid ;
235+ }
236+ }
237+
238+ private sealed record ReplayGuidStatus ( int Phase , string Guid ) ;
239+
150240 private sealed class SimpleActivity : WorkflowActivity < string , string >
151241 {
152242 public override Task < string > RunAsync ( WorkflowActivityContext context , string input )
0 commit comments