Skip to content

Commit d6ecb6c

Browse files
authored
Include exception properties in failure details when orchestration throws directly (#482)
* update * update comment * Update VersionPrefix to 1.16.1 * Update Microsoft.Azure.DurableTask.Core to version 3.5.1
1 parent ff78ef6 commit d6ecb6c

File tree

3 files changed

+134
-2
lines changed

3 files changed

+134
-2
lines changed

Directory.Packages.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929

3030
<!-- DurableTask Packages -->
3131
<ItemGroup>
32-
<PackageVersion Include="Microsoft.Azure.DurableTask.Core" Version="3.5.0" />
32+
<PackageVersion Include="Microsoft.Azure.DurableTask.Core" Version="3.5.1" />
3333
<PackageVersion Include="Microsoft.Azure.Functions.Worker.Extensions.DurableTask" Version="1.2.2" />
3434
</ItemGroup>
3535

eng/targets/Release.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
</PropertyGroup>
1818

1919
<PropertyGroup>
20-
<VersionPrefix>1.16.0</VersionPrefix>
20+
<VersionPrefix>1.16.1</VersionPrefix>
2121
<VersionSuffix></VersionSuffix>
2222
</PropertyGroup>
2323

test/Grpc.IntegrationTests/OrchestrationErrorHandling.cs

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -733,6 +733,138 @@ void MyActivityImpl(TaskActivityContext ctx) =>
733733
Assert.Null(innerFailure.Properties["NullProperty"]);
734734
}
735735

