diff --git a/Directory.Packages.props b/Directory.Packages.props index a0a48e71d..7d320515d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -29,7 +29,7 @@ - + diff --git a/eng/targets/Release.props b/eng/targets/Release.props index 953a0eff6..8bbd37855 100644 --- a/eng/targets/Release.props +++ b/eng/targets/Release.props @@ -17,7 +17,7 @@ - 1.16.0 + 1.16.1 diff --git a/test/Grpc.IntegrationTests/OrchestrationErrorHandling.cs b/test/Grpc.IntegrationTests/OrchestrationErrorHandling.cs index 2235efd65..b4a687a65 100644 --- a/test/Grpc.IntegrationTests/OrchestrationErrorHandling.cs +++ b/test/Grpc.IntegrationTests/OrchestrationErrorHandling.cs @@ -733,6 +733,138 @@ void MyActivityImpl(TaskActivityContext ctx) => Assert.Null(innerFailure.Properties["NullProperty"]); } + /// + /// Tests that exception properties are included in FailureDetails when an orchestration + /// throws exception directly without calling any other functions and a custom provider is set. + /// + [Fact] + public async Task OrchestrationDirectArgumentOutOfRangeExceptionProperties() + { + TaskName orchestratorName = "OrchestrationWithDirectArgumentException"; + string paramName = "testParameter"; + string actualValue = "invalidValue"; + string errorMessage = $"Parameter '{paramName}' is out of range."; + + void MyOrchestrationImpl(TaskOrchestrationContext ctx) => + throw new ArgumentOutOfRangeException(paramName, actualValue, errorMessage); + + await using HostTestLifetime server = await this.StartWorkerAsync(b => + { + // Register the custom exception properties provider + b.Services.AddSingleton(); + + b.AddTasks(tasks => tasks + .AddOrchestratorFunc(orchestratorName, MyOrchestrationImpl)); + }); + + string instanceId = await server.Client.ScheduleNewOrchestrationInstanceAsync(orchestratorName); + OrchestrationMetadata metadata = await server.Client.WaitForInstanceCompletionAsync( + instanceId, getInputsAndOutputs: true, this.TimeoutToken); + + Assert.NotNull(metadata); + Assert.Equal(instanceId, metadata.InstanceId); + Assert.Equal(OrchestrationRuntimeStatus.Failed, metadata.RuntimeStatus); + + Assert.NotNull(metadata.FailureDetails); + TaskFailureDetails failureDetails = metadata.FailureDetails!; + Assert.Equal(typeof(ArgumentOutOfRangeException).FullName, failureDetails.ErrorType); + Assert.Contains(errorMessage, failureDetails.ErrorMessage); + + // Check that custom properties are included for ArgumentOutOfRangeException + Assert.NotNull(failureDetails.Properties); + Assert.Equal(2, failureDetails.Properties.Count); + + // Verify parameter name property + Assert.True(failureDetails.Properties.ContainsKey("Name")); + Assert.Equal(paramName, failureDetails.Properties["Name"]); + + // Verify actual value property + Assert.True(failureDetails.Properties.ContainsKey("Value")); + Assert.Equal(actualValue, failureDetails.Properties["Value"]); + + // Verify the exception type is correctly identified + Assert.True(failureDetails.IsCausedBy()); + } + + /// + /// Tests that exception properties are included through nested orchestration calls when + /// a provider is set. + /// + [Fact] + public async Task NestedOrchestrationArgumentOutOfRangeExceptionProperties() + { + TaskName parentOrchestratorName = "ParentOrchestrationWithNestedArgumentException"; + TaskName subOrchestratorName = "SubOrchestrationWithArgumentException"; + TaskName activityName = "ActivityWithArgumentException"; + string paramName = "nestedParameter"; + string actualValue = "badNestedValue"; + string errorMessage = $"Nested parameter '{paramName}' is out of range."; + + async Task ParentOrchestrationImpl(TaskOrchestrationContext ctx) => + await ctx.CallSubOrchestratorAsync(subOrchestratorName); + + async Task SubOrchestrationImpl(TaskOrchestrationContext ctx) => + await ctx.CallActivityAsync(activityName); + + void ActivityImpl(TaskActivityContext ctx) => + throw new ArgumentOutOfRangeException(paramName, actualValue, errorMessage); + + await using HostTestLifetime server = await this.StartWorkerAsync(b => + { + // Register the custom exception properties provider + b.Services.AddSingleton(); + + b.AddTasks(tasks => tasks + .AddOrchestratorFunc(parentOrchestratorName, ParentOrchestrationImpl) + .AddOrchestratorFunc(subOrchestratorName, SubOrchestrationImpl) + .AddActivityFunc(activityName, ActivityImpl)); + }); + + string instanceId = await server.Client.ScheduleNewOrchestrationInstanceAsync(parentOrchestratorName); + OrchestrationMetadata metadata = await server.Client.WaitForInstanceCompletionAsync( + instanceId, getInputsAndOutputs: true, this.TimeoutToken); + + Assert.NotNull(metadata); + Assert.Equal(instanceId, metadata.InstanceId); + Assert.Equal(OrchestrationRuntimeStatus.Failed, metadata.RuntimeStatus); + + Assert.NotNull(metadata.FailureDetails); + TaskFailureDetails failureDetails = metadata.FailureDetails!; + + // The parent orchestration failed due to a TaskFailedException from the sub-orchestration + Assert.Equal(typeof(TaskFailedException).FullName, failureDetails.ErrorType); + Assert.Contains(subOrchestratorName, failureDetails.ErrorMessage); + + // Check the first level inner failure (sub-orchestration failure) + Assert.NotNull(failureDetails.InnerFailure); + TaskFailureDetails subOrchestrationFailure = failureDetails.InnerFailure!; + Assert.Equal(typeof(TaskFailedException).FullName, subOrchestrationFailure.ErrorType); + Assert.Contains(activityName, subOrchestrationFailure.ErrorMessage); + + // Check the second level inner failure (activity failure with ArgumentOutOfRangeException) + Assert.NotNull(subOrchestrationFailure.InnerFailure); + TaskFailureDetails activityFailure = subOrchestrationFailure.InnerFailure!; + Assert.Equal(typeof(ArgumentOutOfRangeException).FullName, activityFailure.ErrorType); + Assert.Contains(errorMessage, activityFailure.ErrorMessage); + + // Verify that the original ArgumentOutOfRangeException properties are preserved + Assert.NotNull(activityFailure.Properties); + Assert.Equal(2, activityFailure.Properties.Count); + + // Verify parameter name property + Assert.True(activityFailure.Properties.ContainsKey("Name")); + Assert.Equal(paramName, activityFailure.Properties["Name"]); + + // Verify actual value property + Assert.True(activityFailure.Properties.ContainsKey("Value")); + Assert.Equal(actualValue, activityFailure.Properties["Value"]); + + // Verify the exception type hierarchy is correctly identified + Assert.True(failureDetails.IsCausedBy()); + Assert.True(subOrchestrationFailure.IsCausedBy()); + Assert.True(activityFailure.IsCausedBy()); + } + [Serializable] class CustomException : Exception {