diff --git a/Sagaway.Callback.Router/ISagawayActor.cs b/Sagaway.Callback.Router/ISagawayActor.cs
index 29c887f..8f9d50e 100644
--- a/Sagaway.Callback.Router/ISagawayActor.cs
+++ b/Sagaway.Callback.Router/ISagawayActor.cs
@@ -1,5 +1,4 @@
-using System.Linq.Expressions;
-using Dapr.Actors;
+using Dapr.Actors;
namespace Sagaway.Callback.Router;
diff --git a/Sagaway.Hosts.DaprActorHost/CallSubSagaOptions.cs b/Sagaway.Hosts.DaprActorHost/CallSubSagaOptions.cs
new file mode 100644
index 0000000..cc7ce19
--- /dev/null
+++ b/Sagaway.Hosts.DaprActorHost/CallSubSagaOptions.cs
@@ -0,0 +1,32 @@
+namespace Sagaway.Hosts.DaprActorHost;
+
+///
+/// Represents the options for configuring a sub-saga invocation call.
+///
+public record CallSubSagaOptions
+{
+ ///
+ /// Gets the name of the callback method in the main saga that should be invoked
+ /// once the sub-saga completes its operation.
+ ///
+ public required string CallbackMethodName { get; init; }
+
+ ///
+ /// Gets any additional metadata specific to the sub-saga invocation,
+ /// which will be included in the Dapr binding call context.
+ ///
+ public string CustomSagawayMetadata { get; init; } = string.Empty;
+
+ ///
+ /// Gets the custom binding name to use for the Dapr binding operation.
+ /// If not specified, the default binding name is used.
+ ///
+ public string UseBindingName { get; init; } = string.Empty;
+
+ ///
+ /// Gets a dictionary of additional metadata to include in the binding context.
+ /// These values will override any defaults if the same keys are present.
+ ///
+ // ReSharper disable once CollectionNeverUpdated.Global
+ public Dictionary? CustomBindingMetadata { get; init; } = [];
+}
\ No newline at end of file
diff --git a/Sagaway.Hosts.DaprActorHost/DaprActorHost.cs b/Sagaway.Hosts.DaprActorHost/DaprActorHost.cs
index c7948a4..bcfa406 100644
--- a/Sagaway.Hosts.DaprActorHost/DaprActorHost.cs
+++ b/Sagaway.Hosts.DaprActorHost/DaprActorHost.cs
@@ -529,6 +529,107 @@ public IDictionary CaptureCallbackContext(string callbackMethodN
}
+ ///
+ /// Invokes a method on a sub-saga actor using an expression to capture the method call.
+ /// Extracts the method name and parameters from the provided expression and sends them via Dapr binding.
+ /// Includes support for custom callback contexts, metadata, and binding options.
+ ///
+ /// The type of the interface of the sub-saga actor.
+ /// An expression that represents the method to invoke on the sub-saga actor.
+ /// The type name of the actor as registered in the Dapr actor runtime.
+ /// The unique identifier of the sub-saga actor.
+ ///
+ /// A set of additional options for the sub-saga call, including:
+ ///
+ /// - CallbackMethodName: The method to invoke in the main saga after the sub-saga completes.
+ /// - CustomSagawayMetadata: Metadata to include in the Dapr binding call context.
+ /// - CustomBindingMetadata: A dictionary of additional binding metadata, which will override default values if keys overlap.
+ /// - UseBindingName: Specifies an alternate binding name, defaulting to the callback binding name if not set.
+ ///
+ ///
+ /// A task representing the asynchronous operation of invoking the sub-saga actor.
+ ///
+ /// Example usage:
+ ///
+ /// await CallSubSagaAsync<ISubSaga>(
+ /// saga => saga.ExecuteAsync(param1, param2),
+ /// "SubSagaActor",
+ /// "SubSaga123",
+ /// new CallSubSagaOptions(
+ /// CallbackMethodName: "MainSagaCallback",
+ /// CustomSagawayMetadata: "SomeMetadata",
+ /// CustomBindingMetadata: new Dictionary<string, string>
+ /// {
+ /// { "key1", "value1" },
+ /// { "key2", "value2" }
+ /// },
+ /// UseBindingName: "customBindingName"));
+ ///
+ ///
+ protected async Task CallSubSagaAsync(Expression> methodExpression, string actorTypeName,
+ string newActorId, CallSubSagaOptions options)
+ where TSubSaga : ISagawayActor
+ {
+ _logger.LogInformation("Starting sub-saga with actor id {NewActorId} using method {CallbackMethodName}", newActorId, options.CallbackMethodName);
+
+ // Use the method name of StartSubSagaWithContextAsync to handle the sub-saga dispatch
+ var callbackContext = CaptureCallbackContext(options.CallbackMethodName);
+
+ // Extract method name and parameters from the expression
+ var methodCall = (MethodCallExpression)methodExpression.Body;
+ var methodName = methodCall.Method.Name;
+ var arguments = methodCall.Arguments.Select(a => Expression.Lambda(a).Compile().DynamicInvoke()).ToArray();
+
+ _logger.LogDebug("Extracted method {MethodName} with arguments for sub-saga", methodName);
+
+ // Prepare the SubSagaInvocationContext object
+ var invocationContext = new SubSagaInvocationContext
+ {
+ MethodName = methodName, // The target method to invoke in the sub-saga
+ CallbackContext = callbackContext,
+ ParametersJson = JsonSerializer.Serialize(arguments, GetJsonSerializerOptions())
+ };
+
+ var invokeDispatcherParameters = new Dictionary()
+ {
+ ["x-sagaway-dapr-callback-method-name"] = nameof(ProcessASubSagaCallAsync),
+ ["x-sagaway-dapr-actor-id"] = newActorId,
+ ["x-sagaway-dapr-actor-type"] = actorTypeName,
+ ["x-sagaway-dapr-message-dispatch-time"] = DateTime.UtcNow.ToString("o"), // ISO 8601 format
+ ["x-sagaway-dapr-custom-metadata"] = options.CustomSagawayMetadata,
+ };
+
+ LogDebugContext("Sub Saga call context", invokeDispatcherParameters);
+
+ if (options.CustomBindingMetadata is not null)
+ {
+ foreach (var (key, value) in options.CustomBindingMetadata)
+ {
+ invokeDispatcherParameters[key] = value;
+ }
+ }
+
+ _logger.LogInformation("Dispatching sub-saga invocation for method {MethodName}", methodName);
+
+ // Create a new DaprClient for the sub-saga invocation, so it will not use the preconfigured HttpClient with the default headers
+ var daprClientBuilder = new DaprClientBuilder();
+ var subSagaDaprClient = daprClientBuilder.Build(); // No custom headers for sub-saga
+
+ var bindingName = string.IsNullOrWhiteSpace(options.UseBindingName)
+ ? GetCallbackBindingName()
+ : options.UseBindingName;
+
+ // Dispatch the sub-saga invocation with a single parameter (invocationContext)
+ await subSagaDaprClient.InvokeBindingAsync(
+ bindingName,
+ "create", // Binding operation
+ invocationContext,
+ invokeDispatcherParameters
+ );
+
+ _logger.LogInformation("Sub-saga invocation dispatched successfully for method {MethodName}", methodName);
+ }
+
///
/// Call or start a sub-saga by invoking a method on a sub-saga actor using an expression to capture the method call.
/// The method extracts the function name and parameters from the provided expression, and sends them via Dapr binding,
@@ -548,7 +649,8 @@ public IDictionary CaptureCallbackContext(string callbackMethodN
///
/// This will invoke the "DoSomethingAsync" method on the sub-saga and allow for a callback to the "MainSagaCallback" method on completion.
///
- protected async Task CallSubSagaAsync(Expression> methodExpression, string actorTypeName,
+ [Obsolete("Use overload with options instead")]
+ protected async Task CallSubSagaAsync(Expression> methodExpression, string actorTypeName,
string newActorId, string callbackMethodName = "", string customMetadata = "")
where TSubSaga : ISagawayActor
{
diff --git a/Sagaway.IntegrationTests/Sagaway.IntegrationTests.OrchestrationService/Actors/Argument.cs b/Sagaway.IntegrationTests/Sagaway.IntegrationTests.OrchestrationService/Actors/Argument.cs
index c173a48..747a669 100644
--- a/Sagaway.IntegrationTests/Sagaway.IntegrationTests.OrchestrationService/Actors/Argument.cs
+++ b/Sagaway.IntegrationTests/Sagaway.IntegrationTests.OrchestrationService/Actors/Argument.cs
@@ -1,5 +1,4 @@
using System.Text.Json;
-using System.Text.Json.Nodes;
namespace Sagaway.IntegrationTests.OrchestrationService.Actors;
diff --git a/Sagaway.IntegrationTests/Sagaway.IntegrationTests.TestSubSagaCommunicationService/IMainSagaActor.cs b/Sagaway.IntegrationTests/Sagaway.IntegrationTests.TestSubSagaCommunicationService/IMainSagaActor.cs
index 76551b0..d09e942 100644
--- a/Sagaway.IntegrationTests/Sagaway.IntegrationTests.TestSubSagaCommunicationService/IMainSagaActor.cs
+++ b/Sagaway.IntegrationTests/Sagaway.IntegrationTests.TestSubSagaCommunicationService/IMainSagaActor.cs
@@ -1,4 +1,3 @@
-using Dapr.Actors;
using Sagaway.Callback.Router;
namespace Sagaway.IntegrationTests.TestSubSagaCommunicationService;
diff --git a/Sagaway.IntegrationTests/Sagaway.IntegrationTests.TestSubSagaCommunicationService/ISubSagaActor.cs b/Sagaway.IntegrationTests/Sagaway.IntegrationTests.TestSubSagaCommunicationService/ISubSagaActor.cs
index 7e0f736..2be84b9 100644
--- a/Sagaway.IntegrationTests/Sagaway.IntegrationTests.TestSubSagaCommunicationService/ISubSagaActor.cs
+++ b/Sagaway.IntegrationTests/Sagaway.IntegrationTests.TestSubSagaCommunicationService/ISubSagaActor.cs
@@ -1,4 +1,3 @@
-using Dapr.Actors;
using Sagaway.Callback.Router;
namespace Sagaway.IntegrationTests.TestSubSagaCommunicationService;
diff --git a/Sagaway.IntegrationTests/Sagaway.IntegrationTests.TestSubSagaCommunicationService/MainSagaActor.cs b/Sagaway.IntegrationTests/Sagaway.IntegrationTests.TestSubSagaCommunicationService/MainSagaActor.cs
index 19e65c2..1e1d247 100644
--- a/Sagaway.IntegrationTests/Sagaway.IntegrationTests.TestSubSagaCommunicationService/MainSagaActor.cs
+++ b/Sagaway.IntegrationTests/Sagaway.IntegrationTests.TestSubSagaCommunicationService/MainSagaActor.cs
@@ -1,5 +1,6 @@
using Dapr.Actors.Runtime;
using Sagaway.Hosts;
+using Sagaway.Hosts.DaprActorHost;
namespace Sagaway.IntegrationTests.TestSubSagaCommunicationService;
@@ -94,8 +95,12 @@ private async Task OnCallSubSagaAsync()
{
Logger.LogInformation("Start calling sub-saga...");
+ var subSagaOptions = new CallSubSagaOptions()
+ {
+ CallbackMethodName = nameof(OnAddResultAsync),
+ };
await CallSubSagaAsync(subSaga => subSaga.AddAsync(38, 4, TimeSpan.FromSeconds(5)),
- "SubSagaActor","Sub" + ActorHost.Id, nameof(OnAddResultAsync));
+ "SubSagaActor", "Sub" + ActorHost.Id, subSagaOptions);
}
private async Task OnAddResultAsync(AddResult addResult)
@@ -105,8 +110,12 @@ private async Task OnAddResultAsync(AddResult addResult)
if (addResult.Result == 42)
{
Logger.LogInformation("Telling SubSagaActor to complete...");
+ var subSagaOptions = new CallSubSagaOptions()
+ {
+ CallbackMethodName = nameof(OnSubSagaEndAsync),
+ };
await CallSubSagaAsync(subSaga => subSaga.DoneAsync(),
- "SubSagaActor", "Sub" + ActorHost.Id, nameof(OnSubSagaEndAsync));
+ "SubSagaActor", "Sub" + ActorHost.Id, subSagaOptions);
// Wait for the sub-saga to be fully done before marking the operation complete.
await ReportCompleteOperationOutcomeAsync(MainSagaActorOperations.CallSubSaga, true);