736+
/// <summary>
737+
/// Tests that exception properties are included in FailureDetails when an orchestration
738+
/// throws exception directly without calling any other functions and a custom provider is set.
739+
/// </summary>
740+
[Fact]
741+
public async Task OrchestrationDirectArgumentOutOfRangeExceptionProperties()
742+
{
743+
TaskName orchestratorName = "OrchestrationWithDirectArgumentException";
744+
string paramName = "testParameter";
745+
string actualValue = "invalidValue";
746+
string errorMessage = $"Parameter '{paramName}' is out of range.";
747+
748+
void MyOrchestrationImpl(TaskOrchestrationContext ctx) =>
749+
throw new ArgumentOutOfRangeException(paramName, actualValue, errorMessage);
750+
751+
await using HostTestLifetime server = await this.StartWorkerAsync(b =>
752+
{
753+
// Register the custom exception properties provider
754+
b.Services.AddSingleton<IExceptionPropertiesProvider, TestExceptionPropertiesProvider>();
755+
756+
b.AddTasks(tasks => tasks
757+
.AddOrchestratorFunc(orchestratorName, MyOrchestrationImpl));
758+
});
759+
760+
string instanceId = await server.Client.ScheduleNewOrchestrationInstanceAsync(orchestratorName);
761+
OrchestrationMetadata metadata = await server.Client.WaitForInstanceCompletionAsync(
762+
instanceId, getInputsAndOutputs: true, this.TimeoutToken);
763+
764+
Assert.NotNull(metadata);
765+
Assert.Equal(instanceId, metadata.InstanceId);
766+
Assert.Equal(OrchestrationRuntimeStatus.Failed, metadata.RuntimeStatus);
767+
768+
Assert.NotNull(metadata.FailureDetails);
769+
TaskFailureDetails failureDetails = metadata.FailureDetails!;
770+
Assert.Equal(typeof(ArgumentOutOfRangeException).FullName, failureDetails.ErrorType);
771+
Assert.Contains(errorMessage, failureDetails.ErrorMessage);
772+
773+
// Check that custom properties are included for ArgumentOutOfRangeException
774+
Assert.NotNull(failureDetails.Properties);
775+
Assert.Equal(2, failureDetails.Properties.Count);
776+
777+
// Verify parameter name property
778+
Assert.True(failureDetails.Properties.ContainsKey("Name"));
779+
Assert.Equal(paramName, failureDetails.Properties["Name"]);
780+
781+
// Verify actual value property
782+
Assert.True(failureDetails.Properties.ContainsKey("Value"));
783+
Assert.Equal(actualValue, failureDetails.Properties["Value"]);
784+
785+
// Verify the exception type is correctly identified
786+
Assert.True(failureDetails.IsCausedBy<ArgumentOutOfRangeException>());
787+
}
788+
789+
/// <summary>
790+
/// Tests that exception properties are included through nested orchestration calls when
791+
/// a provider is set.
792+
/// </summary>
793+
[Fact]
794+
public async Task NestedOrchestrationArgumentOutOfRangeExceptionProperties()
795+
{
796+
TaskName parentOrchestratorName = "ParentOrchestrationWithNestedArgumentException";
797+
TaskName subOrchestratorName = "SubOrchestrationWithArgumentException";
798+
TaskName activityName = "ActivityWithArgumentException";
799+
string paramName = "nestedParameter";
800+
string actualValue = "badNestedValue";
801+
string errorMessage = $"Nested parameter '{paramName}' is out of range.";
802+
803+
async Task ParentOrchestrationImpl(TaskOrchestrationContext ctx) =>
804+
await ctx.CallSubOrchestratorAsync(subOrchestratorName);
805+
806+
async Task SubOrchestrationImpl(TaskOrchestrationContext ctx) =>
807+
await ctx.CallActivityAsync(activityName);
808+
809+
void ActivityImpl(TaskActivityContext ctx) =>
810+
throw new ArgumentOutOfRangeException(paramName, actualValue, errorMessage);
811+
812+
await using HostTestLifetime server = await this.StartWorkerAsync(b =>
813+
{
814+
// Register the custom exception properties provider
815+
b.Services.AddSingleton<IExceptionPropertiesProvider, TestExceptionPropertiesProvider>();
816+
817+
b.AddTasks(tasks => tasks
818+
.AddOrchestratorFunc(parentOrchestratorName, ParentOrchestrationImpl)
819+
.AddOrchestratorFunc(subOrchestratorName, SubOrchestrationImpl)
820+
.AddActivityFunc(activityName, ActivityImpl));
821+
});
822+
823+
string instanceId = await server.Client.ScheduleNewOrchestrationInstanceAsync(parentOrchestratorName);
824+
OrchestrationMetadata metadata = await server.Client.WaitForInstanceCompletionAsync(
825+
instanceId, getInputsAndOutputs: true, this.TimeoutToken);
826+
827+
Assert.NotNull(metadata);
828+
Assert.Equal(instanceId, metadata.InstanceId);
829+
Assert.Equal(OrchestrationRuntimeStatus.Failed, metadata.RuntimeStatus);
830+
831+
Assert.NotNull(metadata.FailureDetails);
832+
TaskFailureDetails failureDetails = metadata.FailureDetails!;
833+
834+
// The parent orchestration failed due to a TaskFailedException from the sub-orchestration
835+
Assert.Equal(typeof(TaskFailedException).FullName, failureDetails.ErrorType);
836+
Assert.Contains(subOrchestratorName, failureDetails.ErrorMessage);
837+
838+
// Check the first level inner failure (sub-orchestration failure)
839+
Assert.NotNull(failureDetails.InnerFailure);
840+
TaskFailureDetails subOrchestrationFailure = failureDetails.InnerFailure!;
841+
Assert.Equal(typeof(TaskFailedException).FullName, subOrchestrationFailure.ErrorType);
842+
Assert.Contains(activityName, subOrchestrationFailure.ErrorMessage);
843+
844+
// Check the second level inner failure (activity failure with ArgumentOutOfRangeException)
845+
Assert.NotNull(subOrchestrationFailure.InnerFailure);
846+
TaskFailureDetails activityFailure = subOrchestrationFailure.InnerFailure!;
847+
Assert.Equal(typeof(ArgumentOutOfRangeException).FullName, activityFailure.ErrorType);
848+
Assert.Contains(errorMessage, activityFailure.ErrorMessage);
849+
850+
// Verify that the original ArgumentOutOfRangeException properties are preserved
851+
Assert.NotNull(activityFailure.Properties);
852+
Assert.Equal(2, activityFailure.Properties.Count);
853+
854+
// Verify parameter name property
855+
Assert.True(activityFailure.Properties.ContainsKey("Name"));
856+
Assert.Equal(paramName, activityFailure.Properties["Name"]);
857+
858+
// Verify actual value property
859+
Assert.True(activityFailure.Properties.ContainsKey("Value"));
860+
Assert.Equal(actualValue, activityFailure.Properties["Value"]);
861+
862+
// Verify the exception type hierarchy is correctly identified
863+
Assert.True(failureDetails.IsCausedBy<TaskFailedException>());
864+
Assert.True(subOrchestrationFailure.IsCausedBy<TaskFailedException>());
865+
Assert.True(activityFailure.IsCausedBy<ArgumentOutOfRangeException>());
866+
}
867+
736868
[Serializable]
737869
class CustomException : Exception
738870
{

0 commit comments

Comments
 (0